@quadrel-enterprise-ui/framework 20.6.1 → 20.7.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,7 +3,7 @@ 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, combineLatestWith, forkJoin, delayWhen, withLatestFrom, async, filter as filter$1 } from 'rxjs';
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, combineLatestWith, forkJoin, delayWhen, withLatestFrom, async } from 'rxjs';
7
7
  import { map, takeUntil, take, filter, catchError, debounceTime, startWith, distinctUntilChanged, concatMap, tap, skip, 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';
@@ -1161,6 +1161,7 @@ class QdConfirmationDialogOpenerService {
1161
1161
  open(component, config) {
1162
1162
  config.panelClass = 'qd-custom-panel';
1163
1163
  config.width = config.dialogSize || QdDialogSize.Default;
1164
+ config.disableClose = true;
1164
1165
  return this.dialog.open(component, config);
1165
1166
  }
1166
1167
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdConfirmationDialogOpenerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
@@ -1222,8 +1223,8 @@ class QdDialogService {
1222
1223
  open(component, config) {
1223
1224
  config.panelClass = 'qd-custom-panel';
1224
1225
  config.width = config.dialogSize || QdDialogSize.Default;
1226
+ config.disableClose = true;
1225
1227
  if (config.dialogSize === QdDialogSize.FullWidth) {
1226
- config.disableClose = true;
1227
1228
  config.maxWidth = '100vw';
1228
1229
  config.width = '100vw';
1229
1230
  config.maxHeight = '100vh';
@@ -1435,8 +1436,7 @@ class QdDialogComponent {
1435
1436
  this.dialogService.pageDialogCanClose$.pipe(takeUntil(this._destroyed$)).subscribe(entry => {
1436
1437
  this._pageDialogCanCloseFn = entry?.owner === this.dialogRef ? entry.fn : null;
1437
1438
  });
1438
- if (this.isFullWidth)
1439
- this.bindEscToClose();
1439
+ this.bindEscToClose();
1440
1440
  }
1441
1441
  ngAfterContentChecked() {
1442
1442
  const children = Array.from(this.body?.nativeElement?.children ?? []);
@@ -6855,185 +6855,230 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
6855
6855
  }] });
6856
6856
 
6857
6857
  /**
6858
- * Service for handling real-time server-sent events.
6858
+ * Manages a server-sent event (SSE) connection with automatic reconnection and heartbeat monitoring.
6859
6859
  *
6860
- * ### Benefits
6860
+ * All service endpoints — including SSE — must be protected. The connection is therefore secured
6861
+ * via Quadrel Auth (Bearer token) by default. When the access token refreshes, the connection is
6862
+ * transparently rebuilt. Only disable authentication for endpoints that are explicitly public.
6861
6863
  *
6862
- * - **Real-Time Interaction**: Enables live updates without user actions or polling.
6863
- * - **Scalable and Flexible**: The system is flexible and easily expandable to handle new events.
6864
- * - **Consistency**: Ensures synchronization between server and client states for a consistent user experience.
6865
- * - **Authorization**: It uses the user's authentication (QdAuth) to secure the connection against the backend out-of-the-box. This requires a proper QdAuth setup. You can disable this feature when starting the connection.
6864
+ * ### Reconnection behavior
6865
+ *
6866
+ * - **Heartbeat timeout**: If no HEARTBEAT event arrives within the expected interval, the service reconnects.
6867
+ * - **401 Unauthorized**: Triggers exponential-backoff reconnection (max 5 attempts, capped at 30 s).
6868
+ * - **Other errors**: Reconnects immediately when the EventSource transitions to CLOSED.
6869
+ *
6870
+ * Existing `observe()` subscriptions are preserved across reconnections.
6866
6871
  *
6867
6872
  * ### Usage
6868
6873
  *
6869
6874
  * ```ts
6870
- * // Start the connection to the events
6871
- * pushEventsService.connect('http://service-endpint/events');
6872
- * // or start without authentication in case you don't need:
6873
- * pushEventsService.connect('http://service-endpint/events', { disableAuthentication: true });
6875
+ * pushEventsService.connect('https://api.example.com/events');
6874
6876
  *
6875
- * // Subscribe to the event (Side Effect)
6876
- * const subscription = pushEventsService.observe('RESOURCE_UPDATED').subscribe(event => handleEvent(event));
6877
+ * const sub = pushEventsService.observe('RESOURCE_UPDATED').subscribe(event => handleEvent(event));
6877
6878
  *
6878
- * // Unsubscribe to prevent memory leaks
6879
- * subscription.unsubscribe();
6880
- *
6881
- * // Disconnect the connection
6879
+ * sub.unsubscribe();
6882
6880
  * pushEventsService.disconnect();
6883
6881
  * ```
6884
6882
  */
6885
6883
  class QdPushEventsService {
6886
6884
  authenticationService = inject('QdAuthenticationService', { optional: true });
6887
6885
  _eventSource;
6888
- _eventSubscriptionSubjects = new Map();
6886
+ _subjects = new Map();
6889
6887
  _listeners = [];
6890
- _heartbeatTimeout;
6891
- _reconnectDelayTime = 10000;
6892
- _heartbeatReconnectDelayTime = 100;
6893
6888
  _accessTokenSub;
6894
6889
  _options;
6890
+ _heartbeatTimeout;
6891
+ _heartbeatGracePeriod = 100;
6892
+ _initialHeartbeatTimeout = 10000;
6895
6893
  _isUnauthorized = false;
6894
+ _reconnectAttempts = 0;
6895
+ _maxReconnectAttempts = 5;
6896
+ _backoffTimer;
6896
6897
  /**
6897
- * Establishes an EventSource connection to the given URL.
6898
- * Automatically reconnects if heartbeat fails or is delayed.
6899
- * Subscribers are retained across reconnections.
6898
+ * Opens an SSE connection to the given URL.
6899
+ *
6900
+ * With authentication enabled (default), the service subscribes to the `accessToken$` observable
6901
+ * provided by Quadrel Auth and rebuilds the connection whenever the token changes.
6902
+ * No-op if already connected.
6903
+ *
6904
+ * @param url The SSE endpoint URL.
6905
+ * @param options Set `{ disableAuthentication: true }` only for explicitly public endpoints.
6906
+ *
6907
+ * @example
6908
+ * ```ts
6909
+ * // Authenticated (default — requires Quadrel Auth)
6910
+ * pushEventsService.connect('https://api.example.com/events');
6900
6911
  *
6901
- * @param url The backend URL for the event stream.
6902
- * @param options Options for the connection
6912
+ * // Without authentication (only for public endpoints)
6913
+ * pushEventsService.connect('https://api.example.com/public-events', { disableAuthentication: true });
6914
+ * ```
6903
6915
  */
6904
6916
  connect(url, options) {
6905
6917
  if (this.isConnectedOrConnecting())
6906
6918
  return;
6907
- if (!options?.disableAuthentication) {
6908
- if (!this.authenticationService) {
6909
- this.logError("Can't connect to SSE without QdAuth as the connection has to be secured. Please install QdAuth or disable authentication when connecting.");
6910
- return;
6911
- }
6912
- if (this._accessTokenSub)
6913
- this._accessTokenSub.unsubscribe();
6914
- this._accessTokenSub = this.authenticationService.accessToken$.subscribe((token) => {
6915
- this._options = { url, options };
6916
- this.connectEventSource(url, {
6917
- headers: { Authorization: `Bearer ${token}` }
6918
- }, true);
6919
- });
6919
+ this._options = { url, options };
6920
+ if (options?.disableAuthentication) {
6921
+ this.openEventSource(url);
6920
6922
  }
6921
6923
  else {
6922
- this._options = { url, options };
6923
- this.connectEventSource(url);
6924
+ this.connectWithAuth(url);
6924
6925
  }
6925
6926
  }
6926
6927
  /**
6927
- * Closes the EventSource connection and clears all listeners.
6928
- * Subscribers are preserved for reconnection.
6928
+ * Closes the EventSource connection, clears all event listeners, and cancels pending backoff timers.
6929
+ * Subscription subjects are preserved so that a subsequent `connect()` re-attaches them.
6930
+ *
6931
+ * @example
6932
+ * ```ts
6933
+ * pushEventsService.disconnect();
6934
+ * // Subjects survive — a new connect() will re-attach existing observe() subscriptions.
6935
+ * ```
6929
6936
  */
6930
6937
  disconnect() {
6931
6938
  if (!this._eventSource) {
6932
6939
  this.logWarn('No active connection to disconnect.');
6933
6940
  return;
6934
6941
  }
6935
- this.removeAllEventListenersFromEventSource();
6942
+ if (this._accessTokenSub) {
6943
+ this._accessTokenSub.unsubscribe();
6944
+ this._accessTokenSub = undefined;
6945
+ }
6946
+ if (this._heartbeatTimeout)
6947
+ clearTimeout(this._heartbeatTimeout);
6948
+ if (this._backoffTimer) {
6949
+ clearTimeout(this._backoffTimer);
6950
+ this._backoffTimer = undefined;
6951
+ }
6952
+ this.removeAllListeners();
6936
6953
  this._eventSource.close();
6937
6954
  }
6938
6955
  /**
6939
- * Returns an Observable for the specified event name.
6940
- * If not connected, returns `NEVER` and logs an error.
6941
- * Automatically adds a listener for the event if needed.
6956
+ * Returns an Observable that emits whenever the server sends an event of the given type.
6942
6957
  *
6943
- * @param eventName The event type ('RESOURCE_CREATED', 'RESOURCE_UPDATED', 'RESOURCE_DELETED').
6944
- * @returns Observable<MessageEvent> The event stream.
6958
+ * Lazily registers an EventSource listener on first call per event name.
6959
+ * Returns `NEVER` and logs an error if no connection exists.
6960
+ *
6961
+ * @param eventName The SSE event type to listen for.
6962
+ *
6963
+ * @example
6964
+ * ```ts
6965
+ * pushEventsService.observe('RESOURCE_UPDATED').subscribe(event => handleUpdate(event));
6966
+ * ```
6945
6967
  */
6946
6968
  observe(eventName) {
6947
6969
  if (!this._eventSource) {
6948
- this.logError('Cannot observe events without a connection. Call connect() first.');
6970
+ this.logWarn('Cannot observe events without a connection. Call connect() first.');
6949
6971
  return NEVER;
6950
6972
  }
6951
- if (!this._eventSubscriptionSubjects.has(eventName)) {
6952
- this._eventSubscriptionSubjects.set(eventName, new Subject());
6953
- this.addEventListenerForEventName(eventName);
6973
+ if (!this._subjects.has(eventName)) {
6974
+ this._subjects.set(eventName, new Subject());
6975
+ this.addListener(eventName, (event) => this._subjects.get(eventName).next(event));
6954
6976
  }
6955
- return this._eventSubscriptionSubjects.get(eventName).asObservable();
6977
+ return this._subjects.get(eventName).asObservable();
6956
6978
  }
6957
6979
  /**
6958
- * Removes all listeners and clears all subscriptions.
6959
- * The EventSource connection remains open.
6980
+ * Removes all event listeners and clears all subscription subjects. The connection stays open.
6981
+ * Use this to reset all subscriptions without disconnecting the SSE stream.
6982
+ *
6983
+ * @example
6984
+ * ```ts
6985
+ * pushEventsService.unobserveAll();
6986
+ * // Connection remains open, but no events are forwarded until observe() is called again.
6987
+ * ```
6960
6988
  */
6961
6989
  unobserveAll() {
6962
- this.removeAllEventListenersFromEventSource();
6963
- this._eventSubscriptionSubjects.clear();
6990
+ this.removeAllListeners();
6991
+ this._subjects.clear();
6964
6992
  }
6965
- /**
6966
- * Checks if the EventSource is connected or in the process of connecting.
6967
- */
6993
+ /** Returns `true` when the EventSource is in state OPEN or CONNECTING. */
6968
6994
  isConnectedOrConnecting() {
6969
6995
  return (this._eventSource &&
6970
6996
  (this._eventSource.readyState === EventSource.OPEN || this._eventSource.readyState === EventSource.CONNECTING));
6971
6997
  }
6972
- reconnect() {
6973
- if (this.isConnectedOrConnecting())
6998
+ connectWithAuth(url) {
6999
+ if (!this.authenticationService) {
7000
+ this.logError("Can't connect to SSE without Quadrel Auth as the connection has to be secured. Please install Quadrel Auth or disable authentication when connecting.");
6974
7001
  return;
6975
- if (this._isUnauthorized)
7002
+ }
7003
+ this._accessTokenSub = this.authenticationService.accessToken$.subscribe((token) => {
7004
+ this.openEventSource(url, { headers: { Authorization: `Bearer ${token}` } });
7005
+ });
7006
+ }
7007
+ openEventSource(url, options = {}) {
7008
+ this._isUnauthorized = false;
7009
+ if (this._eventSource && this._eventSource.readyState !== EventSource.CLOSED)
7010
+ this._eventSource.close();
7011
+ this._eventSource = new EventSourcePolyfill(url, options);
7012
+ this._eventSource.onerror = (err) => this.handleError(err);
7013
+ this._eventSource.addEventListener('HEARTBEAT', (message) => this.handleHeartbeat(message));
7014
+ if (this._heartbeatTimeout)
7015
+ clearTimeout(this._heartbeatTimeout);
7016
+ this._heartbeatTimeout = setTimeout(() => this.reconnect(), this._initialHeartbeatTimeout);
7017
+ if (this._backoffTimer) {
7018
+ clearTimeout(this._backoffTimer);
7019
+ this._backoffTimer = undefined;
7020
+ }
7021
+ this._listeners = [];
7022
+ this.reattachListeners();
7023
+ }
7024
+ reconnect() {
7025
+ if (this.isConnectedOrConnecting() || this._isUnauthorized)
6976
7026
  return;
6977
7027
  this.disconnect();
6978
7028
  this.connect(this._options.url, this._options.options);
6979
7029
  }
6980
- addEventListenerForEventName(eventName) {
6981
- const callback = (messageEvent) => {
6982
- const subject = this._eventSubscriptionSubjects.get(eventName);
6983
- if (subject)
6984
- subject.next(messageEvent);
6985
- };
6986
- this._eventSource.addEventListener(eventName, callback);
6987
- this._listeners.push([eventName, callback]);
7030
+ handleError(err) {
7031
+ const status = err?.status || err?.target?.status;
7032
+ if (status === 401) {
7033
+ this._isUnauthorized = true;
7034
+ this.logError('SSE connection unauthorized (401):', err);
7035
+ this._eventSource.close();
7036
+ this.scheduleRetry();
7037
+ return;
7038
+ }
7039
+ if (this._eventSource.readyState === EventSource.CLOSED)
7040
+ this.reconnect();
7041
+ this.logError('SSE connection error:', err);
7042
+ }
7043
+ scheduleRetry() {
7044
+ if (this._reconnectAttempts >= this._maxReconnectAttempts) {
7045
+ this.logError(`SSE reconnect aborted after ${this._maxReconnectAttempts} failed attempts.`);
7046
+ return;
7047
+ }
7048
+ const delay = Math.min(1000 * Math.pow(2, this._reconnectAttempts), 30000);
7049
+ this._reconnectAttempts++;
7050
+ this._backoffTimer = setTimeout(() => {
7051
+ this._isUnauthorized = false;
7052
+ this.reconnect();
7053
+ }, delay);
6988
7054
  }
6989
- addEventListenersForExistingSubscriptions() {
6990
- this._eventSubscriptionSubjects.forEach((subject, eventName) => this.addEventListenerToEventSource(eventName, (messageEvent) => subject.next(messageEvent)));
7055
+ handleHeartbeat(message) {
7056
+ if (this._isUnauthorized)
7057
+ return;
7058
+ if (this._heartbeatTimeout)
7059
+ clearTimeout(this._heartbeatTimeout);
7060
+ this._reconnectAttempts = 0;
7061
+ const interval = JSON.parse(message.data).interval;
7062
+ this._heartbeatTimeout = setTimeout(() => this.reconnect(), interval + this._heartbeatGracePeriod);
6991
7063
  }
6992
- addEventListenerToEventSource(eventName, callback) {
7064
+ addListener(eventName, callback) {
6993
7065
  this._eventSource.addEventListener(eventName, callback);
6994
7066
  this._listeners.push([eventName, callback]);
6995
7067
  }
6996
- removeAllEventListenersFromEventSource() {
6997
- if (!this._eventSource) {
6998
- this.logWarn('Cannot remove listeners: No active connection.');
7068
+ reattachListeners() {
7069
+ this._subjects.forEach((subject, eventName) => this.addListener(eventName, (event) => subject.next(event)));
7070
+ }
7071
+ removeAllListeners() {
7072
+ if (!this._eventSource)
6999
7073
  return;
7000
- }
7001
7074
  this._listeners.forEach(([eventName, callback]) => this._eventSource.removeEventListener(eventName, callback));
7002
7075
  this._listeners = [];
7003
7076
  }
7004
7077
  logWarn(message) {
7005
- console.warn(`QD-UI | QdPushEventsService - ${message}`);
7078
+ console.warn(`QD-UI | QdPushEvents - ${message}`);
7006
7079
  }
7007
7080
  logError(message, err) {
7008
- console.error(`QD-UI | QdPushEventsService - ${message}`, err);
7009
- }
7010
- connectEventSource(url, options = {}, reconnect) {
7011
- if (this._eventSource && this._eventSource.readyState !== EventSource.CLOSED)
7012
- this._eventSource.close();
7013
- if (!this._eventSource || this._eventSource.readyState === EventSource.CLOSED || reconnect)
7014
- this._eventSource = new EventSourcePolyfill(url, options);
7015
- this._eventSource.onerror = (err) => {
7016
- const status = err?.status || err?.target?.status;
7017
- if (status === 401) {
7018
- this._isUnauthorized = true;
7019
- this.logError('SSE connection unauthorized (401):', err);
7020
- this._eventSource.close();
7021
- return;
7022
- }
7023
- if (this._eventSource.readyState === EventSource.CLOSED)
7024
- this.reconnect();
7025
- this.logError('SSE connection error:', err);
7026
- };
7027
- this._eventSource.addEventListener('HEARTBEAT', (message) => {
7028
- if (this._isUnauthorized)
7029
- return;
7030
- if (this._heartbeatTimeout)
7031
- clearTimeout(this._heartbeatTimeout);
7032
- const interval = JSON.parse(message.data).interval;
7033
- this._heartbeatTimeout = setTimeout(() => this.reconnect(), interval + this._heartbeatReconnectDelayTime);
7034
- });
7035
- this._heartbeatTimeout = setTimeout(() => this.reconnect(), this._reconnectDelayTime);
7036
- this.addEventListenersForExistingSubscriptions();
7081
+ console.error(`QD-UI | QdPushEvents - ${message}`, err);
7037
7082
  }
7038
7083
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPushEventsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
7039
7084
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPushEventsService, providedIn: 'root' });
@@ -20736,11 +20781,11 @@ class QdFilterItemSelectCategoryComponent {
20736
20781
  }));
20737
20782
  }
20738
20783
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdFilterItemSelectCategoryComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
20739
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: QdFilterItemSelectCategoryComponent, isStandalone: false, selector: "qd-filter-category-select", inputs: { filterId: "filterId", categoryIndex: "categoryIndex", testId: ["data-test-id", "testId"] }, host: { classAttribute: "qd-filter__category-select" }, viewQueries: [{ propertyName: "popoverDirective", first: true, predicate: QdPopoverOnClickDirective, descendants: true }], ngImport: i0, template: "<div\n *ngIf=\"showSelectButton$ | async\"\n [class]=\"buttonClassName\"\n [qdPopoverOnClick]=\"filterLayer\"\n [qdPopoverCloseStrategy]=\"'onOutsideClick'\"\n [qdPopoverStopPropagation]=\"true\"\n [qdPopoverMaxHeight]=\"maxFlyoutHeight\"\n [positionStrategy]=\"positionStrategy\"\n (opened)=\"onLayerOpened()\"\n (closed)=\"onLayerClosed()\"\n [attr.data-test-id]=\"testId + '-select-button'\"\n>\n <qd-icon\n *ngIf=\"!open\"\n [icon]=\"'ctrlDown'\"\n [class]=\"'qd-filter__category-icon qd-filter__category-icon--closed'\"\n [attr.data-test-id]=\"testId + '-closed'\"\n ></qd-icon>\n <qd-icon\n *ngIf=\"open\"\n [icon]=\"'ctrlTop'\"\n [class]=\"'qd-filter__category-icon qd-filter__category-icon--open'\"\n [attr.data-test-id]=\"testId + '-opened'\"\n ></qd-icon>\n\n <div class=\"qd-filter__category-button-caption\">\n <span>\n {{ i18n | translate }}\n </span>\n </div>\n\n <ng-container *ngTemplateOutlet=\"SelectedChip\"></ng-container>\n</div>\n\n<ng-template #filterLayer>\n <div [class]=\"layerContentClassName\">\n <qd-icon\n *ngIf=\"closeButton\"\n [class]=\"'qd-filter__category-layer-close'\"\n (click)=\"closeLayer()\"\n [icon]=\"'timesLargeLight'\"\n [attr.data-test-id]=\"testId + '-layer-close'\"\n ></qd-icon>\n\n <ng-container [ngSwitch]=\"type\">\n <ng-container *ngSwitchCase=\"'multiSelect'\">\n <qd-filter-form-items\n *ngIf=\"filter\"\n [inputFilterValue]=\"filterCategoryValue$ | async\"\n (filterValueChange)=\"changeValue($event)\"\n [data-test-id]=\"testId\"\n ></qd-filter-form-items>\n\n <div class=\"qd-filter__category-layer-items\">\n <ng-container *ngFor=\"let item of items; let itemIndex = index\">\n <qd-filter-item-multi-select\n *ngIf=\"!item.hidden\"\n [categoryIndex]=\"categoryIndex\"\n [itemIndex]=\"itemIndex\"\n [filterId]=\"filterId\"\n [data-test-id]=\"testId + '-item-' + itemIndex + '-' + item.item\"\n >\n </qd-filter-item-multi-select>\n </ng-container>\n </div>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'singleSelect'\">\n <qd-filter-form-items\n *ngIf=\"filter\"\n [inputFilterValue]=\"filterCategoryValue$ | async\"\n (filterValueChange)=\"changeValue($event)\"\n [data-test-id]=\"testId\"\n ></qd-filter-form-items>\n\n <div class=\"qd-filter__category-layer-items\">\n <ng-container *ngFor=\"let item of items; let itemIndex = index\">\n <qd-filter-item-single-select\n *ngIf=\"!item.hidden\"\n [categoryIndex]=\"categoryIndex\"\n [itemIndex]=\"itemIndex\"\n (closeEventEmitter)=\"closeLayer()\"\n [filterId]=\"filterId\"\n [data-test-id]=\"testId + '-item-' + itemIndex + '-' + item.item\"\n >\n </qd-filter-item-single-select>\n </ng-container>\n </div>\n </ng-container>\n </ng-container>\n </div>\n</ng-template>\n\n<ng-template #SelectedChip>\n <ng-container *ngFor=\"let item of items; let itemIndex = index\">\n <!-- TODO: Add tooltip-->\n <qd-chip\n *ngIf=\"item.active\"\n [state]=\"'filter'\"\n [close]=\"true\"\n (closeClickEmitter)=\"close($event)\"\n [data]=\"itemIndex\"\n [data-test-id]=\"testId + '-selected-chip-' + item.item\"\n >{{ item.i18n | translate }}</qd-chip\n >\n </ng-container>\n</ng-template>\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: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "component", type: QdChipComponent, selector: "qd-chip", inputs: ["state", "close", "data", "data-test-id"], outputs: ["closeClickEmitter"] }, { kind: "component", type: QdFilterFormItemsComponent, selector: "qd-filter-form-items", inputs: ["inputFilterValue", "data-test-id"], outputs: ["filterValueChange"] }, { kind: "component", type: QdIconComponent, selector: "qd-icon", inputs: ["icon"] }, { kind: "directive", type: QdPopoverOnClickDirective, selector: "[qdPopoverOnClick]", inputs: ["qdPopoverOnClick", "positionStrategy", "qdPopoverCloseStrategy", "qdPopoverDisabled", "qdPopoverStopPropagation", "qdPopoverBackgroundColor", "qdPopoverMaxHeight", "qdPopoverMinWidth", "qdPopoverMaxWidth", "qdPopoverAutoSize", "qdPopoverEnableKeyControl"], outputs: ["opened", "closed"], exportAs: ["qdPopoverOnClick"] }, { kind: "component", type: QdFilterItemMultiSelectComponent, selector: "qd-filter-item-multi-select", inputs: ["filterId", "categoryIndex", "itemIndex", "data-test-id"] }, { kind: "component", type: QdFilterItemSingleSelectComponent, selector: "qd-filter-item-single-select", inputs: ["filterId", "categoryIndex", "itemIndex", "data-test-id"], outputs: ["closeEventEmitter"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }, { kind: "pipe", type: i3.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
20784
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: QdFilterItemSelectCategoryComponent, isStandalone: false, selector: "qd-filter-category-select", inputs: { filterId: "filterId", categoryIndex: "categoryIndex", testId: ["data-test-id", "testId"] }, host: { classAttribute: "qd-filter__category-select" }, viewQueries: [{ propertyName: "popoverDirective", first: true, predicate: QdPopoverOnClickDirective, descendants: true }], ngImport: i0, template: "<div\n *ngIf=\"showSelectButton$ | async\"\n [class]=\"buttonClassName\"\n [qdPopoverOnClick]=\"filterLayer\"\n [qdPopoverCloseStrategy]=\"'onOutsideClick'\"\n [qdPopoverStopPropagation]=\"true\"\n [qdPopoverMaxHeight]=\"maxFlyoutHeight\"\n [positionStrategy]=\"positionStrategy\"\n (opened)=\"onLayerOpened()\"\n (closed)=\"onLayerClosed()\"\n [attr.data-test-id]=\"testId + '-select-button'\"\n>\n <qd-icon\n *ngIf=\"!open\"\n [icon]=\"'ctrlDown'\"\n [class]=\"'qd-filter__category-icon qd-filter__category-icon--closed'\"\n [attr.data-test-id]=\"testId + '-closed'\"\n ></qd-icon>\n <qd-icon\n *ngIf=\"open\"\n [icon]=\"'ctrlTop'\"\n [class]=\"'qd-filter__category-icon qd-filter__category-icon--open'\"\n [attr.data-test-id]=\"testId + '-opened'\"\n ></qd-icon>\n\n <div class=\"qd-filter__category-button-caption\">\n <span>\n {{ i18n | translate }}\n </span>\n </div>\n\n <ng-container *ngTemplateOutlet=\"SelectedChip\"></ng-container>\n</div>\n\n<ng-template #filterLayer>\n <div [class]=\"layerContentClassName\">\n <qd-icon\n *ngIf=\"closeButton\"\n [class]=\"'qd-filter__category-layer-close'\"\n (click)=\"closeLayer()\"\n [icon]=\"'timesLargeLight'\"\n [attr.data-test-id]=\"testId + '-layer-close'\"\n ></qd-icon>\n\n <ng-container [ngSwitch]=\"type\">\n <ng-container *ngSwitchCase=\"'multiSelect'\">\n <qd-filter-form-items\n *ngIf=\"filter\"\n [inputFilterValue]=\"filterCategoryValue$ | async\"\n (filterValueChange)=\"changeValue($event)\"\n [data-test-id]=\"testId\"\n ></qd-filter-form-items>\n\n <div class=\"qd-filter__category-layer-items\">\n <ng-container *ngFor=\"let item of items; let itemIndex = index\">\n <qd-filter-item-multi-select\n *ngIf=\"!item.hidden\"\n [categoryIndex]=\"categoryIndex\"\n [itemIndex]=\"itemIndex\"\n [filterId]=\"filterId\"\n [data-test-id]=\"testId + '-item-' + itemIndex + '-' + item.item\"\n >\n </qd-filter-item-multi-select>\n </ng-container>\n </div>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'singleSelect'\">\n <qd-filter-form-items\n *ngIf=\"filter\"\n [inputFilterValue]=\"filterCategoryValue$ | async\"\n (filterValueChange)=\"changeValue($event)\"\n [data-test-id]=\"testId\"\n ></qd-filter-form-items>\n\n <div class=\"qd-filter__category-layer-items\">\n <ng-container *ngFor=\"let item of items; let itemIndex = index\">\n <qd-filter-item-single-select\n *ngIf=\"!item.hidden\"\n [categoryIndex]=\"categoryIndex\"\n [itemIndex]=\"itemIndex\"\n (closeEventEmitter)=\"closeLayer()\"\n [filterId]=\"filterId\"\n [data-test-id]=\"testId + '-item-' + itemIndex + '-' + item.item\"\n >\n </qd-filter-item-single-select>\n </ng-container>\n </div>\n </ng-container>\n </ng-container>\n </div>\n</ng-template>\n\n<ng-template #SelectedChip>\n <ng-container *ngFor=\"let item of items; let itemIndex = index\">\n <!-- TODO: Add tooltip-->\n <qd-chip\n *ngIf=\"item.active\"\n [state]=\"'filter'\"\n [close]=\"true\"\n (closeClickEmitter)=\"close($event)\"\n [data]=\"itemIndex\"\n [data-test-id]=\"testId + '-selected-chip-' + item.item\"\n >{{ item.i18n | translate }}</qd-chip\n >\n </ng-container>\n</ng-template>\n", styles: ["::ng-deep .qd-filter__category-layer{display:flex;overflow:hidden;min-width:16.25rem;max-width:56.25rem;height:100%;flex-direction:column;padding:1rem 0 0;border:.0625rem solid rgb(151,151,151);background:#fff;box-shadow:#d5d5d5 .125rem .25rem .4375rem}::ng-deep .qd-filter__category-layer .qd-filter-form-items__filter{flex-shrink:0;margin-bottom:0}::ng-deep .qd-filter__category-layer .qd-checkbox__label{display:flex;height:2.5rem}::ng-deep .qd-filter__category-layer .qd-checkbox__indicator,::ng-deep .qd-filter__category-layer .qd-radio-buttons__indicator{transform:translateY(-.0625rem)}::ng-deep .qd-filter__category-layer-items{flex:1;overflow-y:auto}::ng-deep .qd-filter__category-layer-container{position:relative}::ng-deep .qd-filter__category-layer-close{position:absolute;z-index:1;top:1rem;right:1rem;color:#757575;cursor:pointer}::ng-deep .qd-filter__category-layer-close:before{position:absolute;content:\"\";inset:-.5rem}::ng-deep .qd-filter__category-layer-close:hover,::ng-deep .qd-filter__category-layer-close:focus{color:#171717}\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: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "component", type: QdChipComponent, selector: "qd-chip", inputs: ["state", "close", "data", "data-test-id"], outputs: ["closeClickEmitter"] }, { kind: "component", type: QdFilterFormItemsComponent, selector: "qd-filter-form-items", inputs: ["inputFilterValue", "data-test-id"], outputs: ["filterValueChange"] }, { kind: "component", type: QdIconComponent, selector: "qd-icon", inputs: ["icon"] }, { kind: "directive", type: QdPopoverOnClickDirective, selector: "[qdPopoverOnClick]", inputs: ["qdPopoverOnClick", "positionStrategy", "qdPopoverCloseStrategy", "qdPopoverDisabled", "qdPopoverStopPropagation", "qdPopoverBackgroundColor", "qdPopoverMaxHeight", "qdPopoverMinWidth", "qdPopoverMaxWidth", "qdPopoverAutoSize", "qdPopoverEnableKeyControl"], outputs: ["opened", "closed"], exportAs: ["qdPopoverOnClick"] }, { kind: "component", type: QdFilterItemMultiSelectComponent, selector: "qd-filter-item-multi-select", inputs: ["filterId", "categoryIndex", "itemIndex", "data-test-id"] }, { kind: "component", type: QdFilterItemSingleSelectComponent, selector: "qd-filter-item-single-select", inputs: ["filterId", "categoryIndex", "itemIndex", "data-test-id"], outputs: ["closeEventEmitter"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }, { kind: "pipe", type: i3.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
20740
20785
  }
20741
20786
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdFilterItemSelectCategoryComponent, decorators: [{
20742
20787
  type: Component,
20743
- args: [{ selector: 'qd-filter-category-select', host: { class: 'qd-filter__category-select' }, changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<div\n *ngIf=\"showSelectButton$ | async\"\n [class]=\"buttonClassName\"\n [qdPopoverOnClick]=\"filterLayer\"\n [qdPopoverCloseStrategy]=\"'onOutsideClick'\"\n [qdPopoverStopPropagation]=\"true\"\n [qdPopoverMaxHeight]=\"maxFlyoutHeight\"\n [positionStrategy]=\"positionStrategy\"\n (opened)=\"onLayerOpened()\"\n (closed)=\"onLayerClosed()\"\n [attr.data-test-id]=\"testId + '-select-button'\"\n>\n <qd-icon\n *ngIf=\"!open\"\n [icon]=\"'ctrlDown'\"\n [class]=\"'qd-filter__category-icon qd-filter__category-icon--closed'\"\n [attr.data-test-id]=\"testId + '-closed'\"\n ></qd-icon>\n <qd-icon\n *ngIf=\"open\"\n [icon]=\"'ctrlTop'\"\n [class]=\"'qd-filter__category-icon qd-filter__category-icon--open'\"\n [attr.data-test-id]=\"testId + '-opened'\"\n ></qd-icon>\n\n <div class=\"qd-filter__category-button-caption\">\n <span>\n {{ i18n | translate }}\n </span>\n </div>\n\n <ng-container *ngTemplateOutlet=\"SelectedChip\"></ng-container>\n</div>\n\n<ng-template #filterLayer>\n <div [class]=\"layerContentClassName\">\n <qd-icon\n *ngIf=\"closeButton\"\n [class]=\"'qd-filter__category-layer-close'\"\n (click)=\"closeLayer()\"\n [icon]=\"'timesLargeLight'\"\n [attr.data-test-id]=\"testId + '-layer-close'\"\n ></qd-icon>\n\n <ng-container [ngSwitch]=\"type\">\n <ng-container *ngSwitchCase=\"'multiSelect'\">\n <qd-filter-form-items\n *ngIf=\"filter\"\n [inputFilterValue]=\"filterCategoryValue$ | async\"\n (filterValueChange)=\"changeValue($event)\"\n [data-test-id]=\"testId\"\n ></qd-filter-form-items>\n\n <div class=\"qd-filter__category-layer-items\">\n <ng-container *ngFor=\"let item of items; let itemIndex = index\">\n <qd-filter-item-multi-select\n *ngIf=\"!item.hidden\"\n [categoryIndex]=\"categoryIndex\"\n [itemIndex]=\"itemIndex\"\n [filterId]=\"filterId\"\n [data-test-id]=\"testId + '-item-' + itemIndex + '-' + item.item\"\n >\n </qd-filter-item-multi-select>\n </ng-container>\n </div>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'singleSelect'\">\n <qd-filter-form-items\n *ngIf=\"filter\"\n [inputFilterValue]=\"filterCategoryValue$ | async\"\n (filterValueChange)=\"changeValue($event)\"\n [data-test-id]=\"testId\"\n ></qd-filter-form-items>\n\n <div class=\"qd-filter__category-layer-items\">\n <ng-container *ngFor=\"let item of items; let itemIndex = index\">\n <qd-filter-item-single-select\n *ngIf=\"!item.hidden\"\n [categoryIndex]=\"categoryIndex\"\n [itemIndex]=\"itemIndex\"\n (closeEventEmitter)=\"closeLayer()\"\n [filterId]=\"filterId\"\n [data-test-id]=\"testId + '-item-' + itemIndex + '-' + item.item\"\n >\n </qd-filter-item-single-select>\n </ng-container>\n </div>\n </ng-container>\n </ng-container>\n </div>\n</ng-template>\n\n<ng-template #SelectedChip>\n <ng-container *ngFor=\"let item of items; let itemIndex = index\">\n <!-- TODO: Add tooltip-->\n <qd-chip\n *ngIf=\"item.active\"\n [state]=\"'filter'\"\n [close]=\"true\"\n (closeClickEmitter)=\"close($event)\"\n [data]=\"itemIndex\"\n [data-test-id]=\"testId + '-selected-chip-' + item.item\"\n >{{ item.i18n | translate }}</qd-chip\n >\n </ng-container>\n</ng-template>\n" }]
20788
+ args: [{ selector: 'qd-filter-category-select', host: { class: 'qd-filter__category-select' }, changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<div\n *ngIf=\"showSelectButton$ | async\"\n [class]=\"buttonClassName\"\n [qdPopoverOnClick]=\"filterLayer\"\n [qdPopoverCloseStrategy]=\"'onOutsideClick'\"\n [qdPopoverStopPropagation]=\"true\"\n [qdPopoverMaxHeight]=\"maxFlyoutHeight\"\n [positionStrategy]=\"positionStrategy\"\n (opened)=\"onLayerOpened()\"\n (closed)=\"onLayerClosed()\"\n [attr.data-test-id]=\"testId + '-select-button'\"\n>\n <qd-icon\n *ngIf=\"!open\"\n [icon]=\"'ctrlDown'\"\n [class]=\"'qd-filter__category-icon qd-filter__category-icon--closed'\"\n [attr.data-test-id]=\"testId + '-closed'\"\n ></qd-icon>\n <qd-icon\n *ngIf=\"open\"\n [icon]=\"'ctrlTop'\"\n [class]=\"'qd-filter__category-icon qd-filter__category-icon--open'\"\n [attr.data-test-id]=\"testId + '-opened'\"\n ></qd-icon>\n\n <div class=\"qd-filter__category-button-caption\">\n <span>\n {{ i18n | translate }}\n </span>\n </div>\n\n <ng-container *ngTemplateOutlet=\"SelectedChip\"></ng-container>\n</div>\n\n<ng-template #filterLayer>\n <div [class]=\"layerContentClassName\">\n <qd-icon\n *ngIf=\"closeButton\"\n [class]=\"'qd-filter__category-layer-close'\"\n (click)=\"closeLayer()\"\n [icon]=\"'timesLargeLight'\"\n [attr.data-test-id]=\"testId + '-layer-close'\"\n ></qd-icon>\n\n <ng-container [ngSwitch]=\"type\">\n <ng-container *ngSwitchCase=\"'multiSelect'\">\n <qd-filter-form-items\n *ngIf=\"filter\"\n [inputFilterValue]=\"filterCategoryValue$ | async\"\n (filterValueChange)=\"changeValue($event)\"\n [data-test-id]=\"testId\"\n ></qd-filter-form-items>\n\n <div class=\"qd-filter__category-layer-items\">\n <ng-container *ngFor=\"let item of items; let itemIndex = index\">\n <qd-filter-item-multi-select\n *ngIf=\"!item.hidden\"\n [categoryIndex]=\"categoryIndex\"\n [itemIndex]=\"itemIndex\"\n [filterId]=\"filterId\"\n [data-test-id]=\"testId + '-item-' + itemIndex + '-' + item.item\"\n >\n </qd-filter-item-multi-select>\n </ng-container>\n </div>\n </ng-container>\n\n <ng-container *ngSwitchCase=\"'singleSelect'\">\n <qd-filter-form-items\n *ngIf=\"filter\"\n [inputFilterValue]=\"filterCategoryValue$ | async\"\n (filterValueChange)=\"changeValue($event)\"\n [data-test-id]=\"testId\"\n ></qd-filter-form-items>\n\n <div class=\"qd-filter__category-layer-items\">\n <ng-container *ngFor=\"let item of items; let itemIndex = index\">\n <qd-filter-item-single-select\n *ngIf=\"!item.hidden\"\n [categoryIndex]=\"categoryIndex\"\n [itemIndex]=\"itemIndex\"\n (closeEventEmitter)=\"closeLayer()\"\n [filterId]=\"filterId\"\n [data-test-id]=\"testId + '-item-' + itemIndex + '-' + item.item\"\n >\n </qd-filter-item-single-select>\n </ng-container>\n </div>\n </ng-container>\n </ng-container>\n </div>\n</ng-template>\n\n<ng-template #SelectedChip>\n <ng-container *ngFor=\"let item of items; let itemIndex = index\">\n <!-- TODO: Add tooltip-->\n <qd-chip\n *ngIf=\"item.active\"\n [state]=\"'filter'\"\n [close]=\"true\"\n (closeClickEmitter)=\"close($event)\"\n [data]=\"itemIndex\"\n [data-test-id]=\"testId + '-selected-chip-' + item.item\"\n >{{ item.i18n | translate }}</qd-chip\n >\n </ng-container>\n</ng-template>\n", styles: ["::ng-deep .qd-filter__category-layer{display:flex;overflow:hidden;min-width:16.25rem;max-width:56.25rem;height:100%;flex-direction:column;padding:1rem 0 0;border:.0625rem solid rgb(151,151,151);background:#fff;box-shadow:#d5d5d5 .125rem .25rem .4375rem}::ng-deep .qd-filter__category-layer .qd-filter-form-items__filter{flex-shrink:0;margin-bottom:0}::ng-deep .qd-filter__category-layer .qd-checkbox__label{display:flex;height:2.5rem}::ng-deep .qd-filter__category-layer .qd-checkbox__indicator,::ng-deep .qd-filter__category-layer .qd-radio-buttons__indicator{transform:translateY(-.0625rem)}::ng-deep .qd-filter__category-layer-items{flex:1;overflow-y:auto}::ng-deep .qd-filter__category-layer-container{position:relative}::ng-deep .qd-filter__category-layer-close{position:absolute;z-index:1;top:1rem;right:1rem;color:#757575;cursor:pointer}::ng-deep .qd-filter__category-layer-close:before{position:absolute;content:\"\";inset:-.5rem}::ng-deep .qd-filter__category-layer-close:hover,::ng-deep .qd-filter__category-layer-close:focus{color:#171717}\n"] }]
20744
20789
  }], propDecorators: { filterId: [{
20745
20790
  type: Input
20746
20791
  }], categoryIndex: [{
@@ -27158,18 +27203,24 @@ class QdFormGroupManagerService {
27158
27203
  const snapshot = this._formGroupsSnapshot.get(key);
27159
27204
  if (!snapshot)
27160
27205
  return;
27161
- Object.entries(fg.controls).forEach(([ctrlKey, ctrl]) => {
27162
- const newValue = snapshot[ctrlKey];
27163
- if (ctrl instanceof FormArray && Array.isArray(newValue)) {
27164
- this.resetFormArrayToValues(ctrl, newValue);
27165
- }
27166
- else {
27167
- ctrl.reset(newValue);
27168
- }
27169
- });
27206
+ this.restoreFormGroup(fg, snapshot);
27170
27207
  });
27171
27208
  this.cancelPendingAsyncValidation();
27172
27209
  }
27210
+ restoreFormGroup(fg, snapshot) {
27211
+ Object.entries(fg.controls).forEach(([ctrlKey, ctrl]) => {
27212
+ const newValue = snapshot[ctrlKey];
27213
+ if (ctrl instanceof FormArray && Array.isArray(newValue)) {
27214
+ this.resetFormArrayToValues(ctrl, newValue);
27215
+ }
27216
+ else if (ctrl instanceof FormGroup && newValue && typeof newValue === 'object') {
27217
+ this.restoreFormGroup(ctrl, newValue);
27218
+ }
27219
+ else {
27220
+ ctrl.reset(newValue);
27221
+ }
27222
+ });
27223
+ }
27173
27224
  /**
27174
27225
  * Cancels any in-flight async validators on all registered form groups.
27175
27226
  *
@@ -28536,6 +28587,121 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
28536
28587
  type: Output
28537
28588
  }] } });
28538
28589
 
28590
+ /**
28591
+ * Intercepts router navigation when unsaved form changes exist on a QdPage.
28592
+ *
28593
+ * Provided per `QdPageComponent`. Activated automatically when the page enters an editable state
28594
+ * (create pages or inspect pages in edit mode). Deactivated when the page returns to view mode.
28595
+ *
28596
+ * #### When navigation is intercepted
28597
+ *
28598
+ * - The user has unsaved form changes (tracked via `QdFormGroupManagerService`).
28599
+ * - A `NavigationStart` event occurs (browser back, shell back button, or programmatic navigation).
28600
+ *
28601
+ * The current navigation is cancelled, and a confirmation dialog is shown. The user can either
28602
+ * discard changes and proceed or cancel and stay on the page.
28603
+ *
28604
+ * #### When navigation is allowed
28605
+ *
28606
+ * - No unsaved changes exist — navigation proceeds silently.
28607
+ * - A framework action (Submit, SaveDraft) wraps its handler via `executeWithBypass()`.
28608
+ * - A confirmed discard sets `allowNextNavigation()` before the cancel handler navigates.
28609
+ * - The page switches to view mode via `deactivate()`.
28610
+ *
28611
+ * Custom actions defined by the application are not bypassed. If a custom action navigates away
28612
+ * while unsaved changes exist, the confirmation dialog is shown.
28613
+ */
28614
+ class QdPageNavigationInterceptorService {
28615
+ router = inject(Router);
28616
+ formGroupManager = inject(QdFormGroupManagerService);
28617
+ confirmationDialog = inject(QdConfirmationDialogOpenerService);
28618
+ _destroy$ = new Subject();
28619
+ _deactivate$ = new Subject();
28620
+ _bypassInterception = false;
28621
+ _allowedTargetUrls = new Set();
28622
+ _confirmationMessage;
28623
+ ngOnDestroy() {
28624
+ this._allowedTargetUrls.clear();
28625
+ this._destroy$.next();
28626
+ this._destroy$.complete();
28627
+ this._deactivate$.complete();
28628
+ }
28629
+ /**
28630
+ * Starts intercepting navigation events. Replaces any previously active listener.
28631
+ */
28632
+ activate(confirmationMessage) {
28633
+ this._confirmationMessage = confirmationMessage;
28634
+ this._deactivate$.next();
28635
+ this.listenForUnsavedNavigationAttempts();
28636
+ }
28637
+ /**
28638
+ * Stops intercepting and clears all pending URL bypasses.
28639
+ */
28640
+ deactivate() {
28641
+ this._deactivate$.next();
28642
+ this._allowedTargetUrls.clear();
28643
+ }
28644
+ /**
28645
+ * Whitelists the next navigation so it bypasses interception.
28646
+ * Without a URL, any next navigation is bypassed (wildcard). With a URL, only that
28647
+ * specific navigation is bypassed — non-matching navigations are still intercepted.
28648
+ */
28649
+ allowNextNavigation(targetUrl) {
28650
+ this._allowedTargetUrls.add(targetUrl ?? '*');
28651
+ }
28652
+ /**
28653
+ * Executes the callback with interception temporarily disabled.
28654
+ * The callback must navigate synchronously — async navigation after the callback returns
28655
+ * will not be bypassed. This works because Angular's router emits NavigationStart
28656
+ * synchronously within the navigateByUrl() / navigate() call.
28657
+ */
28658
+ executeWithBypass(fn) {
28659
+ this._bypassInterception = true;
28660
+ try {
28661
+ fn();
28662
+ }
28663
+ finally {
28664
+ this._bypassInterception = false;
28665
+ }
28666
+ }
28667
+ listenForUnsavedNavigationAttempts() {
28668
+ this.router.events
28669
+ .pipe(filter$1((event) => event instanceof NavigationStart), filter$1(() => !this._bypassInterception), filter$1(event => this.shouldIntercept(event)), concatMap$1(event => this.checkForPendingChanges(event)), filter$1(({ hasChanges }) => hasChanges), exhaustMap(({ event }) => this.cancelNavigationAndConfirm(event)), filter$1(({ confirmed }) => confirmed), takeUntil$1(this._deactivate$), takeUntil$1(this._destroy$))
28670
+ .subscribe(({ targetUrl }) => this.navigateToConfirmedTarget(targetUrl));
28671
+ }
28672
+ shouldIntercept(event) {
28673
+ if (this._allowedTargetUrls.has('*')) {
28674
+ this._allowedTargetUrls.clear();
28675
+ return false;
28676
+ }
28677
+ if (this._allowedTargetUrls.has(event.url)) {
28678
+ this._allowedTargetUrls.delete(event.url);
28679
+ return false;
28680
+ }
28681
+ return true;
28682
+ }
28683
+ checkForPendingChanges(event) {
28684
+ return this.formGroupManager.$hasValuesChanged().pipe(take$1(1), map$1(hasChanges => ({ event, hasChanges })));
28685
+ }
28686
+ cancelNavigationAndConfirm(event) {
28687
+ this._bypassInterception = true;
28688
+ void this.router.navigateByUrl(this.router.url, { skipLocationChange: true });
28689
+ return this.confirmationDialog
28690
+ .showCancelConfirmation({ cancel: { confirmationMessage: this._confirmationMessage } })
28691
+ .pipe(map$1(confirmed => ({ confirmed, targetUrl: event.url })), finalize(() => (this._bypassInterception = false)));
28692
+ }
28693
+ navigateToConfirmedTarget(targetUrl) {
28694
+ this._bypassInterception = false;
28695
+ this._allowedTargetUrls.add(targetUrl);
28696
+ void this.router.navigateByUrl(targetUrl);
28697
+ }
28698
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPageNavigationInterceptorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
28699
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPageNavigationInterceptorService });
28700
+ }
28701
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPageNavigationInterceptorService, decorators: [{
28702
+ type: Injectable
28703
+ }] });
28704
+
28539
28705
  class QdPageSubmitActionService {
28540
28706
  footerService = inject(QdPageFooterService);
28541
28707
  formGroupManagerService = inject(QdFormGroupManagerService);
@@ -28997,6 +29163,7 @@ class QdPageComponent {
28997
29163
  dialog = inject(QdDialogService);
28998
29164
  bottomOffset$ = inject(QD_SAFE_BOTTOM_OFFSET, { optional: true });
28999
29165
  dialogRef = inject(DialogRef, { optional: true });
29166
+ navigationInterceptor = inject(QdPageNavigationInterceptorService);
29000
29167
  /**
29001
29168
  * This property defines the configuration for the QdPage component, including the page type,
29002
29169
  * title, and specific configurations for each type of page.
@@ -29064,8 +29231,10 @@ class QdPageComponent {
29064
29231
  ]);
29065
29232
  }
29066
29233
  ngAfterViewInit() {
29067
- if (this.config.pageType === 'create')
29234
+ if (this.config.pageType === 'create') {
29068
29235
  this.setupCreatePageFooterActions();
29236
+ this.navigationInterceptor.activate(this.config.pageTypeConfig?.cancel?.confirmationMessage);
29237
+ }
29069
29238
  if (this.config.pageType === 'create' && this.config?.pageTypeConfig?.cancel !== undefined)
29070
29239
  this.handleCancelActionWithFormChanges();
29071
29240
  if (this.config.pageType === 'create' && this.config?.pageTypeConfig?.saveDraft !== undefined)
@@ -29115,8 +29284,12 @@ class QdPageComponent {
29115
29284
  actionKey: 'cancel',
29116
29285
  partialAction: {
29117
29286
  handler: hasChanged
29118
- ? () => this.setupSubmitActionValidation()
29119
- : () => this.config?.pageTypeConfig?.cancel?.handler()
29287
+ ? () => this.setupCancelConfirmation()
29288
+ : () => {
29289
+ this.navigationInterceptor.executeWithBypass(() => {
29290
+ this.config?.pageTypeConfig?.cancel?.handler();
29291
+ });
29292
+ }
29120
29293
  }
29121
29294
  }
29122
29295
  ]);
@@ -29130,7 +29303,7 @@ class QdPageComponent {
29130
29303
  action: {
29131
29304
  titleI18n: pageTypeConfig.saveDraft?.label?.i18n ?? 'i18n.qd.page.footer.saveDraft',
29132
29305
  type: QdFooterActionType.Secondary,
29133
- handler: () => pageTypeConfig?.saveDraft?.handler(),
29306
+ handler: this.generateFooterActionHandler(pageTypeConfig?.saveDraft?.handler),
29134
29307
  isVisible: true,
29135
29308
  isDisabled: false
29136
29309
  }
@@ -29138,6 +29311,12 @@ class QdPageComponent {
29138
29311
  ]);
29139
29312
  }
29140
29313
  updateInspectPageOperationMode(pageTypeConfig, mode) {
29314
+ if (mode === 'edit') {
29315
+ this.navigationInterceptor.activate(pageTypeConfig?.cancel?.confirmationMessage);
29316
+ }
29317
+ else {
29318
+ this.navigationInterceptor.deactivate();
29319
+ }
29141
29320
  if (mode === 'view') {
29142
29321
  this.formGroupManagerService.cancelPendingAsyncValidation();
29143
29322
  setTimeout(() => this.formGroupManagerService.cancelPendingAsyncValidation());
@@ -29155,18 +29334,19 @@ class QdPageComponent {
29155
29334
  return (...args) => {
29156
29335
  if (!handler)
29157
29336
  return;
29158
- handler(this.formGroupManagerService.hasFormGroups() ? this.formGroupManagerService.getAllValues() : args);
29337
+ const values = this.formGroupManagerService.hasFormGroups() ? this.formGroupManagerService.getAllValues() : args;
29338
+ this.navigationInterceptor.executeWithBypass(() => handler(values));
29159
29339
  };
29160
29340
  }
29161
- setupSubmitActionValidation() {
29341
+ setupCancelConfirmation() {
29162
29342
  this.dialog
29163
29343
  .open(QdPageCancelConfirmationDialogComponent, {
29164
29344
  title: { i18n: 'i18n.qd.page.cancel.confirmation.dialog.title' },
29165
29345
  dialogSize: QdDialogSize.Small,
29166
29346
  data: this.config.pageTypeConfig
29167
29347
  })
29168
- .closed.pipe(takeUntil(this._destroyed$))
29169
- .subscribe();
29348
+ .closed.pipe(filter$1(result => !!result), takeUntil(this._destroyed$))
29349
+ .subscribe(() => this.navigationInterceptor.allowNextNavigation());
29170
29350
  }
29171
29351
  initSubmitValidation() {
29172
29352
  this._cancelSubmitValidation$.next();
@@ -29203,11 +29383,23 @@ class QdPageComponent {
29203
29383
  }))), this.dialogRef);
29204
29384
  }
29205
29385
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
29206
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: QdPageComponent, isStandalone: false, selector: "qd-page", inputs: { config: "config", testId: ["data-test-id", "testId"] }, outputs: { operationModeChanged: "operationModeChanged" }, host: { properties: { "class.has-control-panel": "isControlPanelVisible", "class.control-panel-broad": "isControlPanelBroad", "class.has-footer": "this.footerVisible", "class.has-info-banners": "this.hasInfoBanners" } }, providers: [QdPageFooterService, QdFormGroupManagerService, QdPageSubmitActionService, QdResolverTriggerService], queries: [{ propertyName: "controlPanel", first: true, predicate: QdPageControlPanelComponent, descendants: true }, { propertyName: "stepperComponent", first: true, predicate: QdPageStepperComponent, descendants: true }, { propertyName: "tabsComponent", first: true, predicate: QdPageTabsComponent, descendants: true }, { propertyName: "stepperAdapterDirective", first: true, predicate: QdPageStepperAdapterDirective, descendants: true }, { propertyName: "tabsAdapterDirective", first: true, predicate: QdPageTabsAdapterDirective, descendants: true }, { propertyName: "sections", predicate: QdSectionComponent }, { propertyName: "infoBanners", predicate: QdPageInfoBannerComponent }], usesOnChanges: true, ngImport: i0, template: "<main qdSnackbarListener>\n <qd-page-object-header\n [data-test-id]=\"testId\"\n [attr.data-test-id]=\"testId + 'object-header'\"\n [config]=\"config\"\n [hasNavigation]=\"hasNavigation\"\n ></qd-page-object-header>\n\n <div class=\"page-info-banners\">\n <ng-content select=\"qd-page-info-banner\"></ng-content>\n </div>\n\n <ng-container *ngIf=\"config.pageType === 'create' || config.pageType === 'custom'\">\n <ng-content select=\"qd-page-stepper\"></ng-content>\n <ng-content select=\"[qdPageStepperAdapter]\"></ng-content>\n </ng-container>\n\n <ng-container *ngIf=\"config.pageType === 'overview' || config.pageType === 'inspect' || config.pageType === 'custom'\">\n <ng-content select=\"qd-page-tabs\"></ng-content>\n <ng-content select=\"[qdPageTabsAdapter]\"></ng-content>\n <ng-content select=\"qd-section\"></ng-content>\n <ng-content select=\"[qdSectionAdapter]\"></ng-content>\n </ng-container>\n\n <ng-container *ngIf=\"config.pageType === 'custom'\">\n <ng-container *ngTemplateOutlet=\"projectedContent\"></ng-container>\n </ng-container>\n</main>\n\n<footer *ngIf=\"footerHasContent$ | async\">\n <qd-page-footer [attr.data-test-id]=\"testId + '-footer'\">\n <ng-content select=\"[qdPageFooter]\"></ng-content>\n </qd-page-footer>\n</footer>\n\n<aside *ngIf=\"isControlPanelVisible\">\n <ng-content select=\"qd-page-control-panel\"></ng-content>\n</aside>\n\n<qd-projection-guard *ngIf=\"config.pageType !== 'custom'\" [warningMessage]=\"projectionGuardMessage\">\n <ng-container *ngTemplateOutlet=\"projectedContent\"></ng-container>\n</qd-projection-guard>\n\n<ng-template #projectedContent>\n <ng-content></ng-content>\n</ng-template>\n", styles: [":host{position:absolute;display:grid;overflow:hidden;width:100%;height:100%;background:#efefef;grid-template-areas:\"main\";grid-template-columns:1fr;grid-template-rows:1fr}:host.has-control-panel{grid-template-areas:\"main aside\";grid-template-columns:1fr 18.75rem}:host.control-panel-broad{grid-template-columns:1fr 27rem}:host.has-info-banners .page-info-banners{padding:1rem 1.25rem .5rem;border-bottom:rgb(213,213,213) solid .0625rem;background-color:#fff}@media (max-width: 599.98px){:host.has-info-banners .page-info-banners{padding-right:.9375rem;padding-left:.9375rem}}:host.has-info-banners qd-page-info-banner:last-child{margin-bottom:0}:host.has-footer{grid-template-areas:\"main\" \"footer\";grid-template-rows:calc(100% - 4rem) 4rem}:host.has-control-panel.has-footer{grid-template-areas:\"main aside\" \"footer aside\"}:host main{position:relative;grid-area:main;overflow-y:auto}:host aside{border-left:rgb(213,213,213) solid .0625rem;background:#fff;grid-area:aside}:host footer{grid-area:footer}:host ::ng-deep .qd-page-info-banner-info+.qd-page-info-banner-info,:host ::ng-deep .qd-page-info-banner-warning+.qd-page-info-banner-warning,:host ::ng-deep .qd-page-info-banner-critical+.qd-page-info-banner-critical,:host ::ng-deep .qd-page-info-banner-success+.qd-page-info-banner-success{position:relative;top:-.5rem;padding-top:0rem;padding-bottom:0rem;border-top-width:0;margin-bottom:0rem}:host ::ng-deep .qd-page-info-banner-info+.qd-page-info-banner-info .icon,:host ::ng-deep .qd-page-info-banner-warning+.qd-page-info-banner-warning .icon,:host ::ng-deep .qd-page-info-banner-critical+.qd-page-info-banner-critical .icon,:host ::ng-deep .qd-page-info-banner-success+.qd-page-info-banner-success .icon{display:none}:host ::ng-deep .qd-page-info-banner-info+.qd-page-info-banner-info .title,:host ::ng-deep .qd-page-info-banner-info+.qd-page-info-banner-info .message,:host ::ng-deep .qd-page-info-banner-warning+.qd-page-info-banner-warning .title,:host ::ng-deep .qd-page-info-banner-warning+.qd-page-info-banner-warning .message,:host ::ng-deep .qd-page-info-banner-critical+.qd-page-info-banner-critical .title,:host ::ng-deep .qd-page-info-banner-critical+.qd-page-info-banner-critical .message,:host ::ng-deep .qd-page-info-banner-success+.qd-page-info-banner-success .title,:host ::ng-deep .qd-page-info-banner-success+.qd-page-info-banner-success .message{position:relative;top:-.625rem}:host ::ng-deep .qd-page-info-banner-info+.qd-page-info-banner-info:before,:host ::ng-deep .qd-page-info-banner-warning+.qd-page-info-banner-warning:before,:host ::ng-deep .qd-page-info-banner-critical+.qd-page-info-banner-critical:before,:host ::ng-deep .qd-page-info-banner-success+.qd-page-info-banner-success:before{position:relative;top:-.625rem;left:-2.75rem;display:block;width:calc(100% + 4.75rem);height:.625rem;background-color:#fff;content:\"\"}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: QdProjectionGuardComponent, selector: "qd-projection-guard", inputs: ["isDisabled", "warningMessage"] }, { kind: "component", type: QdPageFooterComponent, selector: "qd-page-footer" }, { kind: "component", type: QdPageObjectHeaderComponent, selector: "qd-page-object-header", inputs: ["config", "hasNavigation", "data-test-id"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }] });
29386
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: QdPageComponent, isStandalone: false, selector: "qd-page", inputs: { config: "config", testId: ["data-test-id", "testId"] }, outputs: { operationModeChanged: "operationModeChanged" }, host: { properties: { "class.has-control-panel": "isControlPanelVisible", "class.control-panel-broad": "isControlPanelBroad", "class.has-footer": "this.footerVisible", "class.has-info-banners": "this.hasInfoBanners" } }, providers: [
29387
+ QdPageFooterService,
29388
+ QdFormGroupManagerService,
29389
+ QdPageSubmitActionService,
29390
+ QdResolverTriggerService,
29391
+ QdPageNavigationInterceptorService
29392
+ ], queries: [{ propertyName: "controlPanel", first: true, predicate: QdPageControlPanelComponent, descendants: true }, { propertyName: "stepperComponent", first: true, predicate: QdPageStepperComponent, descendants: true }, { propertyName: "tabsComponent", first: true, predicate: QdPageTabsComponent, descendants: true }, { propertyName: "stepperAdapterDirective", first: true, predicate: QdPageStepperAdapterDirective, descendants: true }, { propertyName: "tabsAdapterDirective", first: true, predicate: QdPageTabsAdapterDirective, descendants: true }, { propertyName: "sections", predicate: QdSectionComponent }, { propertyName: "infoBanners", predicate: QdPageInfoBannerComponent }], usesOnChanges: true, ngImport: i0, template: "<main qdSnackbarListener>\n <qd-page-object-header\n [data-test-id]=\"testId\"\n [attr.data-test-id]=\"testId + 'object-header'\"\n [config]=\"config\"\n [hasNavigation]=\"hasNavigation\"\n ></qd-page-object-header>\n\n <div class=\"page-info-banners\">\n <ng-content select=\"qd-page-info-banner\"></ng-content>\n </div>\n\n <ng-container *ngIf=\"config.pageType === 'create' || config.pageType === 'custom'\">\n <ng-content select=\"qd-page-stepper\"></ng-content>\n <ng-content select=\"[qdPageStepperAdapter]\"></ng-content>\n </ng-container>\n\n <ng-container *ngIf=\"config.pageType === 'overview' || config.pageType === 'inspect' || config.pageType === 'custom'\">\n <ng-content select=\"qd-page-tabs\"></ng-content>\n <ng-content select=\"[qdPageTabsAdapter]\"></ng-content>\n <ng-content select=\"qd-section\"></ng-content>\n <ng-content select=\"[qdSectionAdapter]\"></ng-content>\n </ng-container>\n\n <ng-container *ngIf=\"config.pageType === 'custom'\">\n <ng-container *ngTemplateOutlet=\"projectedContent\"></ng-container>\n </ng-container>\n</main>\n\n<footer *ngIf=\"footerHasContent$ | async\">\n <qd-page-footer [attr.data-test-id]=\"testId + '-footer'\">\n <ng-content select=\"[qdPageFooter]\"></ng-content>\n </qd-page-footer>\n</footer>\n\n<aside *ngIf=\"isControlPanelVisible\">\n <ng-content select=\"qd-page-control-panel\"></ng-content>\n</aside>\n\n<qd-projection-guard *ngIf=\"config.pageType !== 'custom'\" [warningMessage]=\"projectionGuardMessage\">\n <ng-container *ngTemplateOutlet=\"projectedContent\"></ng-container>\n</qd-projection-guard>\n\n<ng-template #projectedContent>\n <ng-content></ng-content>\n</ng-template>\n", styles: [":host{position:absolute;display:grid;overflow:hidden;width:100%;height:100%;background:#efefef;grid-template-areas:\"main\";grid-template-columns:1fr;grid-template-rows:1fr}:host.has-control-panel{grid-template-areas:\"main aside\";grid-template-columns:1fr 18.75rem}:host.control-panel-broad{grid-template-columns:1fr 27rem}:host.has-info-banners .page-info-banners{padding:1rem 1.25rem .5rem;border-bottom:rgb(213,213,213) solid .0625rem;background-color:#fff}@media (max-width: 599.98px){:host.has-info-banners .page-info-banners{padding-right:.9375rem;padding-left:.9375rem}}:host.has-info-banners qd-page-info-banner:last-child{margin-bottom:0}:host.has-footer{grid-template-areas:\"main\" \"footer\";grid-template-rows:calc(100% - 4rem) 4rem}:host.has-control-panel.has-footer{grid-template-areas:\"main aside\" \"footer aside\"}:host main{position:relative;grid-area:main;overflow-y:auto}:host aside{border-left:rgb(213,213,213) solid .0625rem;background:#fff;grid-area:aside}:host footer{grid-area:footer}:host ::ng-deep .qd-page-info-banner-info+.qd-page-info-banner-info,:host ::ng-deep .qd-page-info-banner-warning+.qd-page-info-banner-warning,:host ::ng-deep .qd-page-info-banner-critical+.qd-page-info-banner-critical,:host ::ng-deep .qd-page-info-banner-success+.qd-page-info-banner-success{position:relative;top:-.5rem;padding-top:0rem;padding-bottom:0rem;border-top-width:0;margin-bottom:0rem}:host ::ng-deep .qd-page-info-banner-info+.qd-page-info-banner-info .icon,:host ::ng-deep .qd-page-info-banner-warning+.qd-page-info-banner-warning .icon,:host ::ng-deep .qd-page-info-banner-critical+.qd-page-info-banner-critical .icon,:host ::ng-deep .qd-page-info-banner-success+.qd-page-info-banner-success .icon{display:none}:host ::ng-deep .qd-page-info-banner-info+.qd-page-info-banner-info .title,:host ::ng-deep .qd-page-info-banner-info+.qd-page-info-banner-info .message,:host ::ng-deep .qd-page-info-banner-warning+.qd-page-info-banner-warning .title,:host ::ng-deep .qd-page-info-banner-warning+.qd-page-info-banner-warning .message,:host ::ng-deep .qd-page-info-banner-critical+.qd-page-info-banner-critical .title,:host ::ng-deep .qd-page-info-banner-critical+.qd-page-info-banner-critical .message,:host ::ng-deep .qd-page-info-banner-success+.qd-page-info-banner-success .title,:host ::ng-deep .qd-page-info-banner-success+.qd-page-info-banner-success .message{position:relative;top:-.625rem}:host ::ng-deep .qd-page-info-banner-info+.qd-page-info-banner-info:before,:host ::ng-deep .qd-page-info-banner-warning+.qd-page-info-banner-warning:before,:host ::ng-deep .qd-page-info-banner-critical+.qd-page-info-banner-critical:before,:host ::ng-deep .qd-page-info-banner-success+.qd-page-info-banner-success:before{position:relative;top:-.625rem;left:-2.75rem;display:block;width:calc(100% + 4.75rem);height:.625rem;background-color:#fff;content:\"\"}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: QdProjectionGuardComponent, selector: "qd-projection-guard", inputs: ["isDisabled", "warningMessage"] }, { kind: "component", type: QdPageFooterComponent, selector: "qd-page-footer" }, { kind: "component", type: QdPageObjectHeaderComponent, selector: "qd-page-object-header", inputs: ["config", "hasNavigation", "data-test-id"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }] });
29207
29393
  }
29208
29394
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPageComponent, decorators: [{
29209
29395
  type: Component,
29210
- args: [{ selector: 'qd-page', providers: [QdPageFooterService, QdFormGroupManagerService, QdPageSubmitActionService, QdResolverTriggerService], host: { '[class.has-control-panel]': 'isControlPanelVisible', '[class.control-panel-broad]': 'isControlPanelBroad' }, standalone: false, template: "<main qdSnackbarListener>\n <qd-page-object-header\n [data-test-id]=\"testId\"\n [attr.data-test-id]=\"testId + 'object-header'\"\n [config]=\"config\"\n [hasNavigation]=\"hasNavigation\"\n ></qd-page-object-header>\n\n <div class=\"page-info-banners\">\n <ng-content select=\"qd-page-info-banner\"></ng-content>\n </div>\n\n <ng-container *ngIf=\"config.pageType === 'create' || config.pageType === 'custom'\">\n <ng-content select=\"qd-page-stepper\"></ng-content>\n <ng-content select=\"[qdPageStepperAdapter]\"></ng-content>\n </ng-container>\n\n <ng-container *ngIf=\"config.pageType === 'overview' || config.pageType === 'inspect' || config.pageType === 'custom'\">\n <ng-content select=\"qd-page-tabs\"></ng-content>\n <ng-content select=\"[qdPageTabsAdapter]\"></ng-content>\n <ng-content select=\"qd-section\"></ng-content>\n <ng-content select=\"[qdSectionAdapter]\"></ng-content>\n </ng-container>\n\n <ng-container *ngIf=\"config.pageType === 'custom'\">\n <ng-container *ngTemplateOutlet=\"projectedContent\"></ng-container>\n </ng-container>\n</main>\n\n<footer *ngIf=\"footerHasContent$ | async\">\n <qd-page-footer [attr.data-test-id]=\"testId + '-footer'\">\n <ng-content select=\"[qdPageFooter]\"></ng-content>\n </qd-page-footer>\n</footer>\n\n<aside *ngIf=\"isControlPanelVisible\">\n <ng-content select=\"qd-page-control-panel\"></ng-content>\n</aside>\n\n<qd-projection-guard *ngIf=\"config.pageType !== 'custom'\" [warningMessage]=\"projectionGuardMessage\">\n <ng-container *ngTemplateOutlet=\"projectedContent\"></ng-container>\n</qd-projection-guard>\n\n<ng-template #projectedContent>\n <ng-content></ng-content>\n</ng-template>\n", styles: [":host{position:absolute;display:grid;overflow:hidden;width:100%;height:100%;background:#efefef;grid-template-areas:\"main\";grid-template-columns:1fr;grid-template-rows:1fr}:host.has-control-panel{grid-template-areas:\"main aside\";grid-template-columns:1fr 18.75rem}:host.control-panel-broad{grid-template-columns:1fr 27rem}:host.has-info-banners .page-info-banners{padding:1rem 1.25rem .5rem;border-bottom:rgb(213,213,213) solid .0625rem;background-color:#fff}@media (max-width: 599.98px){:host.has-info-banners .page-info-banners{padding-right:.9375rem;padding-left:.9375rem}}:host.has-info-banners qd-page-info-banner:last-child{margin-bottom:0}:host.has-footer{grid-template-areas:\"main\" \"footer\";grid-template-rows:calc(100% - 4rem) 4rem}:host.has-control-panel.has-footer{grid-template-areas:\"main aside\" \"footer aside\"}:host main{position:relative;grid-area:main;overflow-y:auto}:host aside{border-left:rgb(213,213,213) solid .0625rem;background:#fff;grid-area:aside}:host footer{grid-area:footer}:host ::ng-deep .qd-page-info-banner-info+.qd-page-info-banner-info,:host ::ng-deep .qd-page-info-banner-warning+.qd-page-info-banner-warning,:host ::ng-deep .qd-page-info-banner-critical+.qd-page-info-banner-critical,:host ::ng-deep .qd-page-info-banner-success+.qd-page-info-banner-success{position:relative;top:-.5rem;padding-top:0rem;padding-bottom:0rem;border-top-width:0;margin-bottom:0rem}:host ::ng-deep .qd-page-info-banner-info+.qd-page-info-banner-info .icon,:host ::ng-deep .qd-page-info-banner-warning+.qd-page-info-banner-warning .icon,:host ::ng-deep .qd-page-info-banner-critical+.qd-page-info-banner-critical .icon,:host ::ng-deep .qd-page-info-banner-success+.qd-page-info-banner-success .icon{display:none}:host ::ng-deep .qd-page-info-banner-info+.qd-page-info-banner-info .title,:host ::ng-deep .qd-page-info-banner-info+.qd-page-info-banner-info .message,:host ::ng-deep .qd-page-info-banner-warning+.qd-page-info-banner-warning .title,:host ::ng-deep .qd-page-info-banner-warning+.qd-page-info-banner-warning .message,:host ::ng-deep .qd-page-info-banner-critical+.qd-page-info-banner-critical .title,:host ::ng-deep .qd-page-info-banner-critical+.qd-page-info-banner-critical .message,:host ::ng-deep .qd-page-info-banner-success+.qd-page-info-banner-success .title,:host ::ng-deep .qd-page-info-banner-success+.qd-page-info-banner-success .message{position:relative;top:-.625rem}:host ::ng-deep .qd-page-info-banner-info+.qd-page-info-banner-info:before,:host ::ng-deep .qd-page-info-banner-warning+.qd-page-info-banner-warning:before,:host ::ng-deep .qd-page-info-banner-critical+.qd-page-info-banner-critical:before,:host ::ng-deep .qd-page-info-banner-success+.qd-page-info-banner-success:before{position:relative;top:-.625rem;left:-2.75rem;display:block;width:calc(100% + 4.75rem);height:.625rem;background-color:#fff;content:\"\"}\n"] }]
29396
+ args: [{ selector: 'qd-page', providers: [
29397
+ QdPageFooterService,
29398
+ QdFormGroupManagerService,
29399
+ QdPageSubmitActionService,
29400
+ QdResolverTriggerService,
29401
+ QdPageNavigationInterceptorService
29402
+ ], host: { '[class.has-control-panel]': 'isControlPanelVisible', '[class.control-panel-broad]': 'isControlPanelBroad' }, standalone: false, template: "<main qdSnackbarListener>\n <qd-page-object-header\n [data-test-id]=\"testId\"\n [attr.data-test-id]=\"testId + 'object-header'\"\n [config]=\"config\"\n [hasNavigation]=\"hasNavigation\"\n ></qd-page-object-header>\n\n <div class=\"page-info-banners\">\n <ng-content select=\"qd-page-info-banner\"></ng-content>\n </div>\n\n <ng-container *ngIf=\"config.pageType === 'create' || config.pageType === 'custom'\">\n <ng-content select=\"qd-page-stepper\"></ng-content>\n <ng-content select=\"[qdPageStepperAdapter]\"></ng-content>\n </ng-container>\n\n <ng-container *ngIf=\"config.pageType === 'overview' || config.pageType === 'inspect' || config.pageType === 'custom'\">\n <ng-content select=\"qd-page-tabs\"></ng-content>\n <ng-content select=\"[qdPageTabsAdapter]\"></ng-content>\n <ng-content select=\"qd-section\"></ng-content>\n <ng-content select=\"[qdSectionAdapter]\"></ng-content>\n </ng-container>\n\n <ng-container *ngIf=\"config.pageType === 'custom'\">\n <ng-container *ngTemplateOutlet=\"projectedContent\"></ng-container>\n </ng-container>\n</main>\n\n<footer *ngIf=\"footerHasContent$ | async\">\n <qd-page-footer [attr.data-test-id]=\"testId + '-footer'\">\n <ng-content select=\"[qdPageFooter]\"></ng-content>\n </qd-page-footer>\n</footer>\n\n<aside *ngIf=\"isControlPanelVisible\">\n <ng-content select=\"qd-page-control-panel\"></ng-content>\n</aside>\n\n<qd-projection-guard *ngIf=\"config.pageType !== 'custom'\" [warningMessage]=\"projectionGuardMessage\">\n <ng-container *ngTemplateOutlet=\"projectedContent\"></ng-container>\n</qd-projection-guard>\n\n<ng-template #projectedContent>\n <ng-content></ng-content>\n</ng-template>\n", styles: [":host{position:absolute;display:grid;overflow:hidden;width:100%;height:100%;background:#efefef;grid-template-areas:\"main\";grid-template-columns:1fr;grid-template-rows:1fr}:host.has-control-panel{grid-template-areas:\"main aside\";grid-template-columns:1fr 18.75rem}:host.control-panel-broad{grid-template-columns:1fr 27rem}:host.has-info-banners .page-info-banners{padding:1rem 1.25rem .5rem;border-bottom:rgb(213,213,213) solid .0625rem;background-color:#fff}@media (max-width: 599.98px){:host.has-info-banners .page-info-banners{padding-right:.9375rem;padding-left:.9375rem}}:host.has-info-banners qd-page-info-banner:last-child{margin-bottom:0}:host.has-footer{grid-template-areas:\"main\" \"footer\";grid-template-rows:calc(100% - 4rem) 4rem}:host.has-control-panel.has-footer{grid-template-areas:\"main aside\" \"footer aside\"}:host main{position:relative;grid-area:main;overflow-y:auto}:host aside{border-left:rgb(213,213,213) solid .0625rem;background:#fff;grid-area:aside}:host footer{grid-area:footer}:host ::ng-deep .qd-page-info-banner-info+.qd-page-info-banner-info,:host ::ng-deep .qd-page-info-banner-warning+.qd-page-info-banner-warning,:host ::ng-deep .qd-page-info-banner-critical+.qd-page-info-banner-critical,:host ::ng-deep .qd-page-info-banner-success+.qd-page-info-banner-success{position:relative;top:-.5rem;padding-top:0rem;padding-bottom:0rem;border-top-width:0;margin-bottom:0rem}:host ::ng-deep .qd-page-info-banner-info+.qd-page-info-banner-info .icon,:host ::ng-deep .qd-page-info-banner-warning+.qd-page-info-banner-warning .icon,:host ::ng-deep .qd-page-info-banner-critical+.qd-page-info-banner-critical .icon,:host ::ng-deep .qd-page-info-banner-success+.qd-page-info-banner-success .icon{display:none}:host ::ng-deep .qd-page-info-banner-info+.qd-page-info-banner-info .title,:host ::ng-deep .qd-page-info-banner-info+.qd-page-info-banner-info .message,:host ::ng-deep .qd-page-info-banner-warning+.qd-page-info-banner-warning .title,:host ::ng-deep .qd-page-info-banner-warning+.qd-page-info-banner-warning .message,:host ::ng-deep .qd-page-info-banner-critical+.qd-page-info-banner-critical .title,:host ::ng-deep .qd-page-info-banner-critical+.qd-page-info-banner-critical .message,:host ::ng-deep .qd-page-info-banner-success+.qd-page-info-banner-success .title,:host ::ng-deep .qd-page-info-banner-success+.qd-page-info-banner-success .message{position:relative;top:-.625rem}:host ::ng-deep .qd-page-info-banner-info+.qd-page-info-banner-info:before,:host ::ng-deep .qd-page-info-banner-warning+.qd-page-info-banner-warning:before,:host ::ng-deep .qd-page-info-banner-critical+.qd-page-info-banner-critical:before,:host ::ng-deep .qd-page-info-banner-success+.qd-page-info-banner-success:before{position:relative;top:-.625rem;left:-2.75rem;display:block;width:calc(100% + 4.75rem);height:.625rem;background-color:#fff;content:\"\"}\n"] }]
29211
29403
  }], ctorParameters: () => [], propDecorators: { config: [{
29212
29404
  type: Input,
29213
29405
  args: [{ required: true }]