@quadrel-enterprise-ui/framework 20.12.0 → 20.14.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.
@@ -3,8 +3,8 @@ import { inject, ElementRef, Directive, InjectionToken, HostBinding, Input, View
3
3
  import { Dialog, DialogRef, DialogModule } from '@angular/cdk/dialog';
4
4
  import * as i1 from '@angular/common';
5
5
  import { CommonModule, NgFor, NgIf, NgClass, NgTemplateOutlet, AsyncPipe } from '@angular/common';
6
- import { BehaviorSubject, pairwise, from, switchMap, map as map$1, Subject, throwError, of, ReplaySubject, merge, fromEvent, isObservable, NEVER, Observable, EMPTY, shareReplay, Subscription, distinctUntilChanged as distinctUntilChanged$1, debounce, timer, startWith as startWith$1, debounceTime as debounceTime$1, takeUntil as takeUntil$1, firstValueFrom, combineLatest, concat, take as take$1, delay, tap as tap$1, first, scan, queueScheduler, filter as filter$1, concatMap as concatMap$1, exhaustMap, finalize, forkJoin, delayWhen, combineLatestWith, withLatestFrom, async } from 'rxjs';
7
- import { map, takeUntil, take, filter, catchError, debounceTime, startWith, distinctUntilChanged, concatMap, tap, skip, auditTime, observeOn, switchMap as switchMap$1, pairwise as pairwise$1, mergeMap, delay as delay$1 } from 'rxjs/operators';
6
+ import { BehaviorSubject, pairwise, from, switchMap, map as map$1, Subject, throwError, of, ReplaySubject, merge, fromEvent, isObservable, NEVER, Observable, EMPTY, shareReplay, Subscription, distinctUntilChanged as distinctUntilChanged$1, debounce, timer, startWith as startWith$1, debounceTime as debounceTime$1, takeUntil as takeUntil$1, firstValueFrom, combineLatest, concat, take as take$1, delay, tap as tap$1, first, scan, queueScheduler, filter as filter$1, concatMap as concatMap$1, exhaustMap, finalize, forkJoin, delayWhen, combineLatestWith, withLatestFrom as withLatestFrom$1, async } from 'rxjs';
7
+ import { map, takeUntil, take, filter, catchError, debounceTime, startWith, distinctUntilChanged, concatMap, tap, skip, auditTime, withLatestFrom, observeOn, switchMap as switchMap$1, pairwise as pairwise$1, mergeMap, delay as delay$1 } from 'rxjs/operators';
8
8
  import { v4 } from 'uuid';
9
9
  import * as i3 from '@ngx-translate/core';
10
10
  import { TranslateService, TranslateModule } from '@ngx-translate/core';
@@ -7118,9 +7118,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
7118
7118
  /**
7119
7119
  * Framework-wide funnel for syncing application state with the URL `?key=value` query string.
7120
7120
  *
7121
- * Several Quadrel features mirror their state to the URL the table's sort and pagination today,
7122
- * filter, search, and `qd-page-tabs` over time. Without coordination they each call
7123
- * `router.navigate(...)` directly, which produces two failure modes:
7121
+ * Several Quadrel features mirror their state to the URL via per-feature adapters. Without
7122
+ * coordination they each call `router.navigate(...)` directly, which produces two failure modes:
7124
7123
  *
7125
7124
  * 1. **Race**: two navigates issued in the same JS turn cancel each other. Only the last writer
7126
7125
  * wins; the URL silently loses the cancelled feature's params.
@@ -7150,7 +7149,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
7150
7149
  *
7151
7150
  * connect(state$: Observable<FilterState>, dispatch: (state: FilterState) => void): void {
7152
7151
  * if (!this._hub.isAvailable()) return;
7153
- * if (!this._hub.claim(['filter'], this, this._destroyRef)) return;
7152
+ * if (!this._hub.claim(['filter'], this, this._destroyRef, { name: 'Filter', plural: 'filters' })) return;
7154
7153
  *
7155
7154
  * this._hub
7156
7155
  * .queryParams()
@@ -7159,7 +7158,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
7159
7158
  *
7160
7159
  * state$
7161
7160
  * .pipe(takeUntilDestroyed(this._destroyRef))
7162
- * .subscribe(state => this._hub.write({ filter: serializeFilter(state) }, false));
7161
+ * .subscribe(state => this._hub.write({ filter: serializeFilter(state) }, { replaceUrl: false }));
7163
7162
  * }
7164
7163
  * }
7165
7164
  * ```
@@ -7187,6 +7186,7 @@ class QdRouterQueryParamHubService {
7187
7186
  _activatedRoute = inject(ActivatedRoute, { optional: true });
7188
7187
  _destroyRef = inject(DestroyRef);
7189
7188
  _ownership = new Map();
7189
+ _ownerLabels = new WeakMap();
7190
7190
  _destroyClaims = new WeakMap();
7191
7191
  _navigationSettledSubject = new ReplaySubject(1);
7192
7192
  _pendingParams = {};
@@ -7241,11 +7241,15 @@ class QdRouterQueryParamHubService {
7241
7241
  * @param owner Stable identity of the caller — usually `this`. Used as the registry key.
7242
7242
  * @param destroyRef Optional. If provided, the hub releases the keys automatically when the
7243
7243
  * `DestroyRef` fires. Pass the adapter's `inject(DestroyRef)` to make leaks impossible.
7244
+ * @param featureLabel Optional. User-facing display label for the adapter's owning feature. The
7245
+ * hub uses it to format ownership-conflict error messages so application developers see the
7246
+ * feature names they configured (e.g. `Filter`, `Page Tabs`, `Table`) instead of internal
7247
+ * adapter class names. When omitted, the hub falls back to a generic `another adapter` phrase.
7244
7248
  * @returns `true` if every requested key is now owned by `owner`. `false` if at least one key
7245
7249
  * was already owned by a different adapter; the registry is left untouched and `destroyRef`
7246
7250
  * is not registered.
7247
7251
  */
7248
- claim(keys, owner, destroyRef) {
7252
+ claim(keys, owner, destroyRef, featureLabel) {
7249
7253
  const conflicts = [];
7250
7254
  for (const key of keys) {
7251
7255
  const existing = this._ownership.get(key);
@@ -7253,17 +7257,34 @@ class QdRouterQueryParamHubService {
7253
7257
  conflicts.push(key);
7254
7258
  }
7255
7259
  if (conflicts.length > 0) {
7256
- const list = conflicts.map(k => `"${k}"`).join(', ');
7257
- console.error(`Quadrel Framework | QdRouterQueryParamHub - Query param ${list} ${conflicts.length === 1 ? 'is' : 'are'} ` +
7258
- `already owned by another adapter. Disable the conflicting feature's URL sync or move it to a ` +
7259
- `different param key.`);
7260
+ const existingOwner = this._ownership.get(conflicts[0]);
7261
+ const existingLabel = this._ownerLabels.get(existingOwner);
7262
+ console.error(this.formatOwnershipConflictMessage(conflicts, existingLabel));
7260
7263
  return false;
7261
7264
  }
7262
7265
  keys.forEach(key => this._ownership.set(key, owner));
7266
+ if (featureLabel)
7267
+ this._ownerLabels.set(owner, featureLabel);
7263
7268
  if (destroyRef && keys.length > 0)
7264
7269
  this.registerAutoRelease(owner, keys, destroyRef);
7265
7270
  return true;
7266
7271
  }
7272
+ formatOwnershipConflictMessage(conflicts, existingLabel) {
7273
+ const keysList = conflicts.map(key => `"${key}"`).join(', ');
7274
+ const paramHeader = conflicts.length === 1 ? 'Query param' : 'Query params';
7275
+ const verb = conflicts.length === 1 ? 'is' : 'are';
7276
+ const ownerPhrase = existingLabel?.name ?? 'another adapter';
7277
+ const lines = [
7278
+ `Quadrel Framework | Router Query Param Hub - ${paramHeader} ${keysList} ${verb} already owned by ${ownerPhrase}.`
7279
+ ];
7280
+ if (existingLabel) {
7281
+ lines.push(`Only one router-connected ${existingLabel.name} is allowed per view.`, `Please set connectWithRouter: false on all additional ${existingLabel.plural}.`);
7282
+ }
7283
+ else {
7284
+ lines.push("Disable the conflicting feature's URL sync or move it to a different param key.");
7285
+ }
7286
+ return lines.join('\n');
7287
+ }
7267
7288
  /**
7268
7289
  * Drops `owner`'s claim on the given keys. Keys not owned by `owner` are left untouched —
7269
7290
  * a foreign release call cannot steal another adapter's ownership.
@@ -7322,13 +7343,10 @@ class QdRouterQueryParamHubService {
7322
7343
  *
7323
7344
  * @param params Partial map of query-param keys to their new values. Missing keys are not touched.
7324
7345
  * An empty object short-circuits and is a no-op.
7325
- * @param replaceUrl `true` replaces the current history entry; `false` pushes a new entry.
7326
- * When several `write()` calls in the same microtask disagree, `true` wins (so an initial
7327
- * replace is preserved when a follow-up write would otherwise push). The escalation is
7328
- * per-batch — after each flush the pending flag resets to `false`, so writes that arrive
7329
- * in the next microtask start a fresh batch with their own `replaceUrl` argument.
7346
+ * @param options Write options see `QdRouterQueryParamWriteOptions` for the `replaceUrl`
7347
+ * semantics and per-batch escalation rules.
7330
7348
  */
7331
- write(params, replaceUrl) {
7349
+ write(params, options) {
7332
7350
  const router = this._router;
7333
7351
  const activatedRoute = this._activatedRoute;
7334
7352
  if (!router || !activatedRoute)
@@ -7336,7 +7354,7 @@ class QdRouterQueryParamHubService {
7336
7354
  if (Object.keys(params).length === 0)
7337
7355
  return;
7338
7356
  Object.assign(this._pendingParams, params);
7339
- this._pendingReplaceUrl = this._pendingReplaceUrl || replaceUrl;
7357
+ this._pendingReplaceUrl = this._pendingReplaceUrl || options.replaceUrl;
7340
7358
  if (this._flushScheduled)
7341
7359
  return;
7342
7360
  this._flushScheduled = true;
@@ -19059,7 +19077,12 @@ class QdFileTypeService {
19059
19077
  XLSX: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
19060
19078
  XLSM: 'application/vnd.ms-excel.sheet.macroenabled.12',
19061
19079
  ODS: 'application/vnd.oasis.opendocument.spreadsheet',
19062
- CSV: 'text/csv',
19080
+ CSV: [
19081
+ 'text/csv',
19082
+ 'application/csv',
19083
+ // Workaround for Firefox + Windows + Excel. See Excel's Windows registry entry [HKEY_LOCAL_MACHINE\SOFTWARE\Classes\.csv\Content Type]
19084
+ 'application/vnd.ms-excel'
19085
+ ],
19063
19086
  PPT: [
19064
19087
  'application/powerpoint',
19065
19088
  'application/mspowerpoint',
@@ -21200,99 +21223,86 @@ function unescape(escapedString) {
21200
21223
  return escapedString.replace(new RegExp(`\\\\(${SERIALIZATION_SYMBOLS.map(s => '\\' + s).join('|')})`, 'g'), '$1');
21201
21224
  }
21202
21225
 
21203
- // @ts-strict-ignore
21226
+ const FILTER_PARAM_NAME = 'filter';
21227
+ const OWNED_PARAMS$4 = [FILTER_PARAM_NAME];
21228
+ const FEATURE_LABEL$4 = { name: 'Filter', plural: 'filters' };
21229
+ /**
21230
+ * Per-view adapter that syncs `QdFilterComponent` selection with the URL `?filter=` query
21231
+ * param via `QdRouterQueryParamHubService`. Reads/parses on activation, writes on selection
21232
+ * change. Coordination, ownership, and write batching are delegated to the hub — see its
21233
+ * JSDoc for details. Behavior contracts live in `filter-router-connector.service.spec.ts`
21234
+ * and `router-query-param-hub.integration.cy.ts`.
21235
+ */
21204
21236
  class QdFilterRouterConnectorService {
21205
- filterService = inject(QdFilterService);
21206
- router = inject(Router, { optional: true });
21207
- activatedRoute = inject(ActivatedRoute, { optional: true });
21237
+ _filterService = inject(QdFilterService);
21238
+ _hub = inject(QdRouterQueryParamHubService);
21239
+ _destroyRef = inject(DestroyRef);
21208
21240
  _connectedFilter;
21209
- _activatedRouteSubscription;
21210
- _filterUrlParameterSubscription;
21211
- _navigationEnded$;
21241
+ _readSubscription;
21242
+ _writeSubscription;
21212
21243
  _activatedRouteCheckedSubject = new ReplaySubject(1);
21213
21244
  _activatedRouteChecked$ = this._activatedRouteCheckedSubject.asObservable();
21214
21245
  constructor() {
21215
- if (!this.router || !this.activatedRoute)
21216
- return;
21217
- const navigationPending$ = new ReplaySubject(1);
21218
- this._navigationEnded$ = navigationPending$.pipe(filter(navigationPending => !navigationPending));
21219
- if (this.router.navigated)
21220
- navigationPending$.next(false);
21221
- this.router.events.subscribe(event => {
21222
- switch (event.constructor) {
21223
- case NavigationStart:
21224
- navigationPending$.next(true);
21225
- break;
21226
- case NavigationEnd:
21227
- case NavigationCancel:
21228
- case NavigationError:
21229
- navigationPending$.next(false);
21230
- break;
21231
- }
21232
- });
21246
+ this._destroyRef.onDestroy(() => this.tearDownConnection());
21233
21247
  }
21234
21248
  connectFilterWithRouter(filterComponent) {
21235
- if (filterComponent.filterData?.connectWithRouter === false || !this.router || !this.activatedRoute) {
21249
+ if (filterComponent.filterData?.connectWithRouter === false || !this._hub.isAvailable())
21236
21250
  return of(false);
21237
- }
21238
- if (this._connectedFilter) {
21239
- console.error('Quadrel Framework | QdFilter - More than one filter with enabled "connectWithRouter" config is not allowed. ' +
21240
- 'Please set connectWithRouter to false except for one filter.');
21251
+ if (this._connectedFilter === filterComponent)
21252
+ return this._activatedRouteChecked$;
21253
+ if (!this._hub.claim(OWNED_PARAMS$4, this, this._destroyRef, FEATURE_LABEL$4))
21241
21254
  return of(false);
21242
- }
21243
21255
  this._connectedFilter = filterComponent;
21244
- this.setFilterSelectionFromUrl();
21245
- this.setUrlOnFilterSelection();
21256
+ this._activatedRouteCheckedSubject = new ReplaySubject(1);
21257
+ this._activatedRouteChecked$ = this._activatedRouteCheckedSubject.asObservable();
21258
+ this.subscribeToUrlChanges();
21259
+ this.subscribeToFilterChanges();
21246
21260
  return this._activatedRouteChecked$;
21247
21261
  }
21248
- setFilterSelectionFromUrl() {
21249
- this._activatedRouteSubscription = this._navigationEnded$
21250
- .pipe(switchMap(() => combineLatest([
21251
- this.activatedRoute.queryParams,
21252
- this.filterService.selectFilterDataById$(this._connectedFilter.filterId)
21253
- ])), take$1(1))
21254
- .subscribe(([queryParams, filterData]) => {
21255
- if (typeof queryParams.filter === 'string') {
21256
- this.filterService.setFilterSelection(this._connectedFilter.filterId, {
21257
- ...filterData
21258
- .map(category => ({ [category.category]: [] }))
21259
- .reduce((accumulator, emptyCategory) => ({ ...accumulator, ...emptyCategory }), {}),
21260
- ...parseFilterUrlParameter(queryParams.filter)
21262
+ tearDownConnection() {
21263
+ this._connectedFilter = undefined;
21264
+ this._readSubscription?.unsubscribe();
21265
+ this._writeSubscription?.unsubscribe();
21266
+ }
21267
+ subscribeToUrlChanges() {
21268
+ let isFirstEmit = true;
21269
+ this._readSubscription = this._hub.navigationSettled$
21270
+ .pipe(switchMap(() => this._hub.queryParams()), map(queryParams => queryParams[FILTER_PARAM_NAME]), distinctUntilChanged(), switchMap(rawFilter => this._filterService.selectFilterDataById$(this._connectedFilter.filterId).pipe(take(1), map(filterData => ({ rawFilter, filterData })))), takeUntilDestroyed(this._destroyRef))
21271
+ .subscribe(({ rawFilter, filterData }) => {
21272
+ const emptySelection = filterData
21273
+ .map(category => ({ [category.category]: [] }))
21274
+ .reduce((accumulator, emptyCategory) => ({ ...accumulator, ...emptyCategory }), {});
21275
+ if (typeof rawFilter === 'string') {
21276
+ this._filterService.setFilterSelection(this._connectedFilter.filterId, {
21277
+ ...emptySelection,
21278
+ ...parseFilterUrlParameter(rawFilter)
21261
21279
  });
21262
21280
  }
21263
- this._activatedRouteCheckedSubject.next(true);
21281
+ else if (!isFirstEmit) {
21282
+ this._filterService.setFilterSelection(this._connectedFilter.filterId, emptySelection);
21283
+ }
21284
+ if (isFirstEmit) {
21285
+ this._activatedRouteCheckedSubject.next(true);
21286
+ isFirstEmit = false;
21287
+ }
21264
21288
  });
21265
21289
  }
21266
- setUrlOnFilterSelection() {
21267
- this._filterUrlParameterSubscription = this.filterService
21290
+ subscribeToFilterChanges() {
21291
+ this._writeSubscription = this._filterService
21268
21292
  .getFilterUrlParameter$(this._connectedFilter.filterId)
21269
- .pipe(switchMap(filterUrlParameter => this._navigationEnded$.pipe(take$1(1), map(() => filterUrlParameter))))
21293
+ .pipe(distinctUntilChanged(), switchMap(filterUrlParameter => this._hub.navigationSettled$.pipe(take(1), map(() => filterUrlParameter))), takeUntilDestroyed(this._destroyRef))
21270
21294
  .subscribe(filterUrlParameter => {
21271
- this.router.navigate([], {
21272
- relativeTo: this.activatedRoute,
21273
- queryParams: {
21274
- filter: filterUrlParameter
21275
- },
21276
- queryParamsHandling: 'merge',
21277
- replaceUrl: true
21278
- });
21295
+ const target = filterUrlParameter || undefined;
21296
+ if (this._hub.snapshotQueryParam(FILTER_PARAM_NAME) === target)
21297
+ return;
21298
+ this._hub.write({ [FILTER_PARAM_NAME]: target }, { replaceUrl: false });
21279
21299
  });
21280
21300
  }
21281
- disconnectFilterFromRouter(filterComponent) {
21282
- if (this._connectedFilter !== filterComponent)
21283
- return;
21284
- this._connectedFilter = null;
21285
- this._activatedRouteSubscription?.unsubscribe();
21286
- this._filterUrlParameterSubscription?.unsubscribe();
21287
- }
21288
21301
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdFilterRouterConnectorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
21289
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdFilterRouterConnectorService, providedIn: 'root' });
21302
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdFilterRouterConnectorService });
21290
21303
  }
21291
21304
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdFilterRouterConnectorService, decorators: [{
21292
- type: Injectable,
21293
- args: [{
21294
- providedIn: 'root'
21295
- }]
21305
+ type: Injectable
21296
21306
  }], ctorParameters: () => [] });
21297
21307
 
21298
21308
  class QdDependentFiltersService {
@@ -21432,7 +21442,7 @@ class QdFilterComponent {
21432
21442
  _filterDataSubject = new BehaviorSubject(undefined);
21433
21443
  _hasPreselectionSubject = new ReplaySubject(1);
21434
21444
  _categoriesSubject = new ReplaySubject(1);
21435
- _filterConnectedWithRouterSubject = new ReplaySubject();
21445
+ _filterConnectedWithRouterSubject = new ReplaySubject(1);
21436
21446
  _filterConnectedWithRouter$ = this._filterConnectedWithRouterSubject.asObservable();
21437
21447
  set hasPreselection(hasPreselection) {
21438
21448
  this._hasPreselectionSubject.next(hasPreselection);
@@ -21470,7 +21480,6 @@ class QdFilterComponent {
21470
21480
  }
21471
21481
  }
21472
21482
  ngOnDestroy() {
21473
- this.filterRouterConnectorService.disconnectFilterFromRouter(this);
21474
21483
  this.dependentFiltersService.destroy();
21475
21484
  this._queryStringSubscription?.unsubscribe();
21476
21485
  this._postBodySubscription?.unsubscribe();
@@ -21605,11 +21614,11 @@ class QdFilterComponent {
21605
21614
  });
21606
21615
  }
21607
21616
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdFilterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
21608
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: QdFilterComponent, isStandalone: false, selector: "qd-filter", inputs: { filterData: "filterData", testId: ["data-test-id", "testId"] }, outputs: { queryStringOutput: "queryStringOutput", postBodyOutput: "postBodyOutput", valueChange: "valueChange" }, host: { properties: { "attr.data-test-id": "this.dataTestId" }, classAttribute: "qd-filter" }, providers: [QdDependentFiltersService], viewQueries: [{ propertyName: "filterCategory", predicate: i0.forwardRef(() => QdFilterCategoryComponent), descendants: true }], usesOnChanges: true, ngImport: i0, template: "<ng-container *ngIf=\"(categories$ | async)?.length > 0\">\n <ng-container\n *ngFor=\"let category of categories$ | async; let last = last; let length = count; let categoryIndex = index\"\n >\n <qd-filter-category\n [filterId]=\"filterId\"\n [isInstantFiltering]=\"isInstantFiltering\"\n [categoryIndex]=\"categoryIndex\"\n [lastCategory]=\"last\"\n [data-test-id]=\"testId + '-category-' + categoryIndex + '-' + category\"\n ></qd-filter-category>\n </ng-container>\n\n <div [class]=\"'qd-filter__action-buttons'\" *ngIf=\"!isInstantFiltering\">\n <button\n qdButton\n qdButtonLink\n [disabled]=\"disableFilterButton$ | async\"\n (click)=\"clickFilter()\"\n [color]=\"'primary'\"\n [data-test-id]=\"testId + '-submit-button'\"\n >\n {{ \"i18n.qd.container.toolbar.filter.filter\" | translate }}\n </button>\n\n <button\n qdButton\n qdButtonLink\n [disabled]=\"disableResetButton$ | async\"\n (click)=\"resetFilter()\"\n [color]=\"'secondary'\"\n [data-test-id]=\"testId + '-reset-button'\"\n >\n {{\n ((hasPreselection$ | async)\n ? \"i18n.qd.container.toolbar.filter.backToPreSelect\"\n : \"i18n.qd.container.toolbar.filter.reset\"\n ) | translate\n }}\n </button>\n </div>\n</ng-container>\n", styles: [":host{position:relative;display:flex;flex-wrap:wrap}:host .qd-filter__action-buttons{display:flex;flex-wrap:wrap-reverse}:host ::ng-deep .qd-button{position:relative;margin-bottom:1rem}:host ::ng-deep .qd-button:last-child{margin-left:1rem}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: QdButtonComponent, selector: "button[qdButton], a[qdButton], button[qd-button]", inputs: ["disabled", "color", "icon", "data-test-id", "additionalInfo"] }, { kind: "directive", type: QdButtonLinkDirective, selector: "button[qdButtonLink], a[qdButtonLink], button[qd-button-link]" }, { kind: "component", type: QdFilterCategoryComponent, selector: "qd-filter-category", inputs: ["filterId", "isInstantFiltering", "categoryIndex", "lastCategory", "data-test-id"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }, { kind: "pipe", type: i3.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
21617
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: QdFilterComponent, isStandalone: false, selector: "qd-filter", inputs: { filterData: "filterData", testId: ["data-test-id", "testId"] }, outputs: { queryStringOutput: "queryStringOutput", postBodyOutput: "postBodyOutput", valueChange: "valueChange" }, host: { properties: { "attr.data-test-id": "this.dataTestId" }, classAttribute: "qd-filter" }, providers: [QdDependentFiltersService, QdFilterRouterConnectorService], viewQueries: [{ propertyName: "filterCategory", predicate: i0.forwardRef(() => QdFilterCategoryComponent), descendants: true }], usesOnChanges: true, ngImport: i0, template: "<ng-container *ngIf=\"(categories$ | async)?.length > 0\">\n <ng-container\n *ngFor=\"let category of categories$ | async; let last = last; let length = count; let categoryIndex = index\"\n >\n <qd-filter-category\n [filterId]=\"filterId\"\n [isInstantFiltering]=\"isInstantFiltering\"\n [categoryIndex]=\"categoryIndex\"\n [lastCategory]=\"last\"\n [data-test-id]=\"testId + '-category-' + categoryIndex + '-' + category\"\n ></qd-filter-category>\n </ng-container>\n\n <div [class]=\"'qd-filter__action-buttons'\" *ngIf=\"!isInstantFiltering\">\n <button\n qdButton\n qdButtonLink\n [disabled]=\"disableFilterButton$ | async\"\n (click)=\"clickFilter()\"\n [color]=\"'primary'\"\n [data-test-id]=\"testId + '-submit-button'\"\n >\n {{ \"i18n.qd.container.toolbar.filter.filter\" | translate }}\n </button>\n\n <button\n qdButton\n qdButtonLink\n [disabled]=\"disableResetButton$ | async\"\n (click)=\"resetFilter()\"\n [color]=\"'secondary'\"\n [data-test-id]=\"testId + '-reset-button'\"\n >\n {{\n ((hasPreselection$ | async)\n ? \"i18n.qd.container.toolbar.filter.backToPreSelect\"\n : \"i18n.qd.container.toolbar.filter.reset\"\n ) | translate\n }}\n </button>\n </div>\n</ng-container>\n", styles: [":host{position:relative;display:flex;flex-wrap:wrap}:host .qd-filter__action-buttons{display:flex;flex-wrap:wrap-reverse}:host ::ng-deep .qd-button{position:relative;margin-bottom:1rem}:host ::ng-deep .qd-button:last-child{margin-left:1rem}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: QdButtonComponent, selector: "button[qdButton], a[qdButton], button[qd-button]", inputs: ["disabled", "color", "icon", "data-test-id", "additionalInfo"] }, { kind: "directive", type: QdButtonLinkDirective, selector: "button[qdButtonLink], a[qdButtonLink], button[qd-button-link]" }, { kind: "component", type: QdFilterCategoryComponent, selector: "qd-filter-category", inputs: ["filterId", "isInstantFiltering", "categoryIndex", "lastCategory", "data-test-id"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }, { kind: "pipe", type: i3.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
21609
21618
  }
21610
21619
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdFilterComponent, decorators: [{
21611
21620
  type: Component,
21612
- args: [{ selector: 'qd-filter', changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'qd-filter' }, providers: [QdDependentFiltersService], standalone: false, template: "<ng-container *ngIf=\"(categories$ | async)?.length > 0\">\n <ng-container\n *ngFor=\"let category of categories$ | async; let last = last; let length = count; let categoryIndex = index\"\n >\n <qd-filter-category\n [filterId]=\"filterId\"\n [isInstantFiltering]=\"isInstantFiltering\"\n [categoryIndex]=\"categoryIndex\"\n [lastCategory]=\"last\"\n [data-test-id]=\"testId + '-category-' + categoryIndex + '-' + category\"\n ></qd-filter-category>\n </ng-container>\n\n <div [class]=\"'qd-filter__action-buttons'\" *ngIf=\"!isInstantFiltering\">\n <button\n qdButton\n qdButtonLink\n [disabled]=\"disableFilterButton$ | async\"\n (click)=\"clickFilter()\"\n [color]=\"'primary'\"\n [data-test-id]=\"testId + '-submit-button'\"\n >\n {{ \"i18n.qd.container.toolbar.filter.filter\" | translate }}\n </button>\n\n <button\n qdButton\n qdButtonLink\n [disabled]=\"disableResetButton$ | async\"\n (click)=\"resetFilter()\"\n [color]=\"'secondary'\"\n [data-test-id]=\"testId + '-reset-button'\"\n >\n {{\n ((hasPreselection$ | async)\n ? \"i18n.qd.container.toolbar.filter.backToPreSelect\"\n : \"i18n.qd.container.toolbar.filter.reset\"\n ) | translate\n }}\n </button>\n </div>\n</ng-container>\n", styles: [":host{position:relative;display:flex;flex-wrap:wrap}:host .qd-filter__action-buttons{display:flex;flex-wrap:wrap-reverse}:host ::ng-deep .qd-button{position:relative;margin-bottom:1rem}:host ::ng-deep .qd-button:last-child{margin-left:1rem}\n"] }]
21621
+ args: [{ selector: 'qd-filter', changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'qd-filter' }, providers: [QdDependentFiltersService, QdFilterRouterConnectorService], standalone: false, template: "<ng-container *ngIf=\"(categories$ | async)?.length > 0\">\n <ng-container\n *ngFor=\"let category of categories$ | async; let last = last; let length = count; let categoryIndex = index\"\n >\n <qd-filter-category\n [filterId]=\"filterId\"\n [isInstantFiltering]=\"isInstantFiltering\"\n [categoryIndex]=\"categoryIndex\"\n [lastCategory]=\"last\"\n [data-test-id]=\"testId + '-category-' + categoryIndex + '-' + category\"\n ></qd-filter-category>\n </ng-container>\n\n <div [class]=\"'qd-filter__action-buttons'\" *ngIf=\"!isInstantFiltering\">\n <button\n qdButton\n qdButtonLink\n [disabled]=\"disableFilterButton$ | async\"\n (click)=\"clickFilter()\"\n [color]=\"'primary'\"\n [data-test-id]=\"testId + '-submit-button'\"\n >\n {{ \"i18n.qd.container.toolbar.filter.filter\" | translate }}\n </button>\n\n <button\n qdButton\n qdButtonLink\n [disabled]=\"disableResetButton$ | async\"\n (click)=\"resetFilter()\"\n [color]=\"'secondary'\"\n [data-test-id]=\"testId + '-reset-button'\"\n >\n {{\n ((hasPreselection$ | async)\n ? \"i18n.qd.container.toolbar.filter.backToPreSelect\"\n : \"i18n.qd.container.toolbar.filter.reset\"\n ) | translate\n }}\n </button>\n </div>\n</ng-container>\n", styles: [":host{position:relative;display:flex;flex-wrap:wrap}:host .qd-filter__action-buttons{display:flex;flex-wrap:wrap-reverse}:host ::ng-deep .qd-button{position:relative;margin-bottom:1rem}:host ::ng-deep .qd-button:last-child{margin-left:1rem}\n"] }]
21613
21622
  }], propDecorators: { filterData: [{
21614
21623
  type: Input
21615
21624
  }], testId: [{
@@ -22277,94 +22286,88 @@ function searchReducer(state, action) {
22277
22286
  }
22278
22287
 
22279
22288
  const PHRASE_PRESELECT_SEPARATOR = '|';
22289
+ const SEARCH_PARAM_NAME = 'search';
22290
+ const OWNED_PARAMS$3 = [SEARCH_PARAM_NAME];
22291
+ const FEATURE_LABEL$3 = { name: 'Search', plural: 'searches' };
22292
+ /**
22293
+ * Per-view adapter that syncs `QdSearchComponent` phrase and preselection with the URL
22294
+ * `?search=` query param via `QdRouterQueryParamHubService`. Reads/parses on activation,
22295
+ * writes when the search service emits a submit. Coordination, ownership, and write
22296
+ * batching are delegated to the hub — see its JSDoc for details. Behavior contracts live
22297
+ * in `search-router-connector.service.spec.ts` and
22298
+ * `router-query-param-hub.integration.cy.ts`.
22299
+ */
22280
22300
  class QdSearchRouterConnectorService {
22281
- router = inject(Router, { optional: true });
22282
- activatedRoute = inject(ActivatedRoute, { optional: true });
22301
+ _hub = inject(QdRouterQueryParamHubService);
22302
+ _destroyRef = inject(DestroyRef);
22283
22303
  _connectedSearch;
22284
- _activatedRouteSubscription;
22285
- _searchUrlParameterSubscription;
22286
- _navigationEnded$;
22304
+ _readSubscription;
22305
+ _writeSubscription;
22287
22306
  _activatedRouteCheckedSubject = new ReplaySubject(1);
22288
22307
  _activatedRouteChecked$ = this._activatedRouteCheckedSubject.asObservable();
22289
22308
  constructor() {
22290
- if (!this.router || !this.activatedRoute)
22291
- return;
22292
- const navigationPending$ = new ReplaySubject(1);
22293
- this._navigationEnded$ = navigationPending$.pipe(filter(navigationPending => !navigationPending));
22294
- if (this.router.navigated)
22295
- navigationPending$.next(false);
22296
- this.router.events.subscribe(event => {
22297
- switch (event.constructor) {
22298
- case NavigationStart:
22299
- navigationPending$.next(true);
22300
- break;
22301
- case NavigationEnd:
22302
- case NavigationCancel:
22303
- case NavigationError:
22304
- navigationPending$.next(false);
22305
- break;
22306
- }
22307
- });
22309
+ this._destroyRef.onDestroy(() => this.tearDownConnection());
22308
22310
  }
22309
22311
  connectSearchWithRouter(searchComponent) {
22310
- if (searchComponent.configData?.connectWithRouter !== true || !this.router || !this.activatedRoute) {
22312
+ if (searchComponent.configData?.connectWithRouter !== true || !this._hub.isAvailable())
22311
22313
  return of(false);
22312
- }
22313
- if (this._connectedSearch) {
22314
- console.error('Quadrel Framework | QdSearch - More than one search with enabled "connectWithRouter" config is not allowed. ' +
22315
- 'Please set connectWithRouter to false except for one search.');
22314
+ if (this._connectedSearch === searchComponent)
22315
+ return this._activatedRouteChecked$;
22316
+ if (!this._hub.claim(OWNED_PARAMS$3, this, this._destroyRef, FEATURE_LABEL$3))
22316
22317
  return of(false);
22317
- }
22318
22318
  this._connectedSearch = searchComponent;
22319
- this.setSearchPhraseFromUrl();
22320
- this.setUrlOnSearch();
22319
+ this._activatedRouteCheckedSubject = new ReplaySubject(1);
22320
+ this._activatedRouteChecked$ = this._activatedRouteCheckedSubject.asObservable();
22321
+ this.subscribeToUrlChanges();
22322
+ this.subscribeToSearchChanges();
22321
22323
  return this._activatedRouteChecked$;
22322
22324
  }
22323
- setSearchPhraseFromUrl() {
22324
- this._activatedRouteSubscription = this._navigationEnded$
22325
- .pipe(switchMap(() => this.activatedRoute.queryParams), take$1(1))
22326
- .subscribe((queryParams) => {
22327
- if (typeof queryParams.search === 'string') {
22328
- const [phrase, preSelect] = queryParams.search.split(this.getNotEscapedSplitRegExp()).map(this.unescape);
22329
- if (this._connectedSearch) {
22330
- this._connectedSearch.search = phrase;
22331
- this._connectedSearch.preSelect = preSelect || '';
22332
- }
22325
+ tearDownConnection() {
22326
+ this._connectedSearch = undefined;
22327
+ this._readSubscription?.unsubscribe();
22328
+ this._writeSubscription?.unsubscribe();
22329
+ }
22330
+ subscribeToUrlChanges() {
22331
+ let isFirstEmit = true;
22332
+ this._readSubscription = this._hub.navigationSettled$
22333
+ .pipe(switchMap(() => this._hub.queryParams()), map(queryParams => queryParams[SEARCH_PARAM_NAME]), distinctUntilChanged(), takeUntilDestroyed(this._destroyRef))
22334
+ .subscribe(rawSearch => {
22335
+ if (typeof rawSearch === 'string' && this._connectedSearch) {
22336
+ const [phrase, preSelect] = rawSearch
22337
+ .split(this.getNotEscapedSplitRegExp())
22338
+ .map(value => this.unescape(value));
22339
+ this._connectedSearch.search = phrase;
22340
+ this._connectedSearch.preSelect = preSelect || '';
22341
+ }
22342
+ else if (!isFirstEmit && this._connectedSearch) {
22343
+ this._connectedSearch.search = '';
22344
+ this._connectedSearch.preSelect = '';
22345
+ }
22346
+ if (isFirstEmit) {
22347
+ this._activatedRouteCheckedSubject.next(true);
22348
+ isFirstEmit = false;
22333
22349
  }
22334
- this._activatedRouteCheckedSubject.next(true);
22350
+ });
22351
+ }
22352
+ subscribeToSearchChanges() {
22353
+ this._writeSubscription = this._connectedSearch.searchService.searchPostBody$.pipe(switchMap(searchData => this._hub.navigationSettled$.pipe(take(1), map(() => searchData))), takeUntilDestroyed(this._destroyRef)).subscribe(({ phrase, preSelect }) => {
22354
+ const serialized = this.escape(phrase) + (preSelect ? PHRASE_PRESELECT_SEPARATOR + this.escape(preSelect) : '');
22355
+ const target = serialized || undefined;
22356
+ if (this._hub.snapshotQueryParam(SEARCH_PARAM_NAME) === target)
22357
+ return;
22358
+ this._hub.write({ [SEARCH_PARAM_NAME]: target }, { replaceUrl: false });
22335
22359
  });
22336
22360
  }
22337
22361
  getNotEscapedSplitRegExp() {
22338
22362
  return new RegExp('(?<!\\\\)\\' + PHRASE_PRESELECT_SEPARATOR);
22339
22363
  }
22340
- setUrlOnSearch() {
22341
- this._searchUrlParameterSubscription = this._connectedSearch?.searchService.searchPostBody$
22342
- .pipe(switchMap(searchData => this._navigationEnded$.pipe(take$1(1), map(() => searchData))))
22343
- .subscribe(({ phrase, preSelect }) => {
22344
- this.router.navigate([], {
22345
- relativeTo: this.activatedRoute,
22346
- queryParams: {
22347
- search: this.escape(phrase) + (preSelect ? PHRASE_PRESELECT_SEPARATOR + this.escape(preSelect) : '')
22348
- },
22349
- queryParamsHandling: 'merge',
22350
- replaceUrl: true
22351
- });
22352
- });
22353
- }
22354
- escape(string) {
22355
- if (!string)
22364
+ escape(value) {
22365
+ if (!value)
22356
22366
  return '';
22357
- return string.replace(new RegExp(`(\\${PHRASE_PRESELECT_SEPARATOR})`, 'g'), '\\$1');
22358
- }
22359
- unescape(escapedString) {
22360
- return escapedString.replace(new RegExp(`\\\\(${PHRASE_PRESELECT_SEPARATOR})`, 'g'), '$1');
22367
+ return value.replace(new RegExp(`(\\${PHRASE_PRESELECT_SEPARATOR})`, 'g'), '\\$1');
22361
22368
  }
22362
- disconnectSearchFromRouter(searchComponent) {
22363
- if (this._connectedSearch !== searchComponent)
22364
- return;
22365
- this._connectedSearch = null;
22366
- this._activatedRouteSubscription?.unsubscribe();
22367
- this._searchUrlParameterSubscription?.unsubscribe();
22369
+ unescape(escapedValue) {
22370
+ return escapedValue.replace(new RegExp(`\\\\(${PHRASE_PRESELECT_SEPARATOR})`, 'g'), '$1');
22368
22371
  }
22369
22372
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdSearchRouterConnectorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
22370
22373
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdSearchRouterConnectorService });
@@ -22561,7 +22564,7 @@ class QdSearchComponent {
22561
22564
  _search = '';
22562
22565
  _preSelect = '';
22563
22566
  _destroyed$ = new Subject();
22564
- _searchConnectedWithRouterSubject = new ReplaySubject();
22567
+ _searchConnectedWithRouterSubject = new ReplaySubject(1);
22565
22568
  _searchConnectedWithRouter$ = this._searchConnectedWithRouterSubject.asObservable();
22566
22569
  get inputConfig() {
22567
22570
  return {
@@ -22644,7 +22647,6 @@ class QdSearchComponent {
22644
22647
  }
22645
22648
  }
22646
22649
  ngOnDestroy() {
22647
- this.searchRouterConnectorService.disconnectSearchFromRouter(this);
22648
22650
  this._destroyed$.next();
22649
22651
  this._destroyed$.complete();
22650
22652
  }
@@ -23655,11 +23657,26 @@ var QdPaginatorDirection;
23655
23657
 
23656
23658
  const PAGE_PARAM_NAME = 'page';
23657
23659
  const SIZE_PARAM_NAME = 'size';
23658
- const OWNED_PARAMS$1 = [PAGE_PARAM_NAME, SIZE_PARAM_NAME];
23660
+ const OWNED_PARAMS$2 = [PAGE_PARAM_NAME, SIZE_PARAM_NAME];
23661
+ const FEATURE_LABEL$2 = { name: 'Table', plural: 'tables' };
23659
23662
  const SIZE_SANITY_MAX = 1000;
23663
+ /**
23664
+ * Per-view adapter that syncs `QdTablePaginatorComponent` page and size with the URL
23665
+ * `?page=` and `?size=` query params via `QdRouterQueryParamHubService`. Applies the URL
23666
+ * synchronously on connect (so cross-adapter store reducers cannot stomp on the deep-linked
23667
+ * page), then keeps store and URL in sync. Coordination, ownership, and write batching are
23668
+ * delegated to the hub — see its JSDoc for details. Behavior contracts live in
23669
+ * `pagination-router-connector.service.spec.ts` and `router-query-param-hub.integration.cy.ts`.
23670
+ *
23671
+ * Internal invariant: relies on `pageChangeInfo$()` and `totalCount$()` being
23672
+ * BehaviorSubject-backed selectors that emit their current value on subscribe. If either
23673
+ * becomes async (delay/debounce), the synchronous snapshot in `applyInitialUrlSynchronously`
23674
+ * stays at its initial default silently — the dispatch would always fire and the table would
23675
+ * flicker to "0 elements" on disconnect/reconnect cycles.
23676
+ */
23660
23677
  class QdTablePaginationRouterConnectorService {
23661
- hub = inject(QdRouterQueryParamHubService);
23662
- destroyRef = inject(DestroyRef);
23678
+ _hub = inject(QdRouterQueryParamHubService);
23679
+ _destroyRef = inject(DestroyRef);
23663
23680
  _connection;
23664
23681
  _readSubscription;
23665
23682
  _writeSubscription;
@@ -23667,15 +23684,12 @@ class QdTablePaginationRouterConnectorService {
23667
23684
  _activatedRouteChecked$ = this._activatedRouteCheckedSubject.asObservable();
23668
23685
  _hasInitialUrlBeenSet = false;
23669
23686
  constructor() {
23670
- this.destroyRef.onDestroy(() => {
23671
- this.tearDownConnection();
23672
- this.hub.release(OWNED_PARAMS$1, this);
23673
- });
23687
+ this._destroyRef.onDestroy(() => this.tearDownConnection());
23674
23688
  }
23675
23689
  connectPaginationWithRouter(paginator, tableStoreService) {
23676
- if (!paginator.shouldConnectWithRouter() || !this.hub.isAvailable())
23690
+ if (!paginator.shouldConnectWithRouter() || !this._hub.isAvailable())
23677
23691
  return of(false);
23678
- if (!this.hub.claim(OWNED_PARAMS$1, this))
23692
+ if (!this._hub.claim(OWNED_PARAMS$2, this, this._destroyRef, FEATURE_LABEL$2))
23679
23693
  return of(false);
23680
23694
  this._connection = {
23681
23695
  paginator: paginator,
@@ -23684,15 +23698,32 @@ class QdTablePaginationRouterConnectorService {
23684
23698
  this._hasInitialUrlBeenSet = false;
23685
23699
  this._activatedRouteCheckedSubject = new ReplaySubject(1);
23686
23700
  this._activatedRouteChecked$ = this._activatedRouteCheckedSubject.asObservable();
23701
+ this.applyInitialUrlSynchronously();
23687
23702
  this.subscribeToUrlChanges();
23688
23703
  this.subscribeToStoreChanges();
23689
23704
  return this._activatedRouteChecked$;
23690
23705
  }
23691
- disconnectPaginationFromRouter(paginator) {
23692
- if (this._connection?.paginator !== paginator)
23706
+ applyInitialUrlSynchronously() {
23707
+ const { tableStoreService, paginator } = this._connection;
23708
+ const params = this.parseUrlParams({
23709
+ [PAGE_PARAM_NAME]: this._hub.snapshotQueryParam(PAGE_PARAM_NAME),
23710
+ [SIZE_PARAM_NAME]: this._hub.snapshotQueryParam(SIZE_PARAM_NAME)
23711
+ });
23712
+ const pageSize = params.size ?? paginator.getPageSizeDefault();
23713
+ const pageIndex = params.page !== undefined ? params.page - 1 : 0;
23714
+ let currentInfo;
23715
+ let currentTotalCount = 0;
23716
+ tableStoreService
23717
+ .pageChangeInfo$()
23718
+ .pipe(take(1))
23719
+ .subscribe(info => (currentInfo = info));
23720
+ tableStoreService
23721
+ .totalCount$()
23722
+ .pipe(take(1))
23723
+ .subscribe(value => (currentTotalCount = value));
23724
+ if (currentInfo && currentInfo.pageIndex === pageIndex && currentInfo.pageSize === pageSize)
23693
23725
  return;
23694
- this.tearDownConnection();
23695
- this.hub.release(OWNED_PARAMS$1, this);
23726
+ tableStoreService.setPageParams(pageIndex, pageSize, currentTotalCount);
23696
23727
  }
23697
23728
  tearDownConnection() {
23698
23729
  this._connection = undefined;
@@ -23703,8 +23734,8 @@ class QdTablePaginationRouterConnectorService {
23703
23734
  subscribeToUrlChanges() {
23704
23735
  const { tableStoreService } = this._connection;
23705
23736
  let isFirstEmit = true;
23706
- this._readSubscription = this.hub.navigationSettled$
23707
- .pipe(switchMap(() => this.hub.queryParams()), map(queryParams => this.parseUrlParams(queryParams)), distinctUntilChanged((a, b) => a.page === b.page && a.size === b.size), switchMap(params => combineLatest([tableStoreService.pageChangeInfo$(), tableStoreService.totalCount$()]).pipe(take(1), map(([currentInfo, totalCount]) => ({ params, currentInfo, totalCount })))))
23737
+ this._readSubscription = this._hub.navigationSettled$
23738
+ .pipe(switchMap(() => this._hub.queryParams()), map(queryParams => this.parseUrlParams(queryParams)), distinctUntilChanged((a, b) => a.page === b.page && a.size === b.size), switchMap(params => combineLatest([tableStoreService.pageChangeInfo$(), tableStoreService.totalCount$()]).pipe(take(1), map(([currentInfo, totalCount]) => ({ params, currentInfo, totalCount })))))
23708
23739
  .subscribe(({ params, currentInfo, totalCount }) => {
23709
23740
  const target = this.toStoreState(params);
23710
23741
  const isChanged = !currentInfo || currentInfo.pageIndex !== target.pageIndex || currentInfo.pageSize !== target.pageSize;
@@ -23721,7 +23752,7 @@ class QdTablePaginationRouterConnectorService {
23721
23752
  const { tableStoreService } = this._connection;
23722
23753
  this._writeSubscription = tableStoreService
23723
23754
  .pageChangeInfo$()
23724
- .pipe(filter((info) => info !== undefined), distinctUntilChanged((a, b) => a.pageIndex === b.pageIndex && a.pageSize === b.pageSize), switchMap(info => this.hub.navigationSettled$.pipe(take(1), map(() => info))))
23755
+ .pipe(filter((info) => info !== undefined), distinctUntilChanged((a, b) => a.pageIndex === b.pageIndex && a.pageSize === b.pageSize), switchMap(info => this._hub.navigationSettled$.pipe(take(1), map(() => info))))
23725
23756
  .subscribe(info => this.writeUrl(info.pageIndex, info.pageSize));
23726
23757
  }
23727
23758
  writeUrl(pageIndex, pageSize) {
@@ -23729,11 +23760,11 @@ class QdTablePaginationRouterConnectorService {
23729
23760
  this._hasInitialUrlBeenSet = true;
23730
23761
  const targetPage = String(pageIndex + 1);
23731
23762
  const targetSize = String(pageSize);
23732
- const urlAlreadyMatches = this.hub.snapshotQueryParam(PAGE_PARAM_NAME) === targetPage &&
23733
- this.hub.snapshotQueryParam(SIZE_PARAM_NAME) === targetSize;
23763
+ const urlAlreadyMatches = this._hub.snapshotQueryParam(PAGE_PARAM_NAME) === targetPage &&
23764
+ this._hub.snapshotQueryParam(SIZE_PARAM_NAME) === targetSize;
23734
23765
  if (urlAlreadyMatches)
23735
23766
  return;
23736
- this.hub.write({ [PAGE_PARAM_NAME]: targetPage, [SIZE_PARAM_NAME]: targetSize }, replaceUrl);
23767
+ this._hub.write({ [PAGE_PARAM_NAME]: targetPage, [SIZE_PARAM_NAME]: targetSize }, { replaceUrl });
23737
23768
  }
23738
23769
  parseUrlParams(queryParams) {
23739
23770
  const result = {};
@@ -23754,9 +23785,8 @@ class QdTablePaginationRouterConnectorService {
23754
23785
  }
23755
23786
  isValidSize(value) {
23756
23787
  const pageSizes = this.getConfiguredPageSizes();
23757
- if (pageSizes && pageSizes.length > 0) {
23788
+ if (pageSizes && pageSizes.length > 0)
23758
23789
  return pageSizes.includes(value);
23759
- }
23760
23790
  return value <= SIZE_SANITY_MAX;
23761
23791
  }
23762
23792
  getConfiguredPageSizes() {
@@ -24045,7 +24075,6 @@ class QdTablePaginatorComponent {
24045
24075
  this.startPagination();
24046
24076
  }
24047
24077
  ngOnDestroy() {
24048
- this.routerConnector.disconnectPaginationFromRouter(this);
24049
24078
  this._destroyed$.next(null);
24050
24079
  this._destroyed$.complete();
24051
24080
  }
@@ -24145,12 +24174,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
24145
24174
  }] } });
24146
24175
 
24147
24176
  const SORT_PARAM_NAME = 'sort';
24148
- const OWNED_PARAMS = [SORT_PARAM_NAME];
24177
+ const OWNED_PARAMS$1 = [SORT_PARAM_NAME];
24178
+ const FEATURE_LABEL$1 = { name: 'Table', plural: 'tables' };
24149
24179
  const SEGMENT_SEPARATOR = ',';
24150
24180
  const COLUMN_DIRECTION_SEPARATOR = '.';
24181
+ /**
24182
+ * Per-view adapter that syncs `QdTableComponent` sort state with the URL `?sort=` query
24183
+ * param via `QdRouterQueryParamHubService`. Reads/parses on activation (single segment
24184
+ * only; multi-segment URLs use the first valid entry and warn), writes when the store's
24185
+ * `tableSort$` selector emits. Coordination, ownership, and write batching are delegated
24186
+ * to the hub — see its JSDoc for details. Behavior contracts live in
24187
+ * `sort-router-connector.service.spec.ts` and `router-query-param-hub.integration.cy.ts`.
24188
+ *
24189
+ * Internal invariant: relies on `tableSort$()` being a BehaviorSubject-backed selector that
24190
+ * emits its current value on subscribe. The URL-read pipeline pairs each segments emission
24191
+ * with the latest sort snapshot via `withLatestFrom`; if the selector ever becomes async
24192
+ * (delay/debounce), `withLatestFrom` drops the outer emission until the snapshot arrives
24193
+ * and URL-driven sort changes are silently lost.
24194
+ */
24151
24195
  class QdTableSortRouterConnectorService {
24152
- hub = inject(QdRouterQueryParamHubService);
24153
- destroyRef = inject(DestroyRef);
24196
+ _hub = inject(QdRouterQueryParamHubService);
24197
+ _destroyRef = inject(DestroyRef);
24154
24198
  _connection;
24155
24199
  _readSubscription;
24156
24200
  _writeSubscription;
@@ -24159,15 +24203,12 @@ class QdTableSortRouterConnectorService {
24159
24203
  _hasInitialUrlBeenSet = false;
24160
24204
  _hasWarnedMultiSegment = false;
24161
24205
  constructor() {
24162
- this.destroyRef.onDestroy(() => {
24163
- this.tearDownConnection();
24164
- this.hub.release(OWNED_PARAMS, this);
24165
- });
24206
+ this._destroyRef.onDestroy(() => this.tearDownConnection());
24166
24207
  }
24167
24208
  connectSortWithRouter(table, tableStoreService) {
24168
- if (!this.shouldConnect(table) || !this.hub.isAvailable())
24209
+ if (!this.shouldConnect(table) || !this._hub.isAvailable())
24169
24210
  return of(false);
24170
- if (!this.hub.claim(OWNED_PARAMS, this))
24211
+ if (!this._hub.claim(OWNED_PARAMS$1, this, this._destroyRef, FEATURE_LABEL$1))
24171
24212
  return of(false);
24172
24213
  this._connection = {
24173
24214
  table: table,
@@ -24182,12 +24223,6 @@ class QdTableSortRouterConnectorService {
24182
24223
  this.subscribeToStoreChanges();
24183
24224
  return this._activatedRouteChecked$;
24184
24225
  }
24185
- disconnectSortFromRouter(table) {
24186
- if (this._connection?.table !== table)
24187
- return;
24188
- this.tearDownConnection();
24189
- this.hub.release(OWNED_PARAMS, this);
24190
- }
24191
24226
  tearDownConnection() {
24192
24227
  this._connection = undefined;
24193
24228
  this._hasInitialUrlBeenSet = false;
@@ -24198,12 +24233,14 @@ class QdTableSortRouterConnectorService {
24198
24233
  subscribeToUrlChanges() {
24199
24234
  const { tableStoreService } = this._connection;
24200
24235
  let isFirstEmit = true;
24201
- this._readSubscription = this.hub.navigationSettled$
24202
- .pipe(switchMap(() => this.hub.queryParams()), map(queryParams => (typeof queryParams[SORT_PARAM_NAME] === 'string' ? queryParams[SORT_PARAM_NAME] : '')), distinctUntilChanged(), map(raw => this.parseRawSortValue(raw)))
24203
- .subscribe(segments => {
24236
+ this._readSubscription = this._hub.navigationSettled$
24237
+ .pipe(switchMap(() => this._hub.queryParams()), map(queryParams => (typeof queryParams[SORT_PARAM_NAME] === 'string' ? queryParams[SORT_PARAM_NAME] : '')), distinctUntilChanged(), map(raw => this.parseRawSortValue(raw)), withLatestFrom(tableStoreService.tableSort$()))
24238
+ .subscribe(([segments, currentSort]) => {
24204
24239
  if (segments.length === 1) {
24205
24240
  const [{ column, direction }] = segments;
24206
- tableStoreService.setSort(column, direction);
24241
+ if (!this.isSameAsCurrentSort(currentSort, column, direction)) {
24242
+ tableStoreService.setSort(column, direction);
24243
+ }
24207
24244
  }
24208
24245
  if (isFirstEmit) {
24209
24246
  this._activatedRouteCheckedSubject.next(true);
@@ -24211,19 +24248,25 @@ class QdTableSortRouterConnectorService {
24211
24248
  }
24212
24249
  });
24213
24250
  }
24251
+ isSameAsCurrentSort(currentSort, column, direction) {
24252
+ if (!currentSort)
24253
+ return false;
24254
+ const active = currentSort.filter(entry => entry.direction !== QdSortDirection.NONE);
24255
+ return active.length === 1 && active[0].column === column && active[0].direction === direction;
24256
+ }
24214
24257
  subscribeToStoreChanges() {
24215
24258
  const { tableStoreService } = this._connection;
24216
24259
  this._writeSubscription = tableStoreService
24217
24260
  .tableSort$()
24218
- .pipe(filter((sort) => Array.isArray(sort)), map(sort => this.serializeSort(sort)), distinctUntilChanged(), switchMap(serialized => this.hub.navigationSettled$.pipe(take(1), map(() => serialized))))
24261
+ .pipe(filter((sort) => Array.isArray(sort)), map(sort => this.serializeSort(sort)), distinctUntilChanged(), switchMap(serialized => this._hub.navigationSettled$.pipe(take(1), map(() => serialized))))
24219
24262
  .subscribe(serialized => this.writeUrl(serialized));
24220
24263
  }
24221
24264
  writeUrl(serialized) {
24222
24265
  const replaceUrl = !this._hasInitialUrlBeenSet;
24223
24266
  this._hasInitialUrlBeenSet = true;
24224
- if (this.hub.snapshotQueryParam(SORT_PARAM_NAME) === serialized)
24267
+ if (this._hub.snapshotQueryParam(SORT_PARAM_NAME) === serialized)
24225
24268
  return;
24226
- this.hub.write({ [SORT_PARAM_NAME]: serialized }, replaceUrl);
24269
+ this._hub.write({ [SORT_PARAM_NAME]: serialized }, { replaceUrl });
24227
24270
  }
24228
24271
  shouldConnect(table) {
24229
24272
  const sortConfig = table.config.sort;
@@ -25039,7 +25082,6 @@ class QdTableComponent {
25039
25082
  }
25040
25083
  }
25041
25084
  ngOnDestroy() {
25042
- this.sortRouterConnector.disconnectSortFromRouter(this);
25043
25085
  this.tableStoreService.updateTableStateRecentSecondaryAction(undefined);
25044
25086
  this.tableStoreService.resetConnectorStates();
25045
25087
  this.fillingWidthService.destroy();
@@ -29174,6 +29216,149 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
29174
29216
  args: ['data-test-id']
29175
29217
  }] } });
29176
29218
 
29219
+ const TAB_PARAM_NAME = 'tab';
29220
+ const OWNED_PARAMS = [TAB_PARAM_NAME];
29221
+ const FEATURE_LABEL = { name: 'Page Tabs', plural: 'page tabs' };
29222
+ /**
29223
+ * Per-view adapter that syncs `QdPageTabsComponent` selection with the URL `?tab=` query
29224
+ * param via `QdRouterQueryParamHubService`. Reads on activation and selects the matching
29225
+ * tab (with fallback for unknown/disabled names), writes on `selectionChange`. Coordination,
29226
+ * ownership, and write batching are delegated to the hub — see its JSDoc for details.
29227
+ * Behavior contracts live in `page-tabs-router-connector.service.spec.ts` and
29228
+ * `router-query-param-hub.integration.cy.ts`.
29229
+ *
29230
+ * Note: this is the only connector that writes synchronously without gating on
29231
+ * `navigationSettled$`. Required for the deep-link case where `ngAfterViewInit` fires
29232
+ * before the initial NavigationEnd is replayed — gating there would hang the click-driven
29233
+ * write pipeline (see commit 57f0a271a).
29234
+ *
29235
+ * Internal invariant: when the requested tab is unknown or disabled, `selectFallbackTab()`
29236
+ * writes the URL explicitly. CdkStepper does not emit `selectionChange` if the fallback is
29237
+ * already the selected step, so without the explicit write deep-links like `?tab=phantom`
29238
+ * would leave a stale param in the URL.
29239
+ */
29240
+ class QdPageTabsRouterConnectorService {
29241
+ _hub = inject(QdRouterQueryParamHubService);
29242
+ _destroyRef = inject(DestroyRef);
29243
+ _connectedComponent;
29244
+ _readSubscription;
29245
+ _writeSubscription;
29246
+ _activatedRouteCheckedSubject = new ReplaySubject(1);
29247
+ _activatedRouteChecked$ = this._activatedRouteCheckedSubject.asObservable();
29248
+ _hasInitialUrlBeenSet = false;
29249
+ constructor() {
29250
+ this._destroyRef.onDestroy(() => this.tearDownConnection());
29251
+ }
29252
+ connectTabsWithRouter(component) {
29253
+ if (component.config?.connectWithRouter !== true || !this._hub.isAvailable())
29254
+ return of(false);
29255
+ if (this._connectedComponent === component)
29256
+ return this._activatedRouteChecked$;
29257
+ if (!this._hub.claim(OWNED_PARAMS, this, this._destroyRef, FEATURE_LABEL))
29258
+ return of(false);
29259
+ this._connectedComponent = component;
29260
+ this._hasInitialUrlBeenSet = false;
29261
+ this._activatedRouteCheckedSubject = new ReplaySubject(1);
29262
+ this._activatedRouteChecked$ = this._activatedRouteCheckedSubject.asObservable();
29263
+ this.subscribeToTabChanges();
29264
+ this.subscribeToUrlChanges();
29265
+ return this._activatedRouteChecked$;
29266
+ }
29267
+ tearDownConnection() {
29268
+ this._connectedComponent = undefined;
29269
+ this._hasInitialUrlBeenSet = false;
29270
+ this._readSubscription?.unsubscribe();
29271
+ this._writeSubscription?.unsubscribe();
29272
+ }
29273
+ subscribeToUrlChanges() {
29274
+ let isFirstEmit = true;
29275
+ this._readSubscription = this._hub.navigationSettled$
29276
+ .pipe(switchMap(() => this._hub.queryParams()), map(queryParams => queryParams[TAB_PARAM_NAME]), distinctUntilChanged(), takeUntilDestroyed(this._destroyRef))
29277
+ .subscribe(rawTabName => {
29278
+ this.applyTabFromUrl(rawTabName);
29279
+ if (isFirstEmit) {
29280
+ this._activatedRouteCheckedSubject.next(true);
29281
+ isFirstEmit = false;
29282
+ }
29283
+ });
29284
+ }
29285
+ subscribeToTabChanges() {
29286
+ const component = this._connectedComponent;
29287
+ this._writeSubscription = component.selectionChange
29288
+ .pipe(map(event => event.selectedStep?.config?.name), takeUntilDestroyed(this._destroyRef))
29289
+ .subscribe(name => this.writeUrl(name));
29290
+ }
29291
+ writeUrl(name) {
29292
+ if (!name) {
29293
+ console.warn('Quadrel Framework | QdPageTabs - "connectWithRouter" is active, however the selected <qd-page-tab> has no "name" attribute.');
29294
+ return;
29295
+ }
29296
+ const replaceUrl = !this._hasInitialUrlBeenSet;
29297
+ this._hasInitialUrlBeenSet = true;
29298
+ if (this._hub.snapshotQueryParam(TAB_PARAM_NAME) === name)
29299
+ return;
29300
+ this._hub.write({ [TAB_PARAM_NAME]: name }, { replaceUrl });
29301
+ }
29302
+ applyTabFromUrl(rawTabName) {
29303
+ const component = this._connectedComponent;
29304
+ if (!component)
29305
+ return;
29306
+ if (typeof rawTabName !== 'string' || rawTabName.length === 0) {
29307
+ this.selectFallbackTab();
29308
+ return;
29309
+ }
29310
+ const matchingTab = this.findTabByName(rawTabName);
29311
+ if (matchingTab && !matchingTab.config?.isDisabled) {
29312
+ matchingTab.select();
29313
+ return;
29314
+ }
29315
+ if (matchingTab) {
29316
+ console.warn('Quadrel Framework | QdPageTabs - Tab "' + rawTabName + '" is disabled and cannot be deep-linked.');
29317
+ }
29318
+ else {
29319
+ console.warn('Quadrel Framework | QdPageTabs - No tab found with name "' + rawTabName + '".');
29320
+ }
29321
+ this.selectFallbackTab();
29322
+ }
29323
+ selectFallbackTab() {
29324
+ const component = this._connectedComponent;
29325
+ if (!component)
29326
+ return;
29327
+ const fallbackIndex = this.findFallbackTabIndex();
29328
+ if (fallbackIndex === undefined)
29329
+ return;
29330
+ const fallback = component.tabs.get(fallbackIndex);
29331
+ if (!fallback)
29332
+ return;
29333
+ fallback.select();
29334
+ if (component.config)
29335
+ component.config.selectedIndex = fallbackIndex;
29336
+ this.writeUrl(fallback.config?.name);
29337
+ }
29338
+ findTabByName(name) {
29339
+ return this._connectedComponent?.tabs.find(tab => tab.config?.name === name);
29340
+ }
29341
+ findFallbackTabIndex() {
29342
+ const component = this._connectedComponent;
29343
+ if (!component)
29344
+ return undefined;
29345
+ const configuredIndex = component.config?.selectedIndex;
29346
+ if (typeof configuredIndex === 'number') {
29347
+ const candidate = component.tabs.get(configuredIndex);
29348
+ if (candidate && !candidate.config.isDisabled)
29349
+ return configuredIndex;
29350
+ }
29351
+ const tabsArray = component.tabs.toArray();
29352
+ const firstNonDisabledIndex = tabsArray.findIndex(tab => !tab.config.isDisabled);
29353
+ return firstNonDisabledIndex >= 0 ? firstNonDisabledIndex : undefined;
29354
+ }
29355
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPageTabsRouterConnectorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
29356
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPageTabsRouterConnectorService });
29357
+ }
29358
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPageTabsRouterConnectorService, decorators: [{
29359
+ type: Injectable
29360
+ }], ctorParameters: () => [] });
29361
+
29177
29362
  /**
29178
29363
  * **QdPageTabsComponent** provides a non-linear tabbed navigation system within a **QdPage**.
29179
29364
  * It enables switching between different sections while maintaining form validation and controlled navigation.
@@ -29211,13 +29396,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
29211
29396
  *
29212
29397
  * #### **Submit Button Configuration**
29213
29398
  *
29214
- * The submit button at the bottom of the tab system can be configured via `QdPageTabsConfig`:
29399
+ * The submit button at the bottom of the tab system can be configured via `QdPageTabsConfig.submitButton`:
29215
29400
  *
29216
29401
  * - **i18n**: The translated label for the submit button.
29217
29402
  * - **handler**: A callback function that receives form data upon submission.
29218
29403
  * - **isDisabled**: Controls whether the button should be active or not.
29219
29404
  * - **isHidden**: Determines whether the button is visible.
29220
- * - **connectWithRouter**: If set to true, the tab will be connected to the URL and will be bookmarked. It is obligatory to set the `name` attribute for each tab.
29405
+ *
29406
+ * #### **Router Integration**
29407
+ *
29408
+ * - **connectWithRouter**: If set to true, the active tab is synced with the URL via the `?tab=<name>` query param and becomes bookmarkable. Each `<qd-page-tab>` then needs a unique `name`.
29221
29409
  *
29222
29410
  * #### **Usage**
29223
29411
  *
@@ -29336,8 +29524,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
29336
29524
  class QdPageTabsComponent extends CdkStepper {
29337
29525
  footerService = inject(QdPageFooterService, { optional: true });
29338
29526
  pageStoreService = inject(QdPageStoreService);
29339
- router = inject(Router);
29340
- route = inject(ActivatedRoute, { optional: true });
29527
+ routerConnector = inject(QdPageTabsRouterConnectorService);
29341
29528
  /**
29342
29529
  * Configuration of QdPageTabs.
29343
29530
  */
@@ -29396,8 +29583,11 @@ class QdPageTabsComponent extends CdkStepper {
29396
29583
  ngAfterViewInit() {
29397
29584
  super.ngAfterViewInit();
29398
29585
  setTimeout(() => {
29586
+ if (this.config?.connectWithRouter) {
29587
+ this.configureBookmarkableTabs();
29588
+ return;
29589
+ }
29399
29590
  this.initializeTabSelection();
29400
- this.configureBookmarkableTabs();
29401
29591
  });
29402
29592
  }
29403
29593
  initializeTabSelection() {
@@ -29408,54 +29598,7 @@ class QdPageTabsComponent extends CdkStepper {
29408
29598
  this.selectFirstNotDisabledTab();
29409
29599
  }
29410
29600
  configureBookmarkableTabs() {
29411
- if (this.config?.connectWithRouter) {
29412
- this.initializeTabFromUrl();
29413
- this.initializeFirstTabSelection();
29414
- }
29415
- }
29416
- /**
29417
- * Initializes the tab selection based on the URL parameter 'tab'.
29418
- * @private
29419
- */
29420
- initializeTabFromUrl() {
29421
- this.route.queryParams.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(params => {
29422
- const tabNameFromParams = params['tab'];
29423
- const pageTab = this.tabs.find(tab => tab.config?.name === tabNameFromParams);
29424
- if (pageTab) {
29425
- pageTab.select();
29426
- }
29427
- else {
29428
- console.warn('Quadrel Framework | QdPageTabs - No tab found with name "' + tabNameFromParams + '".');
29429
- this.selectFirstNotDisabledTab(true);
29430
- }
29431
- });
29432
- }
29433
- /**
29434
- * If the user navigates to a page with a tab selected, the tab will be selected automatically in {@link initializeTabFromUrl()} method.
29435
- * If the user navigates to a page without a tab selected, the first active tab will be selected.
29436
- * @private
29437
- */
29438
- initializeFirstTabSelection() {
29439
- this.route.queryParams.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(params => {
29440
- const tabNameFromParams = params['tab'];
29441
- if (!tabNameFromParams) {
29442
- const selectedIndex = this.config.selectedIndex;
29443
- const pageTab = this.tabs.find((tab, tabIndex) => tabIndex === selectedIndex);
29444
- if (pageTab && pageTab?.config.name) {
29445
- this.router.navigate([], {
29446
- relativeTo: this.route,
29447
- queryParams: { tab: pageTab.config.name },
29448
- queryParamsHandling: 'merge'
29449
- });
29450
- }
29451
- else {
29452
- console.warn('Quadrel Framework | QdPageTabs - "connectedWithRouter" is active, however <qd-page-tab> has no "name" attribute.');
29453
- }
29454
- }
29455
- });
29456
- }
29457
- ngOnDestroy() {
29458
- super.ngOnDestroy();
29601
+ this.routerConnector.connectTabsWithRouter(this).pipe(takeUntilDestroyed(this.destroyRef)).subscribe();
29459
29602
  }
29460
29603
  isTabSelectable(index) {
29461
29604
  const tab = this.tabs.get(index);
@@ -29463,22 +29606,14 @@ class QdPageTabsComponent extends CdkStepper {
29463
29606
  }
29464
29607
  /**
29465
29608
  * Selects the first tab that is not disabled.
29466
- * @param updateUrl if true, the URL will be updated to reflect the selected tab.
29467
29609
  */
29468
- selectFirstNotDisabledTab(updateUrl = false) {
29610
+ selectFirstNotDisabledTab() {
29469
29611
  this.tabs.some((tab, tabIndex) => {
29470
29612
  if (!tab.config.isDisabled) {
29471
29613
  if (this.config) {
29472
29614
  this.config.selectedIndex = tabIndex;
29473
29615
  }
29474
29616
  this.selectedIndex = tabIndex;
29475
- if (updateUrl && this.config?.connectWithRouter && tab.config?.name) {
29476
- this.router.navigate([], {
29477
- relativeTo: this.route,
29478
- queryParams: { tab: tab.config.name },
29479
- queryParamsHandling: 'merge'
29480
- });
29481
- }
29482
29617
  return true;
29483
29618
  }
29484
29619
  else {
@@ -29495,13 +29630,6 @@ class QdPageTabsComponent extends CdkStepper {
29495
29630
  if (tab.config?.isDisabled)
29496
29631
  return;
29497
29632
  tab.select();
29498
- if (this.config?.connectWithRouter && tab.config?.name) {
29499
- this.router.navigate([], {
29500
- relativeTo: this.route,
29501
- queryParams: { tab: tab.config.name },
29502
- queryParamsHandling: 'merge'
29503
- });
29504
- }
29505
29633
  }
29506
29634
  isSubmitButtonShown() {
29507
29635
  return this.config?.submitButton?.isHidden !== true && !this.footerService;
@@ -29526,11 +29654,11 @@ class QdPageTabsComponent extends CdkStepper {
29526
29654
  });
29527
29655
  }
29528
29656
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPageTabsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
29529
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: QdPageTabsComponent, isStandalone: true, selector: "qd-page-tabs", inputs: { config: "config", testId: ["data-test-id", "testId"] }, outputs: { tabSelection: "tabSelection" }, host: { properties: { "class.standalone": "!footerService" }, classAttribute: "qd-tabs" }, providers: [{ provide: CdkStepper, useExisting: QdPageTabsComponent }], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: "<div class=\"qd-tabs-items-container\" [ngClass]=\"{ scrollable: config?.scrollabe }\">\n <qd-page-tab-header\n *ngFor=\"let tab of tabs; let i = index\"\n (click)=\"selectTab(tab)\"\n [id]=\"_getTabLabelId(i)\"\n [index]=\"i\"\n [state]=\"_getIndicatorType(i, tab.state)\"\n [label]=\"tab.config.label.i18n | translate\"\n [counters]=\"tab.config.counters\"\n [isSelected]=\"selectedIndex === i\"\n [isDisabled]=\"tab.config?.isDisabled\"\n [data-test-id]=\"testId + '-header' + '-' + i\"\n >\n </qd-page-tab-header>\n</div>\n\n<div class=\"tabs-content\">\n <ng-container *ngIf=\"!selected?.config?.isDisabled\" [ngTemplateOutlet]=\"selected?.content\"></ng-container>\n</div>\n\n<div class=\"qd-tabs-action-area\" *ngIf=\"isSubmitButtonShown()\">\n <button qdButton (click)=\"save()\" [disabled]=\"isSubmitButtonDisabled()\" [data-test-id]=\"testId + '-submit'\">\n {{ config?.submitButton?.i18n || \"i18n.qd.tabs.button.submit\" | translate }}\n </button>\n</div>\n", styles: [":host{display:flex;flex-direction:column}:host.standalone{height:calc(100% - 50px)}:host .qd-tabs-items-container{display:flex;width:100%;flex-direction:row;flex-wrap:wrap;padding:0 1.25rem;border-bottom:.125rem solid rgb(213,213,213);background-color:#fff;gap:0 .9375rem}@media (max-width: 599.98px){:host .qd-tabs-items-container{padding:0 .9375rem}}@media (max-width: 1279.98px){:host .qd-tabs-items-container.scrollable{flex-wrap:nowrap;overflow-x:scroll;overflow-y:hidden;scrollbar-width:none}}:host .tabs-content{flex:1 1 auto}:host .qd-tabs-action-area{display:flex;width:100%;height:50px;justify-content:flex-end}:host .qd-tabs-action-area button{margin-right:1.25rem}\n"], dependencies: [{ kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: QdPageTabHeaderComponent, selector: "qd-page-tab-header", inputs: ["state", "label", "counters", "index", "isSelected", "isDisabled", "data-test-id"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "ngmodule", type: QdButtonModule }, { kind: "component", type: QdButtonComponent, selector: "button[qdButton], a[qdButton], button[qd-button]", inputs: ["disabled", "color", "icon", "data-test-id", "additionalInfo"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "pipe", type: i3.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
29657
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: QdPageTabsComponent, isStandalone: true, selector: "qd-page-tabs", inputs: { config: "config", testId: ["data-test-id", "testId"] }, outputs: { tabSelection: "tabSelection" }, host: { properties: { "class.standalone": "!footerService" }, classAttribute: "qd-tabs" }, providers: [{ provide: CdkStepper, useExisting: QdPageTabsComponent }, QdPageTabsRouterConnectorService], usesInheritance: true, usesOnChanges: true, ngImport: i0, template: "<div class=\"qd-tabs-items-container\" [ngClass]=\"{ scrollable: config?.scrollabe }\">\n <qd-page-tab-header\n *ngFor=\"let tab of tabs; let i = index\"\n (click)=\"selectTab(tab)\"\n [id]=\"_getTabLabelId(i)\"\n [index]=\"i\"\n [state]=\"_getIndicatorType(i, tab.state)\"\n [label]=\"tab.config.label.i18n | translate\"\n [counters]=\"tab.config.counters\"\n [isSelected]=\"selectedIndex === i\"\n [isDisabled]=\"tab.config?.isDisabled\"\n [data-test-id]=\"testId + '-header' + '-' + i\"\n >\n </qd-page-tab-header>\n</div>\n\n<div class=\"tabs-content\">\n <ng-container *ngIf=\"!selected?.config?.isDisabled\" [ngTemplateOutlet]=\"selected?.content\"></ng-container>\n</div>\n\n<div class=\"qd-tabs-action-area\" *ngIf=\"isSubmitButtonShown()\">\n <button qdButton (click)=\"save()\" [disabled]=\"isSubmitButtonDisabled()\" [data-test-id]=\"testId + '-submit'\">\n {{ config?.submitButton?.i18n || \"i18n.qd.tabs.button.submit\" | translate }}\n </button>\n</div>\n", styles: [":host{display:flex;flex-direction:column}:host.standalone{height:calc(100% - 50px)}:host .qd-tabs-items-container{display:flex;width:100%;flex-direction:row;flex-wrap:wrap;padding:0 1.25rem;border-bottom:.125rem solid rgb(213,213,213);background-color:#fff;gap:0 .9375rem}@media (max-width: 599.98px){:host .qd-tabs-items-container{padding:0 .9375rem}}@media (max-width: 1279.98px){:host .qd-tabs-items-container.scrollable{flex-wrap:nowrap;overflow-x:scroll;overflow-y:hidden;scrollbar-width:none}}:host .tabs-content{flex:1 1 auto}:host .qd-tabs-action-area{display:flex;width:100%;height:50px;justify-content:flex-end}:host .qd-tabs-action-area button{margin-right:1.25rem}\n"], dependencies: [{ kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: QdPageTabHeaderComponent, selector: "qd-page-tab-header", inputs: ["state", "label", "counters", "index", "isSelected", "isDisabled", "data-test-id"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "ngmodule", type: QdButtonModule }, { kind: "component", type: QdButtonComponent, selector: "button[qdButton], a[qdButton], button[qd-button]", inputs: ["disabled", "color", "icon", "data-test-id", "additionalInfo"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "pipe", type: i3.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
29530
29658
  }
29531
29659
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPageTabsComponent, decorators: [{
29532
29660
  type: Component,
29533
- args: [{ selector: 'qd-page-tabs', providers: [{ provide: CdkStepper, useExisting: QdPageTabsComponent }], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'qd-tabs', '[class.standalone]': '!footerService' }, standalone: true, imports: [NgFor, NgIf, NgTemplateOutlet, QdPageTabHeaderComponent, TranslateModule, QdButtonModule, CommonModule], template: "<div class=\"qd-tabs-items-container\" [ngClass]=\"{ scrollable: config?.scrollabe }\">\n <qd-page-tab-header\n *ngFor=\"let tab of tabs; let i = index\"\n (click)=\"selectTab(tab)\"\n [id]=\"_getTabLabelId(i)\"\n [index]=\"i\"\n [state]=\"_getIndicatorType(i, tab.state)\"\n [label]=\"tab.config.label.i18n | translate\"\n [counters]=\"tab.config.counters\"\n [isSelected]=\"selectedIndex === i\"\n [isDisabled]=\"tab.config?.isDisabled\"\n [data-test-id]=\"testId + '-header' + '-' + i\"\n >\n </qd-page-tab-header>\n</div>\n\n<div class=\"tabs-content\">\n <ng-container *ngIf=\"!selected?.config?.isDisabled\" [ngTemplateOutlet]=\"selected?.content\"></ng-container>\n</div>\n\n<div class=\"qd-tabs-action-area\" *ngIf=\"isSubmitButtonShown()\">\n <button qdButton (click)=\"save()\" [disabled]=\"isSubmitButtonDisabled()\" [data-test-id]=\"testId + '-submit'\">\n {{ config?.submitButton?.i18n || \"i18n.qd.tabs.button.submit\" | translate }}\n </button>\n</div>\n", styles: [":host{display:flex;flex-direction:column}:host.standalone{height:calc(100% - 50px)}:host .qd-tabs-items-container{display:flex;width:100%;flex-direction:row;flex-wrap:wrap;padding:0 1.25rem;border-bottom:.125rem solid rgb(213,213,213);background-color:#fff;gap:0 .9375rem}@media (max-width: 599.98px){:host .qd-tabs-items-container{padding:0 .9375rem}}@media (max-width: 1279.98px){:host .qd-tabs-items-container.scrollable{flex-wrap:nowrap;overflow-x:scroll;overflow-y:hidden;scrollbar-width:none}}:host .tabs-content{flex:1 1 auto}:host .qd-tabs-action-area{display:flex;width:100%;height:50px;justify-content:flex-end}:host .qd-tabs-action-area button{margin-right:1.25rem}\n"] }]
29661
+ args: [{ selector: 'qd-page-tabs', providers: [{ provide: CdkStepper, useExisting: QdPageTabsComponent }, QdPageTabsRouterConnectorService], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'qd-tabs', '[class.standalone]': '!footerService' }, standalone: true, imports: [NgFor, NgIf, NgTemplateOutlet, QdPageTabHeaderComponent, TranslateModule, QdButtonModule, CommonModule], template: "<div class=\"qd-tabs-items-container\" [ngClass]=\"{ scrollable: config?.scrollabe }\">\n <qd-page-tab-header\n *ngFor=\"let tab of tabs; let i = index\"\n (click)=\"selectTab(tab)\"\n [id]=\"_getTabLabelId(i)\"\n [index]=\"i\"\n [state]=\"_getIndicatorType(i, tab.state)\"\n [label]=\"tab.config.label.i18n | translate\"\n [counters]=\"tab.config.counters\"\n [isSelected]=\"selectedIndex === i\"\n [isDisabled]=\"tab.config?.isDisabled\"\n [data-test-id]=\"testId + '-header' + '-' + i\"\n >\n </qd-page-tab-header>\n</div>\n\n<div class=\"tabs-content\">\n <ng-container *ngIf=\"!selected?.config?.isDisabled\" [ngTemplateOutlet]=\"selected?.content\"></ng-container>\n</div>\n\n<div class=\"qd-tabs-action-area\" *ngIf=\"isSubmitButtonShown()\">\n <button qdButton (click)=\"save()\" [disabled]=\"isSubmitButtonDisabled()\" [data-test-id]=\"testId + '-submit'\">\n {{ config?.submitButton?.i18n || \"i18n.qd.tabs.button.submit\" | translate }}\n </button>\n</div>\n", styles: [":host{display:flex;flex-direction:column}:host.standalone{height:calc(100% - 50px)}:host .qd-tabs-items-container{display:flex;width:100%;flex-direction:row;flex-wrap:wrap;padding:0 1.25rem;border-bottom:.125rem solid rgb(213,213,213);background-color:#fff;gap:0 .9375rem}@media (max-width: 599.98px){:host .qd-tabs-items-container{padding:0 .9375rem}}@media (max-width: 1279.98px){:host .qd-tabs-items-container.scrollable{flex-wrap:nowrap;overflow-x:scroll;overflow-y:hidden;scrollbar-width:none}}:host .tabs-content{flex:1 1 auto}:host .qd-tabs-action-area{display:flex;width:100%;height:50px;justify-content:flex-end}:host .qd-tabs-action-area button{margin-right:1.25rem}\n"] }]
29534
29662
  }], ctorParameters: () => [], propDecorators: { config: [{
29535
29663
  type: Input
29536
29664
  }], testId: [{
@@ -31925,7 +32053,7 @@ class QdShellHeaderComponent {
31925
32053
  const navigationService = this.navigationService;
31926
32054
  const shellLeftService = this.shellLeftService;
31927
32055
  const shellRightService = this.shellRightService;
31928
- this.backLinkDisplayed$ = navigationService.getPreviousHref$().pipe(withLatestFrom(navigationService.getRouteComponentInstance$()), switchMap(([previousHref, routeComponentInstance]) => {
32056
+ this.backLinkDisplayed$ = navigationService.getPreviousHref$().pipe(withLatestFrom$1(navigationService.getRouteComponentInstance$()), switchMap(([previousHref, routeComponentInstance]) => {
31929
32057
  if (typeof previousHref === 'function')
31930
32058
  previousHref = previousHref(routeComponentInstance);
31931
32059
  return isObservable(previousHref) ? previousHref.pipe(take(1)) : of(previousHref);