@quadrel-enterprise-ui/framework 20.12.0 → 20.13.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;
@@ -21200,99 +21218,86 @@ function unescape(escapedString) {
21200
21218
  return escapedString.replace(new RegExp(`\\\\(${SERIALIZATION_SYMBOLS.map(s => '\\' + s).join('|')})`, 'g'), '$1');
21201
21219
  }
21202
21220
 
21203
- // @ts-strict-ignore
21221
+ const FILTER_PARAM_NAME = 'filter';
21222
+ const OWNED_PARAMS$4 = [FILTER_PARAM_NAME];
21223
+ const FEATURE_LABEL$4 = { name: 'Filter', plural: 'filters' };
21224
+ /**
21225
+ * Per-view adapter that syncs `QdFilterComponent` selection with the URL `?filter=` query
21226
+ * param via `QdRouterQueryParamHubService`. Reads/parses on activation, writes on selection
21227
+ * change. Coordination, ownership, and write batching are delegated to the hub — see its
21228
+ * JSDoc for details. Behavior contracts live in `filter-router-connector.service.spec.ts`
21229
+ * and `router-query-param-hub.integration.cy.ts`.
21230
+ */
21204
21231
  class QdFilterRouterConnectorService {
21205
- filterService = inject(QdFilterService);
21206
- router = inject(Router, { optional: true });
21207
- activatedRoute = inject(ActivatedRoute, { optional: true });
21232
+ _filterService = inject(QdFilterService);
21233
+ _hub = inject(QdRouterQueryParamHubService);
21234
+ _destroyRef = inject(DestroyRef);
21208
21235
  _connectedFilter;
21209
- _activatedRouteSubscription;
21210
- _filterUrlParameterSubscription;
21211
- _navigationEnded$;
21236
+ _readSubscription;
21237
+ _writeSubscription;
21212
21238
  _activatedRouteCheckedSubject = new ReplaySubject(1);
21213
21239
  _activatedRouteChecked$ = this._activatedRouteCheckedSubject.asObservable();
21214
21240
  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
- });
21241
+ this._destroyRef.onDestroy(() => this.tearDownConnection());
21233
21242
  }
21234
21243
  connectFilterWithRouter(filterComponent) {
21235
- if (filterComponent.filterData?.connectWithRouter === false || !this.router || !this.activatedRoute) {
21244
+ if (filterComponent.filterData?.connectWithRouter === false || !this._hub.isAvailable())
21236
21245
  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.');
21246
+ if (this._connectedFilter === filterComponent)
21247
+ return this._activatedRouteChecked$;
21248
+ if (!this._hub.claim(OWNED_PARAMS$4, this, this._destroyRef, FEATURE_LABEL$4))
21241
21249
  return of(false);
21242
- }
21243
21250
  this._connectedFilter = filterComponent;
21244
- this.setFilterSelectionFromUrl();
21245
- this.setUrlOnFilterSelection();
21251
+ this._activatedRouteCheckedSubject = new ReplaySubject(1);
21252
+ this._activatedRouteChecked$ = this._activatedRouteCheckedSubject.asObservable();
21253
+ this.subscribeToUrlChanges();
21254
+ this.subscribeToFilterChanges();
21246
21255
  return this._activatedRouteChecked$;
21247
21256
  }
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)
21257
+ tearDownConnection() {
21258
+ this._connectedFilter = undefined;
21259
+ this._readSubscription?.unsubscribe();
21260
+ this._writeSubscription?.unsubscribe();
21261
+ }
21262
+ subscribeToUrlChanges() {
21263
+ let isFirstEmit = true;
21264
+ this._readSubscription = this._hub.navigationSettled$
21265
+ .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))
21266
+ .subscribe(({ rawFilter, filterData }) => {
21267
+ const emptySelection = filterData
21268
+ .map(category => ({ [category.category]: [] }))
21269
+ .reduce((accumulator, emptyCategory) => ({ ...accumulator, ...emptyCategory }), {});
21270
+ if (typeof rawFilter === 'string') {
21271
+ this._filterService.setFilterSelection(this._connectedFilter.filterId, {
21272
+ ...emptySelection,
21273
+ ...parseFilterUrlParameter(rawFilter)
21261
21274
  });
21262
21275
  }
21263
- this._activatedRouteCheckedSubject.next(true);
21276
+ else if (!isFirstEmit) {
21277
+ this._filterService.setFilterSelection(this._connectedFilter.filterId, emptySelection);
21278
+ }
21279
+ if (isFirstEmit) {
21280
+ this._activatedRouteCheckedSubject.next(true);
21281
+ isFirstEmit = false;
21282
+ }
21264
21283
  });
21265
21284
  }
21266
- setUrlOnFilterSelection() {
21267
- this._filterUrlParameterSubscription = this.filterService
21285
+ subscribeToFilterChanges() {
21286
+ this._writeSubscription = this._filterService
21268
21287
  .getFilterUrlParameter$(this._connectedFilter.filterId)
21269
- .pipe(switchMap(filterUrlParameter => this._navigationEnded$.pipe(take$1(1), map(() => filterUrlParameter))))
21288
+ .pipe(distinctUntilChanged(), switchMap(filterUrlParameter => this._hub.navigationSettled$.pipe(take(1), map(() => filterUrlParameter))), takeUntilDestroyed(this._destroyRef))
21270
21289
  .subscribe(filterUrlParameter => {
21271
- this.router.navigate([], {
21272
- relativeTo: this.activatedRoute,
21273
- queryParams: {
21274
- filter: filterUrlParameter
21275
- },
21276
- queryParamsHandling: 'merge',
21277
- replaceUrl: true
21278
- });
21290
+ const target = filterUrlParameter || undefined;
21291
+ if (this._hub.snapshotQueryParam(FILTER_PARAM_NAME) === target)
21292
+ return;
21293
+ this._hub.write({ [FILTER_PARAM_NAME]: target }, { replaceUrl: false });
21279
21294
  });
21280
21295
  }
21281
- disconnectFilterFromRouter(filterComponent) {
21282
- if (this._connectedFilter !== filterComponent)
21283
- return;
21284
- this._connectedFilter = null;
21285
- this._activatedRouteSubscription?.unsubscribe();
21286
- this._filterUrlParameterSubscription?.unsubscribe();
21287
- }
21288
21296
  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' });
21297
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdFilterRouterConnectorService });
21290
21298
  }
21291
21299
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdFilterRouterConnectorService, decorators: [{
21292
- type: Injectable,
21293
- args: [{
21294
- providedIn: 'root'
21295
- }]
21300
+ type: Injectable
21296
21301
  }], ctorParameters: () => [] });
21297
21302
 
21298
21303
  class QdDependentFiltersService {
@@ -21432,7 +21437,7 @@ class QdFilterComponent {
21432
21437
  _filterDataSubject = new BehaviorSubject(undefined);
21433
21438
  _hasPreselectionSubject = new ReplaySubject(1);
21434
21439
  _categoriesSubject = new ReplaySubject(1);
21435
- _filterConnectedWithRouterSubject = new ReplaySubject();
21440
+ _filterConnectedWithRouterSubject = new ReplaySubject(1);
21436
21441
  _filterConnectedWithRouter$ = this._filterConnectedWithRouterSubject.asObservable();
21437
21442
  set hasPreselection(hasPreselection) {
21438
21443
  this._hasPreselectionSubject.next(hasPreselection);
@@ -21470,7 +21475,6 @@ class QdFilterComponent {
21470
21475
  }
21471
21476
  }
21472
21477
  ngOnDestroy() {
21473
- this.filterRouterConnectorService.disconnectFilterFromRouter(this);
21474
21478
  this.dependentFiltersService.destroy();
21475
21479
  this._queryStringSubscription?.unsubscribe();
21476
21480
  this._postBodySubscription?.unsubscribe();
@@ -21605,11 +21609,11 @@ class QdFilterComponent {
21605
21609
  });
21606
21610
  }
21607
21611
  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 });
21612
+ 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
21613
  }
21610
21614
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdFilterComponent, decorators: [{
21611
21615
  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"] }]
21616
+ 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
21617
  }], propDecorators: { filterData: [{
21614
21618
  type: Input
21615
21619
  }], testId: [{
@@ -22277,94 +22281,88 @@ function searchReducer(state, action) {
22277
22281
  }
22278
22282
 
22279
22283
  const PHRASE_PRESELECT_SEPARATOR = '|';
22284
+ const SEARCH_PARAM_NAME = 'search';
22285
+ const OWNED_PARAMS$3 = [SEARCH_PARAM_NAME];
22286
+ const FEATURE_LABEL$3 = { name: 'Search', plural: 'searches' };
22287
+ /**
22288
+ * Per-view adapter that syncs `QdSearchComponent` phrase and preselection with the URL
22289
+ * `?search=` query param via `QdRouterQueryParamHubService`. Reads/parses on activation,
22290
+ * writes when the search service emits a submit. Coordination, ownership, and write
22291
+ * batching are delegated to the hub — see its JSDoc for details. Behavior contracts live
22292
+ * in `search-router-connector.service.spec.ts` and
22293
+ * `router-query-param-hub.integration.cy.ts`.
22294
+ */
22280
22295
  class QdSearchRouterConnectorService {
22281
- router = inject(Router, { optional: true });
22282
- activatedRoute = inject(ActivatedRoute, { optional: true });
22296
+ _hub = inject(QdRouterQueryParamHubService);
22297
+ _destroyRef = inject(DestroyRef);
22283
22298
  _connectedSearch;
22284
- _activatedRouteSubscription;
22285
- _searchUrlParameterSubscription;
22286
- _navigationEnded$;
22299
+ _readSubscription;
22300
+ _writeSubscription;
22287
22301
  _activatedRouteCheckedSubject = new ReplaySubject(1);
22288
22302
  _activatedRouteChecked$ = this._activatedRouteCheckedSubject.asObservable();
22289
22303
  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
- });
22304
+ this._destroyRef.onDestroy(() => this.tearDownConnection());
22308
22305
  }
22309
22306
  connectSearchWithRouter(searchComponent) {
22310
- if (searchComponent.configData?.connectWithRouter !== true || !this.router || !this.activatedRoute) {
22307
+ if (searchComponent.configData?.connectWithRouter !== true || !this._hub.isAvailable())
22311
22308
  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.');
22309
+ if (this._connectedSearch === searchComponent)
22310
+ return this._activatedRouteChecked$;
22311
+ if (!this._hub.claim(OWNED_PARAMS$3, this, this._destroyRef, FEATURE_LABEL$3))
22316
22312
  return of(false);
22317
- }
22318
22313
  this._connectedSearch = searchComponent;
22319
- this.setSearchPhraseFromUrl();
22320
- this.setUrlOnSearch();
22314
+ this._activatedRouteCheckedSubject = new ReplaySubject(1);
22315
+ this._activatedRouteChecked$ = this._activatedRouteCheckedSubject.asObservable();
22316
+ this.subscribeToUrlChanges();
22317
+ this.subscribeToSearchChanges();
22321
22318
  return this._activatedRouteChecked$;
22322
22319
  }
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
- }
22320
+ tearDownConnection() {
22321
+ this._connectedSearch = undefined;
22322
+ this._readSubscription?.unsubscribe();
22323
+ this._writeSubscription?.unsubscribe();
22324
+ }
22325
+ subscribeToUrlChanges() {
22326
+ let isFirstEmit = true;
22327
+ this._readSubscription = this._hub.navigationSettled$
22328
+ .pipe(switchMap(() => this._hub.queryParams()), map(queryParams => queryParams[SEARCH_PARAM_NAME]), distinctUntilChanged(), takeUntilDestroyed(this._destroyRef))
22329
+ .subscribe(rawSearch => {
22330
+ if (typeof rawSearch === 'string' && this._connectedSearch) {
22331
+ const [phrase, preSelect] = rawSearch
22332
+ .split(this.getNotEscapedSplitRegExp())
22333
+ .map(value => this.unescape(value));
22334
+ this._connectedSearch.search = phrase;
22335
+ this._connectedSearch.preSelect = preSelect || '';
22336
+ }
22337
+ else if (!isFirstEmit && this._connectedSearch) {
22338
+ this._connectedSearch.search = '';
22339
+ this._connectedSearch.preSelect = '';
22340
+ }
22341
+ if (isFirstEmit) {
22342
+ this._activatedRouteCheckedSubject.next(true);
22343
+ isFirstEmit = false;
22333
22344
  }
22334
- this._activatedRouteCheckedSubject.next(true);
22345
+ });
22346
+ }
22347
+ subscribeToSearchChanges() {
22348
+ this._writeSubscription = this._connectedSearch.searchService.searchPostBody$.pipe(switchMap(searchData => this._hub.navigationSettled$.pipe(take(1), map(() => searchData))), takeUntilDestroyed(this._destroyRef)).subscribe(({ phrase, preSelect }) => {
22349
+ const serialized = this.escape(phrase) + (preSelect ? PHRASE_PRESELECT_SEPARATOR + this.escape(preSelect) : '');
22350
+ const target = serialized || undefined;
22351
+ if (this._hub.snapshotQueryParam(SEARCH_PARAM_NAME) === target)
22352
+ return;
22353
+ this._hub.write({ [SEARCH_PARAM_NAME]: target }, { replaceUrl: false });
22335
22354
  });
22336
22355
  }
22337
22356
  getNotEscapedSplitRegExp() {
22338
22357
  return new RegExp('(?<!\\\\)\\' + PHRASE_PRESELECT_SEPARATOR);
22339
22358
  }
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)
22359
+ escape(value) {
22360
+ if (!value)
22356
22361
  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');
22362
+ return value.replace(new RegExp(`(\\${PHRASE_PRESELECT_SEPARATOR})`, 'g'), '\\$1');
22361
22363
  }
22362
- disconnectSearchFromRouter(searchComponent) {
22363
- if (this._connectedSearch !== searchComponent)
22364
- return;
22365
- this._connectedSearch = null;
22366
- this._activatedRouteSubscription?.unsubscribe();
22367
- this._searchUrlParameterSubscription?.unsubscribe();
22364
+ unescape(escapedValue) {
22365
+ return escapedValue.replace(new RegExp(`\\\\(${PHRASE_PRESELECT_SEPARATOR})`, 'g'), '$1');
22368
22366
  }
22369
22367
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdSearchRouterConnectorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
22370
22368
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdSearchRouterConnectorService });
@@ -22561,7 +22559,7 @@ class QdSearchComponent {
22561
22559
  _search = '';
22562
22560
  _preSelect = '';
22563
22561
  _destroyed$ = new Subject();
22564
- _searchConnectedWithRouterSubject = new ReplaySubject();
22562
+ _searchConnectedWithRouterSubject = new ReplaySubject(1);
22565
22563
  _searchConnectedWithRouter$ = this._searchConnectedWithRouterSubject.asObservable();
22566
22564
  get inputConfig() {
22567
22565
  return {
@@ -22644,7 +22642,6 @@ class QdSearchComponent {
22644
22642
  }
22645
22643
  }
22646
22644
  ngOnDestroy() {
22647
- this.searchRouterConnectorService.disconnectSearchFromRouter(this);
22648
22645
  this._destroyed$.next();
22649
22646
  this._destroyed$.complete();
22650
22647
  }
@@ -23655,11 +23652,26 @@ var QdPaginatorDirection;
23655
23652
 
23656
23653
  const PAGE_PARAM_NAME = 'page';
23657
23654
  const SIZE_PARAM_NAME = 'size';
23658
- const OWNED_PARAMS$1 = [PAGE_PARAM_NAME, SIZE_PARAM_NAME];
23655
+ const OWNED_PARAMS$2 = [PAGE_PARAM_NAME, SIZE_PARAM_NAME];
23656
+ const FEATURE_LABEL$2 = { name: 'Table', plural: 'tables' };
23659
23657
  const SIZE_SANITY_MAX = 1000;
23658
+ /**
23659
+ * Per-view adapter that syncs `QdTablePaginatorComponent` page and size with the URL
23660
+ * `?page=` and `?size=` query params via `QdRouterQueryParamHubService`. Applies the URL
23661
+ * synchronously on connect (so cross-adapter store reducers cannot stomp on the deep-linked
23662
+ * page), then keeps store and URL in sync. Coordination, ownership, and write batching are
23663
+ * delegated to the hub — see its JSDoc for details. Behavior contracts live in
23664
+ * `pagination-router-connector.service.spec.ts` and `router-query-param-hub.integration.cy.ts`.
23665
+ *
23666
+ * Internal invariant: relies on `pageChangeInfo$()` and `totalCount$()` being
23667
+ * BehaviorSubject-backed selectors that emit their current value on subscribe. If either
23668
+ * becomes async (delay/debounce), the synchronous snapshot in `applyInitialUrlSynchronously`
23669
+ * stays at its initial default silently — the dispatch would always fire and the table would
23670
+ * flicker to "0 elements" on disconnect/reconnect cycles.
23671
+ */
23660
23672
  class QdTablePaginationRouterConnectorService {
23661
- hub = inject(QdRouterQueryParamHubService);
23662
- destroyRef = inject(DestroyRef);
23673
+ _hub = inject(QdRouterQueryParamHubService);
23674
+ _destroyRef = inject(DestroyRef);
23663
23675
  _connection;
23664
23676
  _readSubscription;
23665
23677
  _writeSubscription;
@@ -23667,15 +23679,12 @@ class QdTablePaginationRouterConnectorService {
23667
23679
  _activatedRouteChecked$ = this._activatedRouteCheckedSubject.asObservable();
23668
23680
  _hasInitialUrlBeenSet = false;
23669
23681
  constructor() {
23670
- this.destroyRef.onDestroy(() => {
23671
- this.tearDownConnection();
23672
- this.hub.release(OWNED_PARAMS$1, this);
23673
- });
23682
+ this._destroyRef.onDestroy(() => this.tearDownConnection());
23674
23683
  }
23675
23684
  connectPaginationWithRouter(paginator, tableStoreService) {
23676
- if (!paginator.shouldConnectWithRouter() || !this.hub.isAvailable())
23685
+ if (!paginator.shouldConnectWithRouter() || !this._hub.isAvailable())
23677
23686
  return of(false);
23678
- if (!this.hub.claim(OWNED_PARAMS$1, this))
23687
+ if (!this._hub.claim(OWNED_PARAMS$2, this, this._destroyRef, FEATURE_LABEL$2))
23679
23688
  return of(false);
23680
23689
  this._connection = {
23681
23690
  paginator: paginator,
@@ -23684,15 +23693,32 @@ class QdTablePaginationRouterConnectorService {
23684
23693
  this._hasInitialUrlBeenSet = false;
23685
23694
  this._activatedRouteCheckedSubject = new ReplaySubject(1);
23686
23695
  this._activatedRouteChecked$ = this._activatedRouteCheckedSubject.asObservable();
23696
+ this.applyInitialUrlSynchronously();
23687
23697
  this.subscribeToUrlChanges();
23688
23698
  this.subscribeToStoreChanges();
23689
23699
  return this._activatedRouteChecked$;
23690
23700
  }
23691
- disconnectPaginationFromRouter(paginator) {
23692
- if (this._connection?.paginator !== paginator)
23701
+ applyInitialUrlSynchronously() {
23702
+ const { tableStoreService, paginator } = this._connection;
23703
+ const params = this.parseUrlParams({
23704
+ [PAGE_PARAM_NAME]: this._hub.snapshotQueryParam(PAGE_PARAM_NAME),
23705
+ [SIZE_PARAM_NAME]: this._hub.snapshotQueryParam(SIZE_PARAM_NAME)
23706
+ });
23707
+ const pageSize = params.size ?? paginator.getPageSizeDefault();
23708
+ const pageIndex = params.page !== undefined ? params.page - 1 : 0;
23709
+ let currentInfo;
23710
+ let currentTotalCount = 0;
23711
+ tableStoreService
23712
+ .pageChangeInfo$()
23713
+ .pipe(take(1))
23714
+ .subscribe(info => (currentInfo = info));
23715
+ tableStoreService
23716
+ .totalCount$()
23717
+ .pipe(take(1))
23718
+ .subscribe(value => (currentTotalCount = value));
23719
+ if (currentInfo && currentInfo.pageIndex === pageIndex && currentInfo.pageSize === pageSize)
23693
23720
  return;
23694
- this.tearDownConnection();
23695
- this.hub.release(OWNED_PARAMS$1, this);
23721
+ tableStoreService.setPageParams(pageIndex, pageSize, currentTotalCount);
23696
23722
  }
23697
23723
  tearDownConnection() {
23698
23724
  this._connection = undefined;
@@ -23703,8 +23729,8 @@ class QdTablePaginationRouterConnectorService {
23703
23729
  subscribeToUrlChanges() {
23704
23730
  const { tableStoreService } = this._connection;
23705
23731
  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 })))))
23732
+ this._readSubscription = this._hub.navigationSettled$
23733
+ .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
23734
  .subscribe(({ params, currentInfo, totalCount }) => {
23709
23735
  const target = this.toStoreState(params);
23710
23736
  const isChanged = !currentInfo || currentInfo.pageIndex !== target.pageIndex || currentInfo.pageSize !== target.pageSize;
@@ -23721,7 +23747,7 @@ class QdTablePaginationRouterConnectorService {
23721
23747
  const { tableStoreService } = this._connection;
23722
23748
  this._writeSubscription = tableStoreService
23723
23749
  .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))))
23750
+ .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
23751
  .subscribe(info => this.writeUrl(info.pageIndex, info.pageSize));
23726
23752
  }
23727
23753
  writeUrl(pageIndex, pageSize) {
@@ -23729,11 +23755,11 @@ class QdTablePaginationRouterConnectorService {
23729
23755
  this._hasInitialUrlBeenSet = true;
23730
23756
  const targetPage = String(pageIndex + 1);
23731
23757
  const targetSize = String(pageSize);
23732
- const urlAlreadyMatches = this.hub.snapshotQueryParam(PAGE_PARAM_NAME) === targetPage &&
23733
- this.hub.snapshotQueryParam(SIZE_PARAM_NAME) === targetSize;
23758
+ const urlAlreadyMatches = this._hub.snapshotQueryParam(PAGE_PARAM_NAME) === targetPage &&
23759
+ this._hub.snapshotQueryParam(SIZE_PARAM_NAME) === targetSize;
23734
23760
  if (urlAlreadyMatches)
23735
23761
  return;
23736
- this.hub.write({ [PAGE_PARAM_NAME]: targetPage, [SIZE_PARAM_NAME]: targetSize }, replaceUrl);
23762
+ this._hub.write({ [PAGE_PARAM_NAME]: targetPage, [SIZE_PARAM_NAME]: targetSize }, { replaceUrl });
23737
23763
  }
23738
23764
  parseUrlParams(queryParams) {
23739
23765
  const result = {};
@@ -23754,9 +23780,8 @@ class QdTablePaginationRouterConnectorService {
23754
23780
  }
23755
23781
  isValidSize(value) {
23756
23782
  const pageSizes = this.getConfiguredPageSizes();
23757
- if (pageSizes && pageSizes.length > 0) {
23783
+ if (pageSizes && pageSizes.length > 0)
23758
23784
  return pageSizes.includes(value);
23759
- }
23760
23785
  return value <= SIZE_SANITY_MAX;
23761
23786
  }
23762
23787
  getConfiguredPageSizes() {
@@ -24045,7 +24070,6 @@ class QdTablePaginatorComponent {
24045
24070
  this.startPagination();
24046
24071
  }
24047
24072
  ngOnDestroy() {
24048
- this.routerConnector.disconnectPaginationFromRouter(this);
24049
24073
  this._destroyed$.next(null);
24050
24074
  this._destroyed$.complete();
24051
24075
  }
@@ -24145,12 +24169,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
24145
24169
  }] } });
24146
24170
 
24147
24171
  const SORT_PARAM_NAME = 'sort';
24148
- const OWNED_PARAMS = [SORT_PARAM_NAME];
24172
+ const OWNED_PARAMS$1 = [SORT_PARAM_NAME];
24173
+ const FEATURE_LABEL$1 = { name: 'Table', plural: 'tables' };
24149
24174
  const SEGMENT_SEPARATOR = ',';
24150
24175
  const COLUMN_DIRECTION_SEPARATOR = '.';
24176
+ /**
24177
+ * Per-view adapter that syncs `QdTableComponent` sort state with the URL `?sort=` query
24178
+ * param via `QdRouterQueryParamHubService`. Reads/parses on activation (single segment
24179
+ * only; multi-segment URLs use the first valid entry and warn), writes when the store's
24180
+ * `tableSort$` selector emits. Coordination, ownership, and write batching are delegated
24181
+ * to the hub — see its JSDoc for details. Behavior contracts live in
24182
+ * `sort-router-connector.service.spec.ts` and `router-query-param-hub.integration.cy.ts`.
24183
+ *
24184
+ * Internal invariant: relies on `tableSort$()` being a BehaviorSubject-backed selector that
24185
+ * emits its current value on subscribe. The URL-read pipeline pairs each segments emission
24186
+ * with the latest sort snapshot via `withLatestFrom`; if the selector ever becomes async
24187
+ * (delay/debounce), `withLatestFrom` drops the outer emission until the snapshot arrives
24188
+ * and URL-driven sort changes are silently lost.
24189
+ */
24151
24190
  class QdTableSortRouterConnectorService {
24152
- hub = inject(QdRouterQueryParamHubService);
24153
- destroyRef = inject(DestroyRef);
24191
+ _hub = inject(QdRouterQueryParamHubService);
24192
+ _destroyRef = inject(DestroyRef);
24154
24193
  _connection;
24155
24194
  _readSubscription;
24156
24195
  _writeSubscription;
@@ -24159,15 +24198,12 @@ class QdTableSortRouterConnectorService {
24159
24198
  _hasInitialUrlBeenSet = false;
24160
24199
  _hasWarnedMultiSegment = false;
24161
24200
  constructor() {
24162
- this.destroyRef.onDestroy(() => {
24163
- this.tearDownConnection();
24164
- this.hub.release(OWNED_PARAMS, this);
24165
- });
24201
+ this._destroyRef.onDestroy(() => this.tearDownConnection());
24166
24202
  }
24167
24203
  connectSortWithRouter(table, tableStoreService) {
24168
- if (!this.shouldConnect(table) || !this.hub.isAvailable())
24204
+ if (!this.shouldConnect(table) || !this._hub.isAvailable())
24169
24205
  return of(false);
24170
- if (!this.hub.claim(OWNED_PARAMS, this))
24206
+ if (!this._hub.claim(OWNED_PARAMS$1, this, this._destroyRef, FEATURE_LABEL$1))
24171
24207
  return of(false);
24172
24208
  this._connection = {
24173
24209
  table: table,
@@ -24182,12 +24218,6 @@ class QdTableSortRouterConnectorService {
24182
24218
  this.subscribeToStoreChanges();
24183
24219
  return this._activatedRouteChecked$;
24184
24220
  }
24185
- disconnectSortFromRouter(table) {
24186
- if (this._connection?.table !== table)
24187
- return;
24188
- this.tearDownConnection();
24189
- this.hub.release(OWNED_PARAMS, this);
24190
- }
24191
24221
  tearDownConnection() {
24192
24222
  this._connection = undefined;
24193
24223
  this._hasInitialUrlBeenSet = false;
@@ -24198,12 +24228,14 @@ class QdTableSortRouterConnectorService {
24198
24228
  subscribeToUrlChanges() {
24199
24229
  const { tableStoreService } = this._connection;
24200
24230
  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 => {
24231
+ this._readSubscription = this._hub.navigationSettled$
24232
+ .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$()))
24233
+ .subscribe(([segments, currentSort]) => {
24204
24234
  if (segments.length === 1) {
24205
24235
  const [{ column, direction }] = segments;
24206
- tableStoreService.setSort(column, direction);
24236
+ if (!this.isSameAsCurrentSort(currentSort, column, direction)) {
24237
+ tableStoreService.setSort(column, direction);
24238
+ }
24207
24239
  }
24208
24240
  if (isFirstEmit) {
24209
24241
  this._activatedRouteCheckedSubject.next(true);
@@ -24211,19 +24243,25 @@ class QdTableSortRouterConnectorService {
24211
24243
  }
24212
24244
  });
24213
24245
  }
24246
+ isSameAsCurrentSort(currentSort, column, direction) {
24247
+ if (!currentSort)
24248
+ return false;
24249
+ const active = currentSort.filter(entry => entry.direction !== QdSortDirection.NONE);
24250
+ return active.length === 1 && active[0].column === column && active[0].direction === direction;
24251
+ }
24214
24252
  subscribeToStoreChanges() {
24215
24253
  const { tableStoreService } = this._connection;
24216
24254
  this._writeSubscription = tableStoreService
24217
24255
  .tableSort$()
24218
- .pipe(filter((sort) => Array.isArray(sort)), map(sort => this.serializeSort(sort)), distinctUntilChanged(), switchMap(serialized => this.hub.navigationSettled$.pipe(take(1), map(() => serialized))))
24256
+ .pipe(filter((sort) => Array.isArray(sort)), map(sort => this.serializeSort(sort)), distinctUntilChanged(), switchMap(serialized => this._hub.navigationSettled$.pipe(take(1), map(() => serialized))))
24219
24257
  .subscribe(serialized => this.writeUrl(serialized));
24220
24258
  }
24221
24259
  writeUrl(serialized) {
24222
24260
  const replaceUrl = !this._hasInitialUrlBeenSet;
24223
24261
  this._hasInitialUrlBeenSet = true;
24224
- if (this.hub.snapshotQueryParam(SORT_PARAM_NAME) === serialized)
24262
+ if (this._hub.snapshotQueryParam(SORT_PARAM_NAME) === serialized)
24225
24263
  return;
24226
- this.hub.write({ [SORT_PARAM_NAME]: serialized }, replaceUrl);
24264
+ this._hub.write({ [SORT_PARAM_NAME]: serialized }, { replaceUrl });
24227
24265
  }
24228
24266
  shouldConnect(table) {
24229
24267
  const sortConfig = table.config.sort;
@@ -25039,7 +25077,6 @@ class QdTableComponent {
25039
25077
  }
25040
25078
  }
25041
25079
  ngOnDestroy() {
25042
- this.sortRouterConnector.disconnectSortFromRouter(this);
25043
25080
  this.tableStoreService.updateTableStateRecentSecondaryAction(undefined);
25044
25081
  this.tableStoreService.resetConnectorStates();
25045
25082
  this.fillingWidthService.destroy();
@@ -29174,6 +29211,149 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
29174
29211
  args: ['data-test-id']
29175
29212
  }] } });
29176
29213
 
29214
+ const TAB_PARAM_NAME = 'tab';
29215
+ const OWNED_PARAMS = [TAB_PARAM_NAME];
29216
+ const FEATURE_LABEL = { name: 'Page Tabs', plural: 'page tabs' };
29217
+ /**
29218
+ * Per-view adapter that syncs `QdPageTabsComponent` selection with the URL `?tab=` query
29219
+ * param via `QdRouterQueryParamHubService`. Reads on activation and selects the matching
29220
+ * tab (with fallback for unknown/disabled names), writes on `selectionChange`. Coordination,
29221
+ * ownership, and write batching are delegated to the hub — see its JSDoc for details.
29222
+ * Behavior contracts live in `page-tabs-router-connector.service.spec.ts` and
29223
+ * `router-query-param-hub.integration.cy.ts`.
29224
+ *
29225
+ * Note: this is the only connector that writes synchronously without gating on
29226
+ * `navigationSettled$`. Required for the deep-link case where `ngAfterViewInit` fires
29227
+ * before the initial NavigationEnd is replayed — gating there would hang the click-driven
29228
+ * write pipeline (see commit 57f0a271a).
29229
+ *
29230
+ * Internal invariant: when the requested tab is unknown or disabled, `selectFallbackTab()`
29231
+ * writes the URL explicitly. CdkStepper does not emit `selectionChange` if the fallback is
29232
+ * already the selected step, so without the explicit write deep-links like `?tab=phantom`
29233
+ * would leave a stale param in the URL.
29234
+ */
29235
+ class QdPageTabsRouterConnectorService {
29236
+ _hub = inject(QdRouterQueryParamHubService);
29237
+ _destroyRef = inject(DestroyRef);
29238
+ _connectedComponent;
29239
+ _readSubscription;
29240
+ _writeSubscription;
29241
+ _activatedRouteCheckedSubject = new ReplaySubject(1);
29242
+ _activatedRouteChecked$ = this._activatedRouteCheckedSubject.asObservable();
29243
+ _hasInitialUrlBeenSet = false;
29244
+ constructor() {
29245
+ this._destroyRef.onDestroy(() => this.tearDownConnection());
29246
+ }
29247
+ connectTabsWithRouter(component) {
29248
+ if (component.config?.connectWithRouter !== true || !this._hub.isAvailable())
29249
+ return of(false);
29250
+ if (this._connectedComponent === component)
29251
+ return this._activatedRouteChecked$;
29252
+ if (!this._hub.claim(OWNED_PARAMS, this, this._destroyRef, FEATURE_LABEL))
29253
+ return of(false);
29254
+ this._connectedComponent = component;
29255
+ this._hasInitialUrlBeenSet = false;
29256
+ this._activatedRouteCheckedSubject = new ReplaySubject(1);
29257
+ this._activatedRouteChecked$ = this._activatedRouteCheckedSubject.asObservable();
29258
+ this.subscribeToTabChanges();
29259
+ this.subscribeToUrlChanges();
29260
+ return this._activatedRouteChecked$;
29261
+ }
29262
+ tearDownConnection() {
29263
+ this._connectedComponent = undefined;
29264
+ this._hasInitialUrlBeenSet = false;
29265
+ this._readSubscription?.unsubscribe();
29266
+ this._writeSubscription?.unsubscribe();
29267
+ }
29268
+ subscribeToUrlChanges() {
29269
+ let isFirstEmit = true;
29270
+ this._readSubscription = this._hub.navigationSettled$
29271
+ .pipe(switchMap(() => this._hub.queryParams()), map(queryParams => queryParams[TAB_PARAM_NAME]), distinctUntilChanged(), takeUntilDestroyed(this._destroyRef))
29272
+ .subscribe(rawTabName => {
29273
+ this.applyTabFromUrl(rawTabName);
29274
+ if (isFirstEmit) {
29275
+ this._activatedRouteCheckedSubject.next(true);
29276
+ isFirstEmit = false;
29277
+ }
29278
+ });
29279
+ }
29280
+ subscribeToTabChanges() {
29281
+ const component = this._connectedComponent;
29282
+ this._writeSubscription = component.selectionChange
29283
+ .pipe(map(event => event.selectedStep?.config?.name), takeUntilDestroyed(this._destroyRef))
29284
+ .subscribe(name => this.writeUrl(name));
29285
+ }
29286
+ writeUrl(name) {
29287
+ if (!name) {
29288
+ console.warn('Quadrel Framework | QdPageTabs - "connectWithRouter" is active, however the selected <qd-page-tab> has no "name" attribute.');
29289
+ return;
29290
+ }
29291
+ const replaceUrl = !this._hasInitialUrlBeenSet;
29292
+ this._hasInitialUrlBeenSet = true;
29293
+ if (this._hub.snapshotQueryParam(TAB_PARAM_NAME) === name)
29294
+ return;
29295
+ this._hub.write({ [TAB_PARAM_NAME]: name }, { replaceUrl });
29296
+ }
29297
+ applyTabFromUrl(rawTabName) {
29298
+ const component = this._connectedComponent;
29299
+ if (!component)
29300
+ return;
29301
+ if (typeof rawTabName !== 'string' || rawTabName.length === 0) {
29302
+ this.selectFallbackTab();
29303
+ return;
29304
+ }
29305
+ const matchingTab = this.findTabByName(rawTabName);
29306
+ if (matchingTab && !matchingTab.config?.isDisabled) {
29307
+ matchingTab.select();
29308
+ return;
29309
+ }
29310
+ if (matchingTab) {
29311
+ console.warn('Quadrel Framework | QdPageTabs - Tab "' + rawTabName + '" is disabled and cannot be deep-linked.');
29312
+ }
29313
+ else {
29314
+ console.warn('Quadrel Framework | QdPageTabs - No tab found with name "' + rawTabName + '".');
29315
+ }
29316
+ this.selectFallbackTab();
29317
+ }
29318
+ selectFallbackTab() {
29319
+ const component = this._connectedComponent;
29320
+ if (!component)
29321
+ return;
29322
+ const fallbackIndex = this.findFallbackTabIndex();
29323
+ if (fallbackIndex === undefined)
29324
+ return;
29325
+ const fallback = component.tabs.get(fallbackIndex);
29326
+ if (!fallback)
29327
+ return;
29328
+ fallback.select();
29329
+ if (component.config)
29330
+ component.config.selectedIndex = fallbackIndex;
29331
+ this.writeUrl(fallback.config?.name);
29332
+ }
29333
+ findTabByName(name) {
29334
+ return this._connectedComponent?.tabs.find(tab => tab.config?.name === name);
29335
+ }
29336
+ findFallbackTabIndex() {
29337
+ const component = this._connectedComponent;
29338
+ if (!component)
29339
+ return undefined;
29340
+ const configuredIndex = component.config?.selectedIndex;
29341
+ if (typeof configuredIndex === 'number') {
29342
+ const candidate = component.tabs.get(configuredIndex);
29343
+ if (candidate && !candidate.config.isDisabled)
29344
+ return configuredIndex;
29345
+ }
29346
+ const tabsArray = component.tabs.toArray();
29347
+ const firstNonDisabledIndex = tabsArray.findIndex(tab => !tab.config.isDisabled);
29348
+ return firstNonDisabledIndex >= 0 ? firstNonDisabledIndex : undefined;
29349
+ }
29350
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPageTabsRouterConnectorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
29351
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPageTabsRouterConnectorService });
29352
+ }
29353
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPageTabsRouterConnectorService, decorators: [{
29354
+ type: Injectable
29355
+ }], ctorParameters: () => [] });
29356
+
29177
29357
  /**
29178
29358
  * **QdPageTabsComponent** provides a non-linear tabbed navigation system within a **QdPage**.
29179
29359
  * It enables switching between different sections while maintaining form validation and controlled navigation.
@@ -29211,13 +29391,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
29211
29391
  *
29212
29392
  * #### **Submit Button Configuration**
29213
29393
  *
29214
- * The submit button at the bottom of the tab system can be configured via `QdPageTabsConfig`:
29394
+ * The submit button at the bottom of the tab system can be configured via `QdPageTabsConfig.submitButton`:
29215
29395
  *
29216
29396
  * - **i18n**: The translated label for the submit button.
29217
29397
  * - **handler**: A callback function that receives form data upon submission.
29218
29398
  * - **isDisabled**: Controls whether the button should be active or not.
29219
29399
  * - **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.
29400
+ *
29401
+ * #### **Router Integration**
29402
+ *
29403
+ * - **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
29404
  *
29222
29405
  * #### **Usage**
29223
29406
  *
@@ -29336,8 +29519,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
29336
29519
  class QdPageTabsComponent extends CdkStepper {
29337
29520
  footerService = inject(QdPageFooterService, { optional: true });
29338
29521
  pageStoreService = inject(QdPageStoreService);
29339
- router = inject(Router);
29340
- route = inject(ActivatedRoute, { optional: true });
29522
+ routerConnector = inject(QdPageTabsRouterConnectorService);
29341
29523
  /**
29342
29524
  * Configuration of QdPageTabs.
29343
29525
  */
@@ -29396,8 +29578,11 @@ class QdPageTabsComponent extends CdkStepper {
29396
29578
  ngAfterViewInit() {
29397
29579
  super.ngAfterViewInit();
29398
29580
  setTimeout(() => {
29581
+ if (this.config?.connectWithRouter) {
29582
+ this.configureBookmarkableTabs();
29583
+ return;
29584
+ }
29399
29585
  this.initializeTabSelection();
29400
- this.configureBookmarkableTabs();
29401
29586
  });
29402
29587
  }
29403
29588
  initializeTabSelection() {
@@ -29408,54 +29593,7 @@ class QdPageTabsComponent extends CdkStepper {
29408
29593
  this.selectFirstNotDisabledTab();
29409
29594
  }
29410
29595
  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();
29596
+ this.routerConnector.connectTabsWithRouter(this).pipe(takeUntilDestroyed(this.destroyRef)).subscribe();
29459
29597
  }
29460
29598
  isTabSelectable(index) {
29461
29599
  const tab = this.tabs.get(index);
@@ -29463,22 +29601,14 @@ class QdPageTabsComponent extends CdkStepper {
29463
29601
  }
29464
29602
  /**
29465
29603
  * Selects the first tab that is not disabled.
29466
- * @param updateUrl if true, the URL will be updated to reflect the selected tab.
29467
29604
  */
29468
- selectFirstNotDisabledTab(updateUrl = false) {
29605
+ selectFirstNotDisabledTab() {
29469
29606
  this.tabs.some((tab, tabIndex) => {
29470
29607
  if (!tab.config.isDisabled) {
29471
29608
  if (this.config) {
29472
29609
  this.config.selectedIndex = tabIndex;
29473
29610
  }
29474
29611
  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
29612
  return true;
29483
29613
  }
29484
29614
  else {
@@ -29495,13 +29625,6 @@ class QdPageTabsComponent extends CdkStepper {
29495
29625
  if (tab.config?.isDisabled)
29496
29626
  return;
29497
29627
  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
29628
  }
29506
29629
  isSubmitButtonShown() {
29507
29630
  return this.config?.submitButton?.isHidden !== true && !this.footerService;
@@ -29526,11 +29649,11 @@ class QdPageTabsComponent extends CdkStepper {
29526
29649
  });
29527
29650
  }
29528
29651
  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 });
29652
+ 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
29653
  }
29531
29654
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPageTabsComponent, decorators: [{
29532
29655
  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"] }]
29656
+ 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
29657
  }], ctorParameters: () => [], propDecorators: { config: [{
29535
29658
  type: Input
29536
29659
  }], testId: [{
@@ -31925,7 +32048,7 @@ class QdShellHeaderComponent {
31925
32048
  const navigationService = this.navigationService;
31926
32049
  const shellLeftService = this.shellLeftService;
31927
32050
  const shellRightService = this.shellRightService;
31928
- this.backLinkDisplayed$ = navigationService.getPreviousHref$().pipe(withLatestFrom(navigationService.getRouteComponentInstance$()), switchMap(([previousHref, routeComponentInstance]) => {
32051
+ this.backLinkDisplayed$ = navigationService.getPreviousHref$().pipe(withLatestFrom$1(navigationService.getRouteComponentInstance$()), switchMap(([previousHref, routeComponentInstance]) => {
31929
32052
  if (typeof previousHref === 'function')
31930
32053
  previousHref = previousHref(routeComponentInstance);
31931
32054
  return isObservable(previousHref) ? previousHref.pipe(take(1)) : of(previousHref);