@quadrel-enterprise-ui/framework 20.27.3-beta.234.1 → 20.27.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, ElementRef, Directive, InjectionToken, HostBinding, Input, ViewEncapsulation, Component, Injectable, Injector, HostListener, ChangeDetectionStrategy, ChangeDetectorRef, ViewChild, NgModule, EventEmitter, Output, Renderer2, Pipe, ViewContainerRef, NO_ERRORS_SCHEMA, DestroyRef, SecurityContext, NgZone, ViewChildren, forwardRef, ContentChildren, ContentChild, afterNextRender, isDevMode, QueryList, TemplateRef, CUSTOM_ELEMENTS_SCHEMA, provideAppInitializer } from '@angular/core';
2
+ import { inject, ElementRef, Directive, InjectionToken, HostBinding, Input, ViewEncapsulation, Component, Injectable, Injector, HostListener, ChangeDetectionStrategy, ChangeDetectorRef, ViewChild, NgModule, EventEmitter, Output, Renderer2, Pipe, ViewContainerRef, NO_ERRORS_SCHEMA, DestroyRef, SecurityContext, NgZone, ViewChildren, forwardRef, ContentChildren, ContentChild, isDevMode, QueryList, TemplateRef, CUSTOM_ELEMENTS_SCHEMA, provideAppInitializer } from '@angular/core';
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';
@@ -1550,11 +1550,11 @@ class QdDialogComponent {
1550
1550
  });
1551
1551
  }
1552
1552
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1553
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: QdDialogComponent, isStandalone: false, selector: "qd-dialog", host: { properties: { "class.full-width": "isFullWidth", "class.has-sections": "this.hasSectionsClass" } }, providers: [QdDialogComponent], viewQueries: [{ propertyName: "body", first: true, predicate: ["body"], descendants: true }, { propertyName: "infoBanners", first: true, predicate: ["banners"], descendants: true }], ngImport: i0, template: "<div *ngIf=\"!isFullWidth\" class=\"dialog-header\">\n <div class=\"title-container\">\n {{ config?.title?.i18n | translate }}\n </div>\n <button\n *ngIf=\"!config?.hideCloseHeaderButton\"\n qdIconButton\n class=\"close\"\n color=\"secondary\"\n tabindex=\"-1\"\n (click)=\"close()\"\n data-test-id=\"dialog-close-button\"\n >\n <qd-icon icon=\"timesLargeLight\"></qd-icon>\n </button>\n</div>\n\n<button\n *ngIf=\"!config?.hideCloseHeaderButton && isFullWidth\"\n qdIconButton\n class=\"close\"\n color=\"secondary\"\n tabindex=\"-1\"\n (click)=\"close()\"\n data-test-id=\"dialog-close-button\"\n>\n <qd-icon icon=\"timesLargeLight\"></qd-icon>\n</button>\n\n<div class=\"wrapper-body\">\n <div [class.has-dialog-info-banners]=\"hasInfoBanner\" #banners>\n <ng-content select=\"qd-page-info-banner\"></ng-content>\n </div>\n <div class=\"body\" [class.full-width]=\"isFullWidth\" #body>\n <ng-content></ng-content>\n </div>\n</div>\n\n<div class=\"qd-dialog-actions\">\n <button\n *ngIf=\"config.cancel\"\n qdButton\n qdButtonGhost\n color=\"secondary\"\n (click)=\"close()\"\n data-test-id=\"dialog-cancel-button\"\n >\n {{ config?.cancel?.label?.i18n ?? \"i18n.qd.dialog.action.cancel\" | translate }}\n </button>\n <button *ngIf=\"config.primary\" qdButton (click)=\"primaryActionClicked()\" data-test-id=\"dialog-primary-button\">\n {{ config?.primary?.label?.i18n ?? \"i18n.qd.dialog.action.primary\" | translate }}\n </button>\n <ng-content select=\"qd-dialog-action\"></ng-content>\n</div>\n", styles: ["qd-dialog{display:flex;width:100%;height:100%;flex-direction:column;background:#efefef}qd-dialog.full-width{position:relative;width:calc(100% - 3rem)!important;height:calc(100% - 3rem)!important;margin:1.5rem}qd-dialog.full-width .close{position:absolute;z-index:9999;top:1.25rem;right:1rem;color:#454545;cursor:pointer}.dialog-header{display:flex;height:3.125rem;flex-direction:row;align-items:center;justify-content:space-between;padding:.5rem 1.5rem;border-bottom:2px solid rgb(213,213,213);background:#fff}.dialog-header .title-container{display:block;color:#171717;font-weight:500}.dialog-header .close{cursor:pointer}.wrapper-body{overflow:auto;flex:1;background:#efefef}.wrapper-body .has-dialog-info-banners{padding:1rem 1.25rem .5rem;border-bottom:rgb(213,213,213) solid .0625rem;background-color:#fff}.wrapper-body .has-dialog-info-banners qd-page-info-banner:last-child{margin-bottom:0}.wrapper-body .body.full-width{height:100%}.wrapper-body .body:not(.full-width){max-height:80vh;padding:1rem 1.5rem}.wrapper-body .body:not(.full-width) qd-section{padding-right:1.5rem;padding-left:1.5rem}.wrapper-body .body:not(.full-width) qd-text-section{margin-right:0;margin-left:0}qd-dialog.has-sections .wrapper-body .body:not(.full-width){padding:0}.qd-dialog-actions{display:flex;justify-content:flex-end;padding:.5rem 1.5rem;border-top:2px solid rgb(213,213,213);background:#efefef;column-gap:.625rem}\n"], dependencies: [{ 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: QdButtonGhostDirective, selector: "button[qdButtonGhost], a[qdButtonGhost]" }, { kind: "component", type: QdIconButtonComponent, selector: "button[qdIconButton], a[qdIconButton], button[qd-icon-button]", inputs: ["color", "data-test-id"] }, { kind: "component", type: QdIconComponent, selector: "qd-icon", inputs: ["icon", "status"] }, { kind: "pipe", type: i3.TranslatePipe, name: "translate" }], encapsulation: i0.ViewEncapsulation.None });
1553
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: QdDialogComponent, isStandalone: false, selector: "qd-dialog", host: { properties: { "class.full-width": "isFullWidth", "class.has-sections": "this.hasSectionsClass" } }, providers: [QdDialogComponent], viewQueries: [{ propertyName: "body", first: true, predicate: ["body"], descendants: true }, { propertyName: "infoBanners", first: true, predicate: ["banners"], descendants: true }], ngImport: i0, template: "<div *ngIf=\"!isFullWidth\" class=\"dialog-header\">\n <div class=\"title-container\">\n {{ config?.title?.i18n | translate }}\n </div>\n <button\n *ngIf=\"!config?.hideCloseHeaderButton\"\n qdIconButton\n class=\"close\"\n color=\"secondary\"\n tabindex=\"-1\"\n (click)=\"close()\"\n >\n <qd-icon icon=\"timesLargeLight\"></qd-icon>\n </button>\n</div>\n\n<button\n *ngIf=\"!config?.hideCloseHeaderButton && isFullWidth\"\n qdIconButton\n class=\"close\"\n color=\"secondary\"\n tabindex=\"-1\"\n (click)=\"close()\"\n>\n <qd-icon icon=\"timesLargeLight\"></qd-icon>\n</button>\n\n<div class=\"wrapper-body\">\n <div [class.has-dialog-info-banners]=\"hasInfoBanner\" #banners>\n <ng-content select=\"qd-page-info-banner\"></ng-content>\n </div>\n <div class=\"body\" [class.full-width]=\"isFullWidth\" #body>\n <ng-content></ng-content>\n </div>\n</div>\n\n<div class=\"qd-dialog-actions\">\n <button *ngIf=\"config.cancel\" qdButton qdButtonGhost color=\"secondary\" (click)=\"close()\">\n {{ config?.cancel?.label?.i18n ?? \"i18n.qd.dialog.action.cancel\" | translate }}\n </button>\n <button *ngIf=\"config.primary\" qdButton (click)=\"primaryActionClicked()\">\n {{ config?.primary?.label?.i18n ?? \"i18n.qd.dialog.action.primary\" | translate }}\n </button>\n <ng-content select=\"qd-dialog-action\"></ng-content>\n</div>\n", styles: ["qd-dialog{display:flex;width:100%;height:100%;flex-direction:column;background:#efefef}qd-dialog.full-width{position:relative;width:calc(100% - 3rem)!important;height:calc(100% - 3rem)!important;margin:1.5rem}qd-dialog.full-width .close{position:absolute;z-index:9999;top:1.25rem;right:1rem;color:#454545;cursor:pointer}.dialog-header{display:flex;height:3.125rem;flex-direction:row;align-items:center;justify-content:space-between;padding:.5rem 1.5rem;border-bottom:2px solid rgb(213,213,213);background:#fff}.dialog-header .title-container{display:block;color:#171717;font-weight:500}.dialog-header .close{cursor:pointer}.wrapper-body{overflow:auto;flex:1;background:#efefef}.wrapper-body .has-dialog-info-banners{padding:1rem 1.25rem .5rem;border-bottom:rgb(213,213,213) solid .0625rem;background-color:#fff}.wrapper-body .has-dialog-info-banners qd-page-info-banner:last-child{margin-bottom:0}.wrapper-body .body.full-width{height:100%}.wrapper-body .body:not(.full-width){max-height:80vh;padding:1rem 1.5rem}.wrapper-body .body:not(.full-width) qd-section{padding-right:1.5rem;padding-left:1.5rem}.wrapper-body .body:not(.full-width) qd-text-section{margin-right:0;margin-left:0}qd-dialog.has-sections .wrapper-body .body:not(.full-width){padding:0}.qd-dialog-actions{display:flex;justify-content:flex-end;padding:.5rem 1.5rem;border-top:2px solid rgb(213,213,213);background:#efefef;column-gap:.625rem}\n"], dependencies: [{ 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: QdButtonGhostDirective, selector: "button[qdButtonGhost], a[qdButtonGhost]" }, { kind: "component", type: QdIconButtonComponent, selector: "button[qdIconButton], a[qdIconButton], button[qd-icon-button]", inputs: ["color", "data-test-id"] }, { kind: "component", type: QdIconComponent, selector: "qd-icon", inputs: ["icon", "status"] }, { kind: "pipe", type: i3.TranslatePipe, name: "translate" }], encapsulation: i0.ViewEncapsulation.None });
1554
1554
  }
1555
1555
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdDialogComponent, decorators: [{
1556
1556
  type: Component,
1557
- args: [{ selector: 'qd-dialog', host: { '[class.full-width]': 'isFullWidth' }, encapsulation: ViewEncapsulation.None, providers: [QdDialogComponent], standalone: false, template: "<div *ngIf=\"!isFullWidth\" class=\"dialog-header\">\n <div class=\"title-container\">\n {{ config?.title?.i18n | translate }}\n </div>\n <button\n *ngIf=\"!config?.hideCloseHeaderButton\"\n qdIconButton\n class=\"close\"\n color=\"secondary\"\n tabindex=\"-1\"\n (click)=\"close()\"\n data-test-id=\"dialog-close-button\"\n >\n <qd-icon icon=\"timesLargeLight\"></qd-icon>\n </button>\n</div>\n\n<button\n *ngIf=\"!config?.hideCloseHeaderButton && isFullWidth\"\n qdIconButton\n class=\"close\"\n color=\"secondary\"\n tabindex=\"-1\"\n (click)=\"close()\"\n data-test-id=\"dialog-close-button\"\n>\n <qd-icon icon=\"timesLargeLight\"></qd-icon>\n</button>\n\n<div class=\"wrapper-body\">\n <div [class.has-dialog-info-banners]=\"hasInfoBanner\" #banners>\n <ng-content select=\"qd-page-info-banner\"></ng-content>\n </div>\n <div class=\"body\" [class.full-width]=\"isFullWidth\" #body>\n <ng-content></ng-content>\n </div>\n</div>\n\n<div class=\"qd-dialog-actions\">\n <button\n *ngIf=\"config.cancel\"\n qdButton\n qdButtonGhost\n color=\"secondary\"\n (click)=\"close()\"\n data-test-id=\"dialog-cancel-button\"\n >\n {{ config?.cancel?.label?.i18n ?? \"i18n.qd.dialog.action.cancel\" | translate }}\n </button>\n <button *ngIf=\"config.primary\" qdButton (click)=\"primaryActionClicked()\" data-test-id=\"dialog-primary-button\">\n {{ config?.primary?.label?.i18n ?? \"i18n.qd.dialog.action.primary\" | translate }}\n </button>\n <ng-content select=\"qd-dialog-action\"></ng-content>\n</div>\n", styles: ["qd-dialog{display:flex;width:100%;height:100%;flex-direction:column;background:#efefef}qd-dialog.full-width{position:relative;width:calc(100% - 3rem)!important;height:calc(100% - 3rem)!important;margin:1.5rem}qd-dialog.full-width .close{position:absolute;z-index:9999;top:1.25rem;right:1rem;color:#454545;cursor:pointer}.dialog-header{display:flex;height:3.125rem;flex-direction:row;align-items:center;justify-content:space-between;padding:.5rem 1.5rem;border-bottom:2px solid rgb(213,213,213);background:#fff}.dialog-header .title-container{display:block;color:#171717;font-weight:500}.dialog-header .close{cursor:pointer}.wrapper-body{overflow:auto;flex:1;background:#efefef}.wrapper-body .has-dialog-info-banners{padding:1rem 1.25rem .5rem;border-bottom:rgb(213,213,213) solid .0625rem;background-color:#fff}.wrapper-body .has-dialog-info-banners qd-page-info-banner:last-child{margin-bottom:0}.wrapper-body .body.full-width{height:100%}.wrapper-body .body:not(.full-width){max-height:80vh;padding:1rem 1.5rem}.wrapper-body .body:not(.full-width) qd-section{padding-right:1.5rem;padding-left:1.5rem}.wrapper-body .body:not(.full-width) qd-text-section{margin-right:0;margin-left:0}qd-dialog.has-sections .wrapper-body .body:not(.full-width){padding:0}.qd-dialog-actions{display:flex;justify-content:flex-end;padding:.5rem 1.5rem;border-top:2px solid rgb(213,213,213);background:#efefef;column-gap:.625rem}\n"] }]
1557
+ args: [{ selector: 'qd-dialog', host: { '[class.full-width]': 'isFullWidth' }, encapsulation: ViewEncapsulation.None, providers: [QdDialogComponent], standalone: false, template: "<div *ngIf=\"!isFullWidth\" class=\"dialog-header\">\n <div class=\"title-container\">\n {{ config?.title?.i18n | translate }}\n </div>\n <button\n *ngIf=\"!config?.hideCloseHeaderButton\"\n qdIconButton\n class=\"close\"\n color=\"secondary\"\n tabindex=\"-1\"\n (click)=\"close()\"\n >\n <qd-icon icon=\"timesLargeLight\"></qd-icon>\n </button>\n</div>\n\n<button\n *ngIf=\"!config?.hideCloseHeaderButton && isFullWidth\"\n qdIconButton\n class=\"close\"\n color=\"secondary\"\n tabindex=\"-1\"\n (click)=\"close()\"\n>\n <qd-icon icon=\"timesLargeLight\"></qd-icon>\n</button>\n\n<div class=\"wrapper-body\">\n <div [class.has-dialog-info-banners]=\"hasInfoBanner\" #banners>\n <ng-content select=\"qd-page-info-banner\"></ng-content>\n </div>\n <div class=\"body\" [class.full-width]=\"isFullWidth\" #body>\n <ng-content></ng-content>\n </div>\n</div>\n\n<div class=\"qd-dialog-actions\">\n <button *ngIf=\"config.cancel\" qdButton qdButtonGhost color=\"secondary\" (click)=\"close()\">\n {{ config?.cancel?.label?.i18n ?? \"i18n.qd.dialog.action.cancel\" | translate }}\n </button>\n <button *ngIf=\"config.primary\" qdButton (click)=\"primaryActionClicked()\">\n {{ config?.primary?.label?.i18n ?? \"i18n.qd.dialog.action.primary\" | translate }}\n </button>\n <ng-content select=\"qd-dialog-action\"></ng-content>\n</div>\n", styles: ["qd-dialog{display:flex;width:100%;height:100%;flex-direction:column;background:#efefef}qd-dialog.full-width{position:relative;width:calc(100% - 3rem)!important;height:calc(100% - 3rem)!important;margin:1.5rem}qd-dialog.full-width .close{position:absolute;z-index:9999;top:1.25rem;right:1rem;color:#454545;cursor:pointer}.dialog-header{display:flex;height:3.125rem;flex-direction:row;align-items:center;justify-content:space-between;padding:.5rem 1.5rem;border-bottom:2px solid rgb(213,213,213);background:#fff}.dialog-header .title-container{display:block;color:#171717;font-weight:500}.dialog-header .close{cursor:pointer}.wrapper-body{overflow:auto;flex:1;background:#efefef}.wrapper-body .has-dialog-info-banners{padding:1rem 1.25rem .5rem;border-bottom:rgb(213,213,213) solid .0625rem;background-color:#fff}.wrapper-body .has-dialog-info-banners qd-page-info-banner:last-child{margin-bottom:0}.wrapper-body .body.full-width{height:100%}.wrapper-body .body:not(.full-width){max-height:80vh;padding:1rem 1.5rem}.wrapper-body .body:not(.full-width) qd-section{padding-right:1.5rem;padding-left:1.5rem}.wrapper-body .body:not(.full-width) qd-text-section{margin-right:0;margin-left:0}qd-dialog.has-sections .wrapper-body .body:not(.full-width){padding:0}.qd-dialog-actions{display:flex;justify-content:flex-end;padding:.5rem 1.5rem;border-top:2px solid rgb(213,213,213);background:#efefef;column-gap:.625rem}\n"] }]
1558
1558
  }], propDecorators: { body: [{
1559
1559
  type: ViewChild,
1560
1560
  args: ['body']
@@ -28995,8 +28995,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
28995
28995
 
28996
28996
  class QdPageCommitActionExecutor {
28997
28997
  static execute(action, values, context) {
28998
- const { formGroupManager, navigationInterceptor, destroyed$, onAfterSnapshot, scheduleFrameworkRebuildStop } = context;
28999
- const captured = formGroupManager.createCurrentValuesSnapshot();
28998
+ const { formGroupManager, navigationInterceptor, destroyed$, onAfterSnapshot } = context;
28999
+ const captured = formGroupManager.captureFormValues();
29000
29000
  let result$;
29001
29001
  navigationInterceptor.executeWithBypass(() => {
29002
29002
  const handlerResult = action.handler(values);
@@ -29004,9 +29004,7 @@ class QdPageCommitActionExecutor {
29004
29004
  result$ = handlerResult;
29005
29005
  });
29006
29006
  const applySuccess = () => {
29007
- formGroupManager.updateSavedSnapshotFromValues(captured);
29008
- formGroupManager.startAdoptingFrameworkRebuild();
29009
- scheduleFrameworkRebuildStop?.();
29007
+ formGroupManager.setFormGroupsSnapshot(captured);
29010
29008
  navigationInterceptor.executeWithBypass(() => {
29011
29009
  try {
29012
29010
  onAfterSnapshot?.();
@@ -29050,170 +29048,136 @@ class QdPageCommitActionExecutor {
29050
29048
  }
29051
29049
 
29052
29050
  /**
29053
- * Central tracker for all reactive forms on a single QdPage.
29054
- *
29055
- * Its main job is to answer one question: **has the user changed anything since the last save?**
29056
- * The page uses that answer to warn before leaving with unsaved changes.
29057
- *
29058
- * #### What it does
29059
- * - **Registry** holds every form of the page and gives access to it.
29060
- * - **Change detection** — keeps a snapshot of the saved values and compares the live form against it.
29061
- * - **Restore** resets the forms back to the snapshot when the user discards changes.
29062
- * - **Validity** reports whether all forms are valid and can cancel in-flight async validators.
29063
- *
29064
- * #### Adopting framework rebuilds
29065
- * After a save, the consumer often rebuilds the form itself — a `FormArray` cleared and pushed
29066
- * again, or a control replaced via `setControl`. A plain value compare would wrongly read that as a
29067
- * user edit. To prevent that:
29068
- *
29069
- * - **What it does** right after each save or snapshot write, the framework briefly *adopts its
29070
- * own rebuild* (`startAdoptingFrameworkRebuild`) and folds those control replacements into the
29071
- * snapshot, so they count as already saved.
29072
- * - **Why it is needed** — otherwise a rebuilt value (text `"99"` becoming number `99`) would look
29073
- * like an unsaved change.
29074
- * - **When it ends** — the page stops adopting once the rebuild has rendered
29075
- * (`stopAdoptingFrameworkRebuild`).
29076
- * - **User edits stay unsaved** — they always happen after that window, so they are still reported.
29077
- *
29078
- * #### Notes
29079
- * - Snapshots store only **raw values** (`getRawValue()`), never control instances — cheap to keep and compare.
29080
- * - Restoring a `FormArray` adjusts its length and resets its contents; it never replaces the array instance.
29081
- * - No structural information is stored in the snapshot, only data.
29051
+ * The QdFormGroupManagerService provides centralized registration, snapshotting, and value tracking
29052
+ * for multiple Angular `FormGroup` instances. It supports efficient state restoration, including dynamic
29053
+ * resizing of embedded `FormArray` controls, while avoiding any structural cloning in snapshots.
29054
+ *
29055
+ * #### Purpose:
29056
+ * This service is useful when managing multiple reactive forms that must be:
29057
+ * - Observed for changes
29058
+ * - Checked for validity
29059
+ * - Compared against a previous state (snapshot)
29060
+ * - Reset to a previous state, including dynamic form arrays
29061
+ *
29062
+ * #### Value Snapshot
29063
+ * Snapshots store only **raw values** (via `getRawValue()`), not any control instances.
29064
+ * This allows for minimal memory usage and easy comparison using deep equality checks.
29065
+ *
29066
+ * #### Smart FormArray Reset
29067
+ * When restoring a `FormArray`, the service ensures that the number and structure of controls
29068
+ * matches the snapshot values. Controls are cloned dynamically from an existing prototype.
29069
+ * If a `FormArray` was emptied (e.g. via `removeAt()`), a minimal control structure is inferred
29070
+ * from the snapshot data.
29071
+ *
29072
+ * #### Example Usage:
29073
+ *
29074
+ * **Register a FormGroup**
29075
+ * ```ts
29076
+ * service.setFormGroup('userForm', formGroup);
29077
+ * ```
29078
+ *
29079
+ * **Take a Snapshot**
29080
+ * ```ts
29081
+ * service.takeFormGroupsSnapshot();
29082
+ * ```
29083
+ *
29084
+ * **Detect Changes**
29085
+ * ```ts
29086
+ * service.$hasValuesChanged().subscribe(changed => {
29087
+ * if (changed) {
29088
+ * console.log('Form data has changed');
29089
+ * }
29090
+ * });
29091
+ * ```
29092
+ *
29093
+ * **Restore from Snapshot**
29094
+ * ```ts
29095
+ * service.restoreFormGroupsFromSnapshot();
29096
+ * ```
29097
+ *
29098
+ * **Check if All Forms Are Valid**
29099
+ * ```ts
29100
+ * service.$areFormGroupsValid().subscribe(valid => {
29101
+ * if (valid) {
29102
+ * console.log('All forms valid!');
29103
+ * }
29104
+ * });
29105
+ * ```
29106
+ *
29107
+ * #### Notes:
29108
+ * - FormArrays are not replaced during restore. Their length is adjusted and their content reset.
29109
+ * - Control cloning is recursive and structure-preserving.
29110
+ * - No structural information is stored in the snapshot; only data.
29082
29111
  */
29083
29112
  class QdFormGroupManagerService {
29084
29113
  _formGroups = new Map();
29085
29114
  _formGroupsSnapshot = new Map();
29086
- _knownControlInstances = new Map();
29087
- _isAdoptingFrameworkRebuild = false;
29088
- _frameworkRebuildSub;
29089
29115
  _formGroupsChanged$ = new BehaviorSubject(undefined);
29090
- _keysInInitialBuildup = new Set();
29091
- /**
29092
- * Registers a form under a key so the manager can track it.
29093
- *
29094
- * On first registration it also seeds the saved snapshot from the current values, so a freshly
29095
- * registered form counts as unchanged until the user actually edits it.
29096
- */
29097
29116
  setFormGroup(key, formGroup) {
29098
29117
  this._formGroups.set(key, formGroup);
29099
- if (!this._formGroupsSnapshot.has(key)) {
29100
- this._formGroupsSnapshot.set(key, formGroup.getRawValue());
29101
- this.rememberControlInstances(key, formGroup);
29102
- this._keysInInitialBuildup.add(key);
29103
- }
29104
29118
  this._formGroupsChanged$.next();
29105
29119
  }
29106
- /**
29107
- * Returns the form registered under the given key, or `undefined` if none is registered.
29108
- */
29109
29120
  getFormGroup(key) {
29110
29121
  return this._formGroups.get(key);
29111
29122
  }
29112
- /**
29113
- * Returns all registered forms, keyed by their registration key.
29114
- */
29115
29123
  getAllFormGroups() {
29116
29124
  return this._formGroups;
29117
29125
  }
29118
- /**
29119
- * Returns `true` if at least one form is registered.
29120
- */
29126
+ tryRemoveFormGroup(key) {
29127
+ this._formGroups.delete(key);
29128
+ this._formGroupsChanged$.next();
29129
+ }
29121
29130
  hasFormGroups() {
29122
29131
  return this._formGroups.size > 0;
29123
29132
  }
29124
- /**
29125
- * Returns `true` if no form is registered under the given key yet.
29126
- */
29127
- isFormGroupKeyUnique(key) {
29128
- return !this._formGroups.has(key);
29129
- }
29130
- /**
29131
- * Returns the current `.value` of every registered form, keyed by registration key.
29132
- *
29133
- * Uses Angular's `.value`, so disabled controls are omitted. Use the snapshot methods when you
29134
- * need the raw values including disabled controls.
29135
- */
29136
29133
  getAllValues() {
29137
29134
  const allValues = {};
29138
29135
  this._formGroups.forEach((formGroup, key) => (allValues[key] = formGroup.value));
29139
29136
  return allValues;
29140
29137
  }
29141
- /**
29142
- * Unregisters the form under the given key and drops its instance tracking.
29143
- *
29144
- * Does nothing if no form is registered for the key.
29145
- */
29146
- tryRemoveFormGroup(key) {
29147
- this._formGroups.delete(key);
29148
- this._knownControlInstances.delete(key);
29149
- this._keysInInitialBuildup.delete(key);
29150
- this._formGroupsChanged$.next();
29138
+ isFormGroupKeyUnique(key) {
29139
+ return !this._formGroups.has(key);
29151
29140
  }
29152
- /**
29153
- * Ends the initial-buildup phase for a key and fixes its baseline from the form as it is now.
29154
- *
29155
- * During that phase the key never counts as changed, so a form filled right after registration
29156
- * (e.g. by an `effect`) is not flagged as unsaved. This stores the current values as the baseline,
29157
- * so later edits are measured against the filled form.
29158
- *
29159
- * Does nothing if the key is not in its initial-buildup phase.
29160
- */
29161
- endInitialBuildup(key) {
29162
- if (!this._keysInInitialBuildup.delete(key))
29163
- return;
29164
- const formGroup = this._formGroups.get(key);
29165
- if (formGroup) {
29166
- this._formGroupsSnapshot.set(key, formGroup.getRawValue());
29167
- this.rememberControlInstances(key, formGroup);
29168
- }
29169
- this._formGroupsChanged$.next();
29141
+ $areFormGroupsValid() {
29142
+ return this._formGroupsChanged$.pipe(observeOn(queueScheduler), switchMap$1(() => {
29143
+ if (!this.hasFormGroups())
29144
+ return of(false);
29145
+ const obs = Array.from(this._formGroups.values()).map(fg => fg.statusChanges.pipe(startWith(fg.status), map(() => this.areFormGroupsValid(fg))));
29146
+ return combineLatest(obs).pipe(map(valids => valids.every(Boolean)));
29147
+ }));
29170
29148
  }
29171
- /**
29172
- * Emits whether any registered form differs from its saved snapshot — i.e. whether the user has
29173
- * unsaved changes.
29174
- *
29175
- * Framework-driven rebuilds are folded into the snapshot first while the framework is adopting
29176
- * them (see the class docs), so they are not reported as changes.
29177
- */
29178
29149
  $hasValuesChanged() {
29179
- return this._formGroupsChanged$.pipe(observeOn(queueScheduler), switchMap$1(() => this.anyRegisteredFormChanged$()));
29150
+ return this._formGroupsChanged$.pipe(observeOn(queueScheduler), switchMap$1(() => {
29151
+ if (!this.hasFormGroups())
29152
+ return of(false);
29153
+ const obs = Array.from(this._formGroups.entries()).map(([key, fg]) => fg.valueChanges.pipe(startWith(fg.getRawValue()), map(() => {
29154
+ const currentValues = fg.getRawValue();
29155
+ const snapshot = this._formGroupsSnapshot.get(key);
29156
+ return snapshot ? !isEqual$1(currentValues, snapshot) : true;
29157
+ })));
29158
+ return combineLatest(obs).pipe(map(changes => changes.some(Boolean)));
29159
+ }));
29180
29160
  }
29181
- /**
29182
- * Overwrites the saved snapshot with the **current** form values — for a **synchronous** save
29183
- * that is already finished.
29184
- *
29185
- * - Stores each form's raw values (`getRawValue`) as the new saved snapshot.
29186
- * - Takes them **directly, without a deep copy** — synchronous means no edits can slip in
29187
- * between, so the cheap path is safe (only the async path needs the copy).
29188
- * - Afterwards the current state counts as saved; later edits are measured against it.
29189
- *
29190
- * For an **asynchronous** save, use `createCurrentValuesSnapshot()` + `updateSavedSnapshotFromValues()`.
29191
- */
29192
- updateSavedSnapshotFromCurrentValues() {
29193
- this._formGroups.forEach((fg, key) => {
29194
- this._formGroupsSnapshot.set(key, fg.getRawValue());
29195
- this.rememberControlInstances(key, fg);
29196
- });
29161
+ takeFormGroupsSnapshot() {
29162
+ this._formGroups.forEach((fg, key) => this._formGroupsSnapshot.set(key, fg.getRawValue()));
29197
29163
  }
29198
29164
  /**
29199
- * Creates an independent snapshot of the **current** form values and returns it — for the
29200
- * **click moment** of an **asynchronous** save.
29165
+ * Captures a deep-cloned snapshot of all current form values without mutating internal state.
29201
29166
  *
29202
- * - Deep-copies each form's raw values (`structuredClone`), detached from the live form.
29203
- * - Writes **nothing** internally — the saved snapshot stays unchanged.
29204
- * - Freezes the click-time state: later edits (while the backend runs) do not change the copy.
29205
- * - Hand the result to `updateSavedSnapshotFromValues()` once the save succeeds.
29167
+ * Used by framework orchestration code that needs to remember the "at click time" state of a form
29168
+ * so the snapshot can later be set to exactly those values even if the user edits the form
29169
+ * while an async commit action is pending.
29206
29170
  *
29207
29171
  * Throws if any form value is not structured-cloneable (functions, symbols, DOM nodes).
29208
29172
  */
29209
- createCurrentValuesSnapshot() {
29173
+ captureFormValues() {
29210
29174
  const captured = new Map();
29211
29175
  this._formGroups.forEach((fg, key) => {
29212
29176
  try {
29213
29177
  captured.set(key, structuredClone(fg.getRawValue()));
29214
29178
  }
29215
29179
  catch (err) {
29216
- throw new Error(`Quadrel Framework | QdFormGroupManager - createCurrentValuesSnapshot() failed for group "${key}". ` +
29180
+ throw new Error(`Quadrel Framework | QdFormGroupManager - captureFormValues() failed for group "${key}". ` +
29217
29181
  `Form values must be structured-cloneable. Non-cloneable values like functions, ` +
29218
29182
  `symbols, or DOM nodes are not supported. Original error: ${String(err)}`);
29219
29183
  }
@@ -29221,27 +29185,15 @@ class QdFormGroupManagerService {
29221
29185
  return captured;
29222
29186
  }
29223
29187
  /**
29224
- * Overwrites the saved snapshot with the **given** snapshot — on **success** of an
29225
- * **asynchronous** save, using the snapshot taken earlier by `createCurrentValuesSnapshot()`.
29226
- *
29227
- * - Stores the passed snapshot as the new saved state.
29228
- * - Uses the click-time snapshot, **not** the current form — so edits the user made while the
29229
- * backend was running stay correctly marked as unsaved.
29230
- */
29231
- updateSavedSnapshotFromValues(snapshot) {
29232
- snapshot.forEach((values, key) => {
29233
- this._formGroupsSnapshot.set(key, values);
29234
- const formGroup = this._formGroups.get(key);
29235
- if (formGroup)
29236
- this.rememberControlInstances(key, formGroup);
29237
- });
29238
- }
29239
- /**
29240
- * Resets every registered form back to its saved snapshot — used when the user discards changes.
29188
+ * Replaces the internal saved snapshot with the provided values.
29241
29189
  *
29242
- * FormArrays are resized to match the snapshot length; pending async validators are cancelled
29243
- * afterwards so no stale validation result survives the restore.
29190
+ * Intended to be paired with `captureFormValues()` so that the snapshot can be set to the
29191
+ * values present when a commit action was initiated — independent of whether the user edited
29192
+ * the form in the meantime.
29244
29193
  */
29194
+ setFormGroupsSnapshot(values) {
29195
+ values.forEach((snapshot, key) => this._formGroupsSnapshot.set(key, snapshot));
29196
+ }
29245
29197
  restoreFormGroupsFromSnapshot() {
29246
29198
  this._formGroups.forEach((fg, key) => {
29247
29199
  const snapshot = this._formGroupsSnapshot.get(key);
@@ -29252,171 +29204,68 @@ class QdFormGroupManagerService {
29252
29204
  this.cancelPendingAsyncValidation();
29253
29205
  }
29254
29206
  /**
29255
- * Starts adopting framework rebuilds into the snapshot. While active, a framework-driven control
29256
- * replacement (a `setControl` on a leaf or a whole `FormArray` rebuilt via clear/push) is folded
29257
- * into the saved snapshot, so it is not reported as an unsaved change.
29207
+ * Cancels any in-flight async validators on all registered form groups.
29258
29208
  *
29259
- * User edits never fall in this short window, so they stay dirty. The framework starts it after a
29260
- * successful commit and after every snapshot write; the page stops it deterministically once the
29261
- * rebuild has rendered.
29262
- */
29263
- startAdoptingFrameworkRebuild() {
29264
- this._isAdoptingFrameworkRebuild = true;
29265
- this._frameworkRebuildSub?.unsubscribe();
29266
- const valueStreams = Array.from(this._formGroups.values()).map(fg => fg.valueChanges);
29267
- if (valueStreams.length)
29268
- this._frameworkRebuildSub = merge(...valueStreams).subscribe(() => this.adoptFrameworkRebuild());
29269
- this.adoptFrameworkRebuild();
29270
- }
29271
- /**
29272
- * Stops adopting framework rebuilds. After this, control replacements count as user edits again.
29273
- *
29274
- * The page calls this once the framework rebuild has rendered (via `afterNextRender`).
29275
- */
29276
- stopAdoptingFrameworkRebuild() {
29277
- this._isAdoptingFrameworkRebuild = false;
29278
- this._frameworkRebuildSub?.unsubscribe();
29279
- }
29280
- /**
29281
- * Emits whether every registered form is currently valid.
29282
- *
29283
- * A disabled control without validators is treated as valid.
29284
- */
29285
- $areFormGroupsValid() {
29286
- return this._formGroupsChanged$.pipe(observeOn(queueScheduler), switchMap$1(() => {
29287
- if (!this.hasFormGroups())
29288
- return of(false);
29289
- const obs = Array.from(this._formGroups.values()).map(fg => fg.statusChanges.pipe(startWith(fg.status), map(() => this.areFormGroupsValid(fg))));
29290
- return combineLatest(obs).pipe(map(valids => valids.every(Boolean)));
29291
- }));
29292
- }
29293
- /**
29294
- * Cancels every in-flight (PENDING) async validator across all registered forms, so no stale
29295
- * validation result can land after a restore or a commit.
29296
- *
29297
- * The async validators themselves are kept — only the pending run is dropped. Each phase runs
29298
- * over **all** pending controls before the next starts (detach → re-evaluate sync-only, no
29299
- * cascade → re-attach), so validity is never recomputed while a run is still settling.
29209
+ * 1. Collect all PENDING controls and their async validators
29210
+ * 2. Clear all async validators, then update validity (sync-only, no cascade)
29211
+ * 3. Re-attach async validators for future use
29300
29212
  */
29301
29213
  cancelPendingAsyncValidation() {
29302
29214
  this._formGroups.forEach(fg => {
29303
29215
  const pendingControls = this.collectPendingControls(fg);
29304
29216
  if (pendingControls.length === 0)
29305
29217
  return;
29306
- pendingControls.forEach(({ control }) => control.clearAsyncValidators());
29307
- pendingControls.forEach(({ control }) => control.updateValueAndValidity({ onlySelf: true }));
29308
- pendingControls.forEach(({ control, asyncValidator }) => control.setAsyncValidators(asyncValidator));
29309
- });
29310
- }
29311
- anyRegisteredFormChanged$() {
29312
- if (!this.hasFormGroups())
29313
- return of(false);
29314
- const perForm = Array.from(this._formGroups.entries()).map(([key, fg]) => fg.valueChanges.pipe(startWith(null), map(() => this.hasFormChanged(key, fg))));
29315
- return combineLatest(perForm).pipe(map(flags => flags.some(Boolean)));
29316
- }
29317
- hasFormChanged(key, fg) {
29318
- if (this._keysInInitialBuildup.has(key))
29319
- return false;
29320
- const snapshot = this._formGroupsSnapshot.get(key);
29321
- if (snapshot)
29322
- this.adoptRebuildInGroup(key, fg, snapshot);
29323
- return snapshot ? !isEqual$1(fg.getRawValue(), snapshot) : true;
29324
- }
29325
- adoptFrameworkRebuild() {
29326
- this._formGroups.forEach((fg, key) => {
29327
- const snapshot = this._formGroupsSnapshot.get(key);
29328
- if (snapshot)
29329
- this.adoptRebuildInGroup(key, fg, snapshot);
29218
+ for (const { control } of pendingControls) {
29219
+ control.clearAsyncValidators();
29220
+ }
29221
+ for (const { control } of pendingControls) {
29222
+ control.updateValueAndValidity({ onlySelf: true });
29223
+ }
29224
+ for (const { control, asyncValidator } of pendingControls) {
29225
+ control.setAsyncValidators(asyncValidator);
29226
+ }
29330
29227
  });
29331
29228
  }
29332
- adoptRebuildInGroup(key, formGroup, snapshot) {
29333
- const tracked = this._knownControlInstances.get(key) ?? new Map();
29334
- this.adoptRebuiltControls(formGroup, snapshot, tracked, '');
29335
- this._knownControlInstances.set(key, tracked);
29336
- }
29337
- adoptRebuiltControls(group, snapshot, tracked, prefix) {
29338
- Object.entries(group.controls).forEach(([name, control]) => {
29339
- const path = prefix ? `${prefix}.${name}` : name;
29340
- const isNewKey = !(name in snapshot);
29341
- if (control instanceof FormArray)
29342
- this.adoptRebuiltList(control, snapshot, name, isNewKey);
29343
- else if (control instanceof FormGroup)
29344
- this.adoptRebuiltGroup(control, snapshot, name, isNewKey, tracked, path);
29345
- else
29346
- this.adoptRebuiltField(control, snapshot, name, isNewKey, tracked.get(path));
29347
- tracked.set(path, control);
29229
+ restoreFormGroup(fg, snapshot) {
29230
+ Object.entries(fg.controls).forEach(([ctrlKey, ctrl]) => {
29231
+ const newValue = snapshot[ctrlKey];
29232
+ if (ctrl instanceof FormArray && Array.isArray(newValue)) {
29233
+ this.resetFormArrayToValues(ctrl, newValue);
29234
+ }
29235
+ else if (ctrl instanceof FormGroup && newValue && typeof newValue === 'object') {
29236
+ this.restoreFormGroup(ctrl, newValue);
29237
+ }
29238
+ else {
29239
+ ctrl.reset(newValue);
29240
+ }
29348
29241
  });
29349
29242
  }
29350
- adoptRebuiltList(control, snapshot, name, isNewKey) {
29351
- if (isNewKey) {
29352
- this.adoptNewPristineControl(control, snapshot, name);
29353
- return;
29243
+ collectPendingControls(control) {
29244
+ const result = [];
29245
+ if (control instanceof FormGroup) {
29246
+ Object.values(control.controls).forEach(c => result.push(...this.collectPendingControls(c)));
29354
29247
  }
29355
- if (!this.isAdoptableArrayRebuild(control, snapshot, name))
29356
- return;
29357
- const snapshotArray = snapshot[name];
29358
- const currentArray = control.getRawValue();
29359
- if (currentArray.length === snapshotArray.length && !isEqual$1(currentArray, snapshotArray))
29360
- snapshot[name] = currentArray;
29361
- }
29362
- adoptRebuiltGroup(group, snapshot, name, isNewKey, tracked, path) {
29363
- if (isNewKey)
29364
- this.adoptNewPristineControl(group, snapshot, name);
29365
- const nested = snapshot[name];
29366
- if (this.isPlainObject(nested))
29367
- this.adoptRebuiltControls(group, nested, tracked, path);
29368
- }
29369
- adoptRebuiltField(control, snapshot, name, isNewKey, known) {
29370
- if (isNewKey)
29371
- this.adoptNewPristineControl(control, snapshot, name);
29372
- else if (this.isReplacedPristineControl(control, known))
29373
- snapshot[name] = control.getRawValue();
29374
- }
29375
- adoptNewPristineControl(control, snapshot, name) {
29376
- if (control.pristine)
29377
- snapshot[name] = control.getRawValue();
29378
- }
29379
- isAdoptableArrayRebuild(control, snapshot, name) {
29380
- return this._isAdoptingFrameworkRebuild && control.pristine && Array.isArray(snapshot[name]);
29381
- }
29382
- isReplacedPristineControl(control, known) {
29383
- return this._isAdoptingFrameworkRebuild && !!known && known !== control && control.pristine;
29384
- }
29385
- rememberControlInstances(key, formGroup) {
29386
- const tracked = new Map();
29387
- this.collectControlInstances(formGroup, tracked, '');
29388
- this._knownControlInstances.set(key, tracked);
29389
- }
29390
- collectControlInstances(group, tracked, prefix) {
29391
- Object.entries(group.controls).forEach(([name, control]) => {
29392
- const path = prefix ? `${prefix}.${name}` : name;
29393
- tracked.set(path, control);
29394
- if (control instanceof FormGroup)
29395
- this.collectControlInstances(control, tracked, path);
29396
- });
29397
- }
29398
- restoreFormGroup(fg, snapshot) {
29399
- Object.entries(fg.controls).forEach(([ctrlKey, ctrl]) => this.restoreControl(ctrl, snapshot[ctrlKey]));
29400
- }
29401
- restoreControl(ctrl, newValue) {
29402
- if (ctrl instanceof FormArray && Array.isArray(newValue))
29403
- this.resetFormArrayToValues(ctrl, newValue);
29404
- else if (ctrl instanceof FormGroup && this.isPlainObject(newValue))
29405
- this.restoreFormGroup(ctrl, newValue);
29406
- else
29407
- ctrl.reset(newValue);
29408
- }
29409
- isPlainObject(value) {
29410
- return !!value && typeof value === 'object';
29248
+ else if (control instanceof FormArray) {
29249
+ control.controls.forEach(c => result.push(...this.collectPendingControls(c)));
29250
+ }
29251
+ if (control.status === 'PENDING') {
29252
+ result.push({ control, asyncValidator: control.asyncValidator });
29253
+ }
29254
+ return result;
29411
29255
  }
29412
29256
  resetFormArrayToValues(array, newValues) {
29413
29257
  if (!Array.isArray(newValues))
29414
29258
  return;
29415
- if (array.length === 0 && newValues.length === 0)
29259
+ const oldLen = array.length;
29260
+ const newLen = newValues.length;
29261
+ if (oldLen === 0 && newLen === 0)
29416
29262
  return;
29417
- if (array.length === 0)
29263
+ if (oldLen === 0 && newLen > 0)
29418
29264
  return this.initFormArrayFromValues(array, newValues);
29419
- this.resizeFormArray(array, newValues.length);
29265
+ while (array.length < newLen)
29266
+ array.push(this.deepCloneControl(array.at(0)));
29267
+ while (array.length > newLen)
29268
+ array.removeAt(array.length - 1);
29420
29269
  array.reset(newValues);
29421
29270
  }
29422
29271
  initFormArrayFromValues(array, values) {
@@ -29432,12 +29281,6 @@ class QdFormGroupManagerService {
29432
29281
  }
29433
29282
  array.reset(values);
29434
29283
  }
29435
- resizeFormArray(array, length) {
29436
- while (array.length < length)
29437
- array.push(this.deepCloneControl(array.at(0)));
29438
- while (array.length > length)
29439
- array.removeAt(array.length - 1);
29440
- }
29441
29284
  deepCloneControl(control) {
29442
29285
  if (control instanceof FormControl)
29443
29286
  return new FormControl(undefined, control.validator, control.asyncValidator);
@@ -29455,19 +29298,6 @@ class QdFormGroupManagerService {
29455
29298
  areFormGroupsValid(fg) {
29456
29299
  return Object.values(fg.controls).every(c => (c.disabled && !c.validator && !c.asyncValidator ? true : c.valid));
29457
29300
  }
29458
- collectPendingControls(control) {
29459
- const result = this.childControlsOf(control).flatMap(child => this.collectPendingControls(child));
29460
- if (control.status === 'PENDING')
29461
- result.push({ control, asyncValidator: control.asyncValidator });
29462
- return result;
29463
- }
29464
- childControlsOf(control) {
29465
- if (control instanceof FormGroup)
29466
- return Object.values(control.controls);
29467
- if (control instanceof FormArray)
29468
- return control.controls;
29469
- return [];
29470
- }
29471
29301
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdFormGroupManagerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
29472
29302
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdFormGroupManagerService });
29473
29303
  }
@@ -29496,7 +29326,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
29496
29326
  * - The page switches to view mode via `deactivate()`.
29497
29327
  *
29498
29328
  * The service has no concept of "pending actions" or bypass windows. Framework actions update the
29499
- * saved snapshot directly via `QdFormGroupManagerService.updateSavedSnapshotFromValues(...)`; cancel-discard
29329
+ * saved snapshot directly via `QdFormGroupManagerService.setFormGroupsSnapshot(...)`; cancel-discard
29500
29330
  * flows reset the form to the saved snapshot via `restoreFormGroupsFromSnapshot()`. Either way, the
29501
29331
  * interceptor only observes the resulting form state — there is no time window where a bypass is active.
29502
29332
  */
@@ -29646,7 +29476,6 @@ class QdPageObjectHeaderComponent {
29646
29476
  confirmationDialogService = inject(QdConfirmationDialogOpenerService);
29647
29477
  contextService = inject(QdContextService);
29648
29478
  resolverTriggerService = inject(QdResolverTriggerService);
29649
- injector = inject(Injector);
29650
29479
  pageStoreService = inject(QdPageStoreService);
29651
29480
  config;
29652
29481
  hasNavigation = false;
@@ -29767,7 +29596,7 @@ class QdPageObjectHeaderComponent {
29767
29596
  this.setupResolverTrigger();
29768
29597
  this.updateCustomActions();
29769
29598
  this.subscribeToMetadataStream();
29770
- this.formGroupManagerService.updateSavedSnapshotFromCurrentValues();
29599
+ this.formGroupManagerService.takeFormGroupsSnapshot();
29771
29600
  this.initContexts();
29772
29601
  }
29773
29602
  ngOnChanges(changes) {
@@ -29792,7 +29621,7 @@ class QdPageObjectHeaderComponent {
29792
29621
  return this.config.headerFacets != null;
29793
29622
  }
29794
29623
  edit() {
29795
- this.formGroupManagerService.updateSavedSnapshotFromCurrentValues();
29624
+ this.formGroupManagerService.takeFormGroupsSnapshot();
29796
29625
  this.pageStoreService.toggleViewonly(false);
29797
29626
  if (this.editButton?.handler)
29798
29627
  this.editButton.handler();
@@ -29822,8 +29651,7 @@ class QdPageObjectHeaderComponent {
29822
29651
  onAfterSnapshot: () => {
29823
29652
  this.formGroupManagerService.cancelPendingAsyncValidation();
29824
29653
  this.pageStoreService.toggleViewonly(true);
29825
- },
29826
- scheduleFrameworkRebuildStop: () => afterNextRender(() => this.formGroupManagerService.stopAdoptingFrameworkRebuild(), { injector: this.injector })
29654
+ }
29827
29655
  });
29828
29656
  }
29829
29657
  changeContext(context, selection, event) {
@@ -29851,11 +29679,13 @@ class QdPageObjectHeaderComponent {
29851
29679
  .pipe(takeUntil(this._destroyed$), filter(shouldTrigger => shouldTrigger), tap(() => this._isLoadingSubject.next(true)), switchMap(() => this.pageObjectResolver.resolve()), tap(objectData => {
29852
29680
  this._pageObjectDataSubject.next({ ...objectData, ...this._pendingMetadata });
29853
29681
  this._pendingMetadata = {};
29854
- }), tap(() => this._isLoadingSubject.next(false)), tap(() => this.formGroupManagerService.updateSavedSnapshotFromCurrentValues()))
29682
+ }), tap(() => this._isLoadingSubject.next(false)), tap(() => this.formGroupManagerService.takeFormGroupsSnapshot()))
29855
29683
  .subscribe();
29856
29684
  }
29857
29685
  initContexts() {
29858
- this.contexts$ = this.contextService.contexts$.pipe(map(contexts => contexts.filter(context => context.selection || this.config.pageType === 'overview')), map(contexts => contexts.map(({ selection, context }) => ({
29686
+ this.contexts$ = this.contextService.contexts$.pipe(map(contexts => this.config.hideContexts
29687
+ ? []
29688
+ : contexts.filter(context => context.selection || this.config.pageType === 'overview')), map(contexts => contexts.map(({ selection, context }) => ({
29859
29689
  label: context.label.i18n,
29860
29690
  value: Array.isArray(selection?.value)
29861
29691
  ? selection?.value.filter(item => item !== null && item !== undefined)
@@ -29941,11 +29771,11 @@ class QdPageObjectHeaderComponent {
29941
29771
  });
29942
29772
  }
29943
29773
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPageObjectHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
29944
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: QdPageObjectHeaderComponent, isStandalone: false, selector: "qd-page-object-header", inputs: { config: "config", hasNavigation: "hasNavigation", testId: ["data-test-id", "testId"] }, host: { listeners: { "click": "toggleFacets()" }, properties: { "style.borderBottom": "borderBottomStyle", "class.in-dialog": "this.isInDialog" } }, usesOnChanges: true, ngImport: i0, template: "<div class=\"loading-overlay\" *ngIf=\"isLoading$ | async\"><qd-spinner></qd-spinner></div>\n\n<div class=\"object-header-top\">\n <header class=\"object-header-title\">{{ config.title.i18n | translate }}</header>\n <div class=\"object-header-toolbar\">\n <button class=\"button-icon\" data-test-id=\"archive-button\">\n <qd-icon\n *ngIf=\"showArchive$ | async\"\n (click)=\"archiveButton?.handler()\"\n class=\"header-action-icon\"\n [icon]=\"'resultNew'\"\n ></qd-icon>\n </button>\n\n <button class=\"button-icon\" data-test-id=\"delete-button\">\n <qd-icon\n class=\"header-action-icon\"\n *ngIf=\"showDelete$ | async\"\n (click)=\"deleteButton?.handler()\"\n [icon]=\"'trash'\"\n ></qd-icon>\n </button>\n\n <button *ngIf=\"showEdit$ | async\" qdButton (click)=\"edit()\" data-test-id=\"edit-button\">\n {{ editButton?.label?.i18n || \"i18n.qd.page.header.edit\" | translate }}\n </button>\n\n <button *ngIf=\"showCancel$ | async\" qdButton qdButtonGhost (click)=\"cancel()\" data-test-id=\"cancel-button\">\n {{ cancelButton.label?.i18n || \"i18n.qd.page.header.cancel\" | translate }}\n </button>\n\n <button\n *ngIf=\"showSave$ | async\"\n qdButton\n (click)=\"save()\"\n data-test-id=\"save-button\"\n [disabled]=\"($canSave | async) === false\"\n >\n {{ saveButton.label?.i18n || \"i18n.qd.page.header.save\" | translate }}\n </button>\n\n <ng-container *ngIf=\"customActions$ | async as customActions\">\n <ng-container *ngIf=\"!adaptiveMode; else adaptive\">\n <button\n [qdMenuButton]=\"customActions\"\n [autoSize]=\"false\"\n [data-test-id]=\"'page-'\"\n *ngIf=\"customActions.actions.length > 1; else singleButton\"\n data-test-id=\"custom-button\"\n >\n {{ config?.customActionsLabel?.i18n || \"i18n.qd.page.header.actions\" | translate }}\n </button>\n\n <ng-template #singleButton>\n <ng-container *ngIf=\"(customActions$ | async).actions[0] as singleAction\">\n <button\n qdButton\n *ngIf=\"singleAction.handler && !singleAction.isHidden\"\n [disabled]=\"singleAction.isDisabled\"\n (click)=\"singleAction.handler()\"\n data-test-id=\"custom-button\"\n >\n {{ singleAction.label?.i18n | translate }}\n </button>\n </ng-container>\n </ng-template>\n </ng-container>\n\n <ng-template #adaptive>\n <button\n *ngIf=\"(customActions$ | async)?.actions.length >= 1\"\n type=\"button\"\n class=\"adaptive-menu-button\"\n [qdPopoverOnClick]=\"menu\"\n [qdPopoverCloseStrategy]=\"'onEveryClick'\"\n [qdPopoverStopPropagation]=\"true\"\n [qdPopoverMinWidth]=\"160\"\n >\n <qd-icon icon=\"overflowMenuVertical\"></qd-icon>\n </button>\n\n <ng-template #menu>\n <button\n *ngFor=\"let action of customActions.actions; let i = index\"\n class=\"adaptive-menu-entry\"\n type=\"button\"\n [disabled]=\"action.isDisabled\"\n (click)=\"action.handler()\"\n >\n {{ action.label.i18n | translate }}\n </button>\n </ng-template>\n </ng-template>\n </ng-container>\n </div>\n</div>\n\n<div class=\"object-header-facets\" [ngClass]=\"{ amongThemselves: adaptiveMode && !facetsCollapsed }\">\n <ng-container *ngIf=\"pageObjectData$ | async as data\">\n <div\n class=\"object-header-facet\"\n *ngFor=\"let facet of headerFacets | slice : 0 : (adaptiveMode && facetsCollapsed ? 2 : headerFacets?.length ?? 0)\"\n [attr.data-test-id]=\"testId + '-facet-' + facet.name\"\n >\n <div class=\"facet-label\">{{ facet.label.i18n | translate }}</div>\n <qd-dynamic-facet class=\"dynamic-facet\" [facet]=\"facet\" [data]=\"data\"></qd-dynamic-facet>\n </div>\n </ng-container>\n\n <ng-container *ngIf=\"!config.hideContexts && contexts$ | async as contexts\">\n <div\n class=\"object-header-facet\"\n *ngFor=\"\n let context of contexts\n | slice : 0 : (adaptiveMode && facetsCollapsed ? 2 - (headerFacets?.length ?? 0) : contexts?.length);\n let index = index;\n let first = first\n \"\n [ngClass]=\"{ hasDivider: headerFacets && first, adaptiveFirst: headerFacets && adaptiveMode && first }\"\n >\n <div class=\"facet-label\">{{ context.label | translate }}</div>\n\n <span *ngIf=\"context.value.length === 0\" class=\"no-selected-context\">{{\n \"i18n.qd.page.context.noSelectionPlaceholder\" | translate\n }}</span>\n\n <span *ngIf=\"context.value.length !== 0\">\n <qd-chip state=\"none\" [close]=\"isContextSelectable\" (closeClickEmitter)=\"clearContext(context.context)\">\n <ng-container *ngFor=\"let val of context.value; let last = last\">\n {{ val.label.i18n | translate }}<span *ngIf=\"!last\">, </span>\n </ng-container>\n </qd-chip>\n </span>\n\n <span *ngIf=\"isContextSelectable && (!facetsCollapsed || !adaptiveMode)\" class=\"select-button\">\n <button\n qdButton\n qdButtonLink\n type=\"button\"\n (click)=\"changeContext(context.context, context.selection, $event)\"\n [data-test-id]=\"'object-header-select-context-' + context.context.id\"\n >\n {{\n context.value.length !== 0\n ? (\"i18n.qd.page.context.button.change\" | translate)\n : (\"i18n.qd.page.context.button.select\" | translate)\n }}\n </button>\n </span>\n </div>\n </ng-container>\n\n <ng-container *ngIf=\"contexts$ | async as togglerContexts\">\n <span class=\"facets-toggler\" *ngIf=\"adaptiveMode && headerFacets?.length + togglerContexts.length > 2\">\n <span *ngIf=\"facetsCollapsed\">+{{ headerFacets.length + togglerContexts.length - 2 }}</span>\n <button qdIconButton [data-test-id]=\"'toggle-button'\">\n <qd-icon [icon]=\"facetsCollapsed ? 'ctrlDown' : 'ctrlTop'\"></qd-icon>\n </button>\n </span>\n </ng-container>\n</div>\n", styles: [":host{display:flex;flex-direction:column;justify-content:space-between;padding:1.25rem;border-bottom:.125rem solid rgb(213,213,213);background:#fff;gap:1.25rem}@media (max-width: 599.98px){:host{padding:1.25rem .9375rem}}:host .object-header-top{display:flex;align-items:flex-start;justify-content:space-between}:host .object-header-top .object-header-title{padding:2px;color:#171717;font-size:1.3125rem;font-weight:500}:host .object-header-top .object-header-toolbar{display:flex;align-items:center;column-gap:.5rem}:host .object-header-top .object-header-toolbar .button-icon{background:none}:host .object-header-top .object-header-toolbar .button-icon+.button-icon{margin-left:-.5rem}:host .object-header-top .object-header-toolbar .header-action-icon{color:#14516f;cursor:pointer;font-size:1.5rem}:host.in-dialog .object-header-toolbar{padding-right:2.5rem}:host .object-header-facets{position:relative;display:flex;flex-wrap:wrap;font-size:1.125rem;gap:1.25rem}:host .object-header-facets .object-header-facet{display:flex;min-width:3.75rem;flex-direction:column}:host .object-header-facets .object-header-facet.hasDivider{padding-left:1.25rem;border-left:.0625rem solid rgb(117,117,117)}:host .object-header-facets .object-header-facet.adaptiveFirst{padding-top:1.25rem}:host .object-header-facets .object-header-facet .facet-label{display:block;margin-bottom:0;color:#757575;font-size:.875rem;padding-bottom:.375rem;font-weight:500}:host .object-header-facets .facets-toggler{position:absolute;top:0;right:0;display:flex;align-items:center;padding-right:.3125rem;font-size:.625rem;line-height:.625rem}:host .object-header-facets.amongThemselves{display:grid}:host .object-header-facets.amongThemselves .object-header-facet.hasDivider{padding-left:0;border-top:.0625rem solid rgb(117,117,117);border-left:none}.loading-overlay{position:absolute;z-index:10000;inset:0;display:flex;align-items:center;justify-content:center;background-color:#0003}.no-selected-context{height:1.625rem;color:#979797;font-size:.875rem;line-height:1.625rem}.select-button{margin-top:.25rem}.adaptive-menu-button{display:flex;background:unset;color:#454545;font-size:2rem}.adaptive-menu-button:hover,.adaptive-menu-button:focus{color:#000}.adaptive-menu-entry{display:block;overflow:hidden;width:100%;min-height:2.25rem;padding:0 1rem;background:#fff0;text-align:left;text-overflow:ellipsis;white-space:nowrap}\n"], dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { 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: QdButtonGhostDirective, selector: "button[qdButtonGhost], a[qdButtonGhost]" }, { kind: "directive", type: QdButtonLinkDirective, selector: "button[qdButtonLink], a[qdButtonLink], button[qd-button-link]" }, { kind: "component", type: QdIconButtonComponent, selector: "button[qdIconButton], a[qdIconButton], button[qd-icon-button]", inputs: ["color", "data-test-id"] }, { kind: "component", type: QdMenuButtonComponent, selector: "button[qdMenuButton], a[qdMenuButton]", inputs: ["qdMenuButton", "data-test-id", "autoSize"] }, { kind: "component", type: QdChipComponent, selector: "qd-chip", inputs: ["state", "close", "data", "data-test-id"], outputs: ["closeClickEmitter"] }, { kind: "component", type: QdIconComponent, selector: "qd-icon", inputs: ["icon", "status"] }, { kind: "component", type: QdSpinnerComponent, selector: "qd-spinner" }, { kind: "directive", type: QdPopoverOnClickDirective, selector: "[qdPopoverOnClick]", inputs: ["qdPopoverOnClick", "qdPopoverPlacement", "qdPopoverOffsetX", "qdPopoverOffsetY", "qdPopoverCloseStrategy", "qdPopoverDisabled", "qdPopoverStopPropagation", "qdPopoverBackgroundColor", "qdPopoverMaxHeight", "qdPopoverMinWidth", "qdPopoverMaxWidth", "qdPopoverAutoSize", "qdPopoverEnableKeyControl", "qdPopoverFlipIntoViewport"], outputs: ["opened", "closed"], exportAs: ["qdPopoverOnClick"] }, { kind: "component", type: QdDynamicFacetComponent, selector: "qd-dynamic-facet", inputs: ["facet", "data"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1.SlicePipe, name: "slice" }, { kind: "pipe", type: i3.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
29774
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: QdPageObjectHeaderComponent, isStandalone: false, selector: "qd-page-object-header", inputs: { config: "config", hasNavigation: "hasNavigation", testId: ["data-test-id", "testId"] }, host: { listeners: { "click": "toggleFacets()" }, properties: { "style.borderBottom": "borderBottomStyle", "class.in-dialog": "this.isInDialog" } }, usesOnChanges: true, ngImport: i0, template: "<div class=\"loading-overlay\" *ngIf=\"isLoading$ | async\"><qd-spinner></qd-spinner></div>\n\n<div class=\"object-header-top\">\n <header class=\"object-header-title\">{{ config.title.i18n | translate }}</header>\n <div class=\"object-header-toolbar\">\n <button class=\"button-icon\" data-test-id=\"archive-button\">\n <qd-icon\n *ngIf=\"showArchive$ | async\"\n (click)=\"archiveButton?.handler()\"\n class=\"header-action-icon\"\n [icon]=\"'resultNew'\"\n ></qd-icon>\n </button>\n\n <button class=\"button-icon\" data-test-id=\"delete-button\">\n <qd-icon\n class=\"header-action-icon\"\n *ngIf=\"showDelete$ | async\"\n (click)=\"deleteButton?.handler()\"\n [icon]=\"'trash'\"\n ></qd-icon>\n </button>\n\n <button *ngIf=\"showEdit$ | async\" qdButton (click)=\"edit()\" data-test-id=\"edit-button\">\n {{ editButton?.label?.i18n || \"i18n.qd.page.header.edit\" | translate }}\n </button>\n\n <button *ngIf=\"showCancel$ | async\" qdButton qdButtonGhost (click)=\"cancel()\" data-test-id=\"cancel-button\">\n {{ cancelButton.label?.i18n || \"i18n.qd.page.header.cancel\" | translate }}\n </button>\n\n <button\n *ngIf=\"showSave$ | async\"\n qdButton\n (click)=\"save()\"\n data-test-id=\"save-button\"\n [disabled]=\"($canSave | async) === false\"\n >\n {{ saveButton.label?.i18n || \"i18n.qd.page.header.save\" | translate }}\n </button>\n\n <ng-container *ngIf=\"customActions$ | async as customActions\">\n <ng-container *ngIf=\"!adaptiveMode; else adaptive\">\n <button\n [qdMenuButton]=\"customActions\"\n [autoSize]=\"false\"\n [data-test-id]=\"'page-'\"\n *ngIf=\"customActions.actions.length > 1; else singleButton\"\n data-test-id=\"custom-button\"\n >\n {{ config?.customActionsLabel?.i18n || \"i18n.qd.page.header.actions\" | translate }}\n </button>\n\n <ng-template #singleButton>\n <ng-container *ngIf=\"(customActions$ | async).actions[0] as singleAction\">\n <button\n qdButton\n *ngIf=\"singleAction.handler && !singleAction.isHidden\"\n [disabled]=\"singleAction.isDisabled\"\n (click)=\"singleAction.handler()\"\n data-test-id=\"custom-button\"\n >\n {{ singleAction.label?.i18n | translate }}\n </button>\n </ng-container>\n </ng-template>\n </ng-container>\n\n <ng-template #adaptive>\n <button\n *ngIf=\"(customActions$ | async)?.actions.length >= 1\"\n type=\"button\"\n class=\"adaptive-menu-button\"\n [qdPopoverOnClick]=\"menu\"\n [qdPopoverCloseStrategy]=\"'onEveryClick'\"\n [qdPopoverStopPropagation]=\"true\"\n [qdPopoverMinWidth]=\"160\"\n >\n <qd-icon icon=\"overflowMenuVertical\"></qd-icon>\n </button>\n\n <ng-template #menu>\n <button\n *ngFor=\"let action of customActions.actions; let i = index\"\n class=\"adaptive-menu-entry\"\n type=\"button\"\n [disabled]=\"action.isDisabled\"\n (click)=\"action.handler()\"\n >\n {{ action.label.i18n | translate }}\n </button>\n </ng-template>\n </ng-template>\n </ng-container>\n </div>\n</div>\n\n<div class=\"object-header-facets\" [ngClass]=\"{ amongThemselves: adaptiveMode && !facetsCollapsed }\">\n <ng-container *ngIf=\"pageObjectData$ | async as data\">\n <div\n class=\"object-header-facet\"\n *ngFor=\"let facet of headerFacets | slice : 0 : (adaptiveMode && facetsCollapsed ? 2 : headerFacets?.length ?? 0)\"\n [attr.data-test-id]=\"testId + '-facet-' + facet.name\"\n >\n <div class=\"facet-label\">{{ facet.label.i18n | translate }}</div>\n <qd-dynamic-facet class=\"dynamic-facet\" [facet]=\"facet\" [data]=\"data\"></qd-dynamic-facet>\n </div>\n </ng-container>\n\n <ng-container *ngIf=\"!config.hideContexts && contexts$ | async as contexts\">\n <div\n class=\"object-header-facet\"\n *ngFor=\"\n let context of contexts\n | slice : 0 : (adaptiveMode && facetsCollapsed ? 2 - (headerFacets?.length ?? 0) : contexts?.length);\n let index = index;\n let first = first\n \"\n [ngClass]=\"{ hasDivider: headerFacets && first, adaptiveFirst: headerFacets && adaptiveMode && first }\"\n >\n <div class=\"facet-label\">{{ context.label | translate }}</div>\n\n <span *ngIf=\"context.value.length === 0\" class=\"no-selected-context\">{{\n \"i18n.qd.page.context.noSelectionPlaceholder\" | translate\n }}</span>\n\n <span *ngIf=\"context.value.length !== 0\">\n <qd-chip state=\"none\" [close]=\"isContextSelectable\" (closeClickEmitter)=\"clearContext(context.context)\">\n <ng-container *ngFor=\"let val of context.value; let last = last\">\n {{ val.label.i18n | translate }}<span *ngIf=\"!last\">, </span>\n </ng-container>\n </qd-chip>\n </span>\n\n <span *ngIf=\"isContextSelectable && (!facetsCollapsed || !adaptiveMode)\" class=\"select-button\">\n <button\n qdButton\n qdButtonLink\n type=\"button\"\n (click)=\"changeContext(context.context, context.selection, $event)\"\n [data-test-id]=\"'object-header-select-context-' + context.context.id\"\n >\n {{\n context.value.length !== 0\n ? (\"i18n.qd.page.context.button.change\" | translate)\n : (\"i18n.qd.page.context.button.select\" | translate)\n }}\n </button>\n </span>\n </div>\n </ng-container>\n\n <ng-container *ngIf=\"contexts$ | async as togglerContexts\">\n <span\n class=\"facets-toggler\"\n [attr.data-test-id]=\"testId + '-facets-toggler'\"\n *ngIf=\"adaptiveMode && headerFacets?.length + togglerContexts.length > 2\"\n >\n <span *ngIf=\"facetsCollapsed\">+{{ headerFacets.length + togglerContexts.length - 2 }}</span>\n <button qdIconButton [data-test-id]=\"'toggle-button'\">\n <qd-icon [icon]=\"facetsCollapsed ? 'ctrlDown' : 'ctrlTop'\"></qd-icon>\n </button>\n </span>\n </ng-container>\n</div>\n", styles: [":host{display:flex;flex-direction:column;justify-content:space-between;padding:1.25rem;border-bottom:.125rem solid rgb(213,213,213);background:#fff;gap:1.25rem}@media (max-width: 599.98px){:host{padding:1.25rem .9375rem}}:host .object-header-top{display:flex;align-items:flex-start;justify-content:space-between}:host .object-header-top .object-header-title{padding:2px;color:#171717;font-size:1.3125rem;font-weight:500}:host .object-header-top .object-header-toolbar{display:flex;align-items:center;column-gap:.5rem}:host .object-header-top .object-header-toolbar .button-icon{background:none}:host .object-header-top .object-header-toolbar .button-icon+.button-icon{margin-left:-.5rem}:host .object-header-top .object-header-toolbar .header-action-icon{color:#14516f;cursor:pointer;font-size:1.5rem}:host.in-dialog .object-header-toolbar{padding-right:2.5rem}:host .object-header-facets{position:relative;display:flex;flex-wrap:wrap;font-size:1.125rem;gap:1.25rem}:host .object-header-facets .object-header-facet{display:flex;min-width:3.75rem;flex-direction:column}:host .object-header-facets .object-header-facet.hasDivider{padding-left:1.25rem;border-left:.0625rem solid rgb(117,117,117)}:host .object-header-facets .object-header-facet.adaptiveFirst{padding-top:1.25rem}:host .object-header-facets .object-header-facet .facet-label{display:block;margin-bottom:0;color:#757575;font-size:.875rem;padding-bottom:.375rem;font-weight:500}:host .object-header-facets .facets-toggler{position:absolute;top:0;right:0;display:flex;align-items:center;padding-right:.3125rem;font-size:.625rem;line-height:.625rem}:host .object-header-facets.amongThemselves{display:grid}:host .object-header-facets.amongThemselves .object-header-facet.hasDivider{padding-left:0;border-top:.0625rem solid rgb(117,117,117);border-left:none}.loading-overlay{position:absolute;z-index:10000;inset:0;display:flex;align-items:center;justify-content:center;background-color:#0003}.no-selected-context{height:1.625rem;color:#979797;font-size:.875rem;line-height:1.625rem}.select-button{margin-top:.25rem}.adaptive-menu-button{display:flex;background:unset;color:#454545;font-size:2rem}.adaptive-menu-button:hover,.adaptive-menu-button:focus{color:#000}.adaptive-menu-entry{display:block;overflow:hidden;width:100%;min-height:2.25rem;padding:0 1rem;background:#fff0;text-align:left;text-overflow:ellipsis;white-space:nowrap}\n"], dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { 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: QdButtonGhostDirective, selector: "button[qdButtonGhost], a[qdButtonGhost]" }, { kind: "directive", type: QdButtonLinkDirective, selector: "button[qdButtonLink], a[qdButtonLink], button[qd-button-link]" }, { kind: "component", type: QdIconButtonComponent, selector: "button[qdIconButton], a[qdIconButton], button[qd-icon-button]", inputs: ["color", "data-test-id"] }, { kind: "component", type: QdMenuButtonComponent, selector: "button[qdMenuButton], a[qdMenuButton]", inputs: ["qdMenuButton", "data-test-id", "autoSize"] }, { kind: "component", type: QdChipComponent, selector: "qd-chip", inputs: ["state", "close", "data", "data-test-id"], outputs: ["closeClickEmitter"] }, { kind: "component", type: QdIconComponent, selector: "qd-icon", inputs: ["icon", "status"] }, { kind: "component", type: QdSpinnerComponent, selector: "qd-spinner" }, { kind: "directive", type: QdPopoverOnClickDirective, selector: "[qdPopoverOnClick]", inputs: ["qdPopoverOnClick", "qdPopoverPlacement", "qdPopoverOffsetX", "qdPopoverOffsetY", "qdPopoverCloseStrategy", "qdPopoverDisabled", "qdPopoverStopPropagation", "qdPopoverBackgroundColor", "qdPopoverMaxHeight", "qdPopoverMinWidth", "qdPopoverMaxWidth", "qdPopoverAutoSize", "qdPopoverEnableKeyControl", "qdPopoverFlipIntoViewport"], outputs: ["opened", "closed"], exportAs: ["qdPopoverOnClick"] }, { kind: "component", type: QdDynamicFacetComponent, selector: "qd-dynamic-facet", inputs: ["facet", "data"] }, { kind: "pipe", type: i1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1.SlicePipe, name: "slice" }, { kind: "pipe", type: i3.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
29945
29775
  }
29946
29776
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPageObjectHeaderComponent, decorators: [{
29947
29777
  type: Component,
29948
- args: [{ selector: 'qd-page-object-header', changeDetection: ChangeDetectionStrategy.OnPush, host: { '[style.borderBottom]': 'borderBottomStyle' }, standalone: false, template: "<div class=\"loading-overlay\" *ngIf=\"isLoading$ | async\"><qd-spinner></qd-spinner></div>\n\n<div class=\"object-header-top\">\n <header class=\"object-header-title\">{{ config.title.i18n | translate }}</header>\n <div class=\"object-header-toolbar\">\n <button class=\"button-icon\" data-test-id=\"archive-button\">\n <qd-icon\n *ngIf=\"showArchive$ | async\"\n (click)=\"archiveButton?.handler()\"\n class=\"header-action-icon\"\n [icon]=\"'resultNew'\"\n ></qd-icon>\n </button>\n\n <button class=\"button-icon\" data-test-id=\"delete-button\">\n <qd-icon\n class=\"header-action-icon\"\n *ngIf=\"showDelete$ | async\"\n (click)=\"deleteButton?.handler()\"\n [icon]=\"'trash'\"\n ></qd-icon>\n </button>\n\n <button *ngIf=\"showEdit$ | async\" qdButton (click)=\"edit()\" data-test-id=\"edit-button\">\n {{ editButton?.label?.i18n || \"i18n.qd.page.header.edit\" | translate }}\n </button>\n\n <button *ngIf=\"showCancel$ | async\" qdButton qdButtonGhost (click)=\"cancel()\" data-test-id=\"cancel-button\">\n {{ cancelButton.label?.i18n || \"i18n.qd.page.header.cancel\" | translate }}\n </button>\n\n <button\n *ngIf=\"showSave$ | async\"\n qdButton\n (click)=\"save()\"\n data-test-id=\"save-button\"\n [disabled]=\"($canSave | async) === false\"\n >\n {{ saveButton.label?.i18n || \"i18n.qd.page.header.save\" | translate }}\n </button>\n\n <ng-container *ngIf=\"customActions$ | async as customActions\">\n <ng-container *ngIf=\"!adaptiveMode; else adaptive\">\n <button\n [qdMenuButton]=\"customActions\"\n [autoSize]=\"false\"\n [data-test-id]=\"'page-'\"\n *ngIf=\"customActions.actions.length > 1; else singleButton\"\n data-test-id=\"custom-button\"\n >\n {{ config?.customActionsLabel?.i18n || \"i18n.qd.page.header.actions\" | translate }}\n </button>\n\n <ng-template #singleButton>\n <ng-container *ngIf=\"(customActions$ | async).actions[0] as singleAction\">\n <button\n qdButton\n *ngIf=\"singleAction.handler && !singleAction.isHidden\"\n [disabled]=\"singleAction.isDisabled\"\n (click)=\"singleAction.handler()\"\n data-test-id=\"custom-button\"\n >\n {{ singleAction.label?.i18n | translate }}\n </button>\n </ng-container>\n </ng-template>\n </ng-container>\n\n <ng-template #adaptive>\n <button\n *ngIf=\"(customActions$ | async)?.actions.length >= 1\"\n type=\"button\"\n class=\"adaptive-menu-button\"\n [qdPopoverOnClick]=\"menu\"\n [qdPopoverCloseStrategy]=\"'onEveryClick'\"\n [qdPopoverStopPropagation]=\"true\"\n [qdPopoverMinWidth]=\"160\"\n >\n <qd-icon icon=\"overflowMenuVertical\"></qd-icon>\n </button>\n\n <ng-template #menu>\n <button\n *ngFor=\"let action of customActions.actions; let i = index\"\n class=\"adaptive-menu-entry\"\n type=\"button\"\n [disabled]=\"action.isDisabled\"\n (click)=\"action.handler()\"\n >\n {{ action.label.i18n | translate }}\n </button>\n </ng-template>\n </ng-template>\n </ng-container>\n </div>\n</div>\n\n<div class=\"object-header-facets\" [ngClass]=\"{ amongThemselves: adaptiveMode && !facetsCollapsed }\">\n <ng-container *ngIf=\"pageObjectData$ | async as data\">\n <div\n class=\"object-header-facet\"\n *ngFor=\"let facet of headerFacets | slice : 0 : (adaptiveMode && facetsCollapsed ? 2 : headerFacets?.length ?? 0)\"\n [attr.data-test-id]=\"testId + '-facet-' + facet.name\"\n >\n <div class=\"facet-label\">{{ facet.label.i18n | translate }}</div>\n <qd-dynamic-facet class=\"dynamic-facet\" [facet]=\"facet\" [data]=\"data\"></qd-dynamic-facet>\n </div>\n </ng-container>\n\n <ng-container *ngIf=\"!config.hideContexts && contexts$ | async as contexts\">\n <div\n class=\"object-header-facet\"\n *ngFor=\"\n let context of contexts\n | slice : 0 : (adaptiveMode && facetsCollapsed ? 2 - (headerFacets?.length ?? 0) : contexts?.length);\n let index = index;\n let first = first\n \"\n [ngClass]=\"{ hasDivider: headerFacets && first, adaptiveFirst: headerFacets && adaptiveMode && first }\"\n >\n <div class=\"facet-label\">{{ context.label | translate }}</div>\n\n <span *ngIf=\"context.value.length === 0\" class=\"no-selected-context\">{{\n \"i18n.qd.page.context.noSelectionPlaceholder\" | translate\n }}</span>\n\n <span *ngIf=\"context.value.length !== 0\">\n <qd-chip state=\"none\" [close]=\"isContextSelectable\" (closeClickEmitter)=\"clearContext(context.context)\">\n <ng-container *ngFor=\"let val of context.value; let last = last\">\n {{ val.label.i18n | translate }}<span *ngIf=\"!last\">, </span>\n </ng-container>\n </qd-chip>\n </span>\n\n <span *ngIf=\"isContextSelectable && (!facetsCollapsed || !adaptiveMode)\" class=\"select-button\">\n <button\n qdButton\n qdButtonLink\n type=\"button\"\n (click)=\"changeContext(context.context, context.selection, $event)\"\n [data-test-id]=\"'object-header-select-context-' + context.context.id\"\n >\n {{\n context.value.length !== 0\n ? (\"i18n.qd.page.context.button.change\" | translate)\n : (\"i18n.qd.page.context.button.select\" | translate)\n }}\n </button>\n </span>\n </div>\n </ng-container>\n\n <ng-container *ngIf=\"contexts$ | async as togglerContexts\">\n <span class=\"facets-toggler\" *ngIf=\"adaptiveMode && headerFacets?.length + togglerContexts.length > 2\">\n <span *ngIf=\"facetsCollapsed\">+{{ headerFacets.length + togglerContexts.length - 2 }}</span>\n <button qdIconButton [data-test-id]=\"'toggle-button'\">\n <qd-icon [icon]=\"facetsCollapsed ? 'ctrlDown' : 'ctrlTop'\"></qd-icon>\n </button>\n </span>\n </ng-container>\n</div>\n", styles: [":host{display:flex;flex-direction:column;justify-content:space-between;padding:1.25rem;border-bottom:.125rem solid rgb(213,213,213);background:#fff;gap:1.25rem}@media (max-width: 599.98px){:host{padding:1.25rem .9375rem}}:host .object-header-top{display:flex;align-items:flex-start;justify-content:space-between}:host .object-header-top .object-header-title{padding:2px;color:#171717;font-size:1.3125rem;font-weight:500}:host .object-header-top .object-header-toolbar{display:flex;align-items:center;column-gap:.5rem}:host .object-header-top .object-header-toolbar .button-icon{background:none}:host .object-header-top .object-header-toolbar .button-icon+.button-icon{margin-left:-.5rem}:host .object-header-top .object-header-toolbar .header-action-icon{color:#14516f;cursor:pointer;font-size:1.5rem}:host.in-dialog .object-header-toolbar{padding-right:2.5rem}:host .object-header-facets{position:relative;display:flex;flex-wrap:wrap;font-size:1.125rem;gap:1.25rem}:host .object-header-facets .object-header-facet{display:flex;min-width:3.75rem;flex-direction:column}:host .object-header-facets .object-header-facet.hasDivider{padding-left:1.25rem;border-left:.0625rem solid rgb(117,117,117)}:host .object-header-facets .object-header-facet.adaptiveFirst{padding-top:1.25rem}:host .object-header-facets .object-header-facet .facet-label{display:block;margin-bottom:0;color:#757575;font-size:.875rem;padding-bottom:.375rem;font-weight:500}:host .object-header-facets .facets-toggler{position:absolute;top:0;right:0;display:flex;align-items:center;padding-right:.3125rem;font-size:.625rem;line-height:.625rem}:host .object-header-facets.amongThemselves{display:grid}:host .object-header-facets.amongThemselves .object-header-facet.hasDivider{padding-left:0;border-top:.0625rem solid rgb(117,117,117);border-left:none}.loading-overlay{position:absolute;z-index:10000;inset:0;display:flex;align-items:center;justify-content:center;background-color:#0003}.no-selected-context{height:1.625rem;color:#979797;font-size:.875rem;line-height:1.625rem}.select-button{margin-top:.25rem}.adaptive-menu-button{display:flex;background:unset;color:#454545;font-size:2rem}.adaptive-menu-button:hover,.adaptive-menu-button:focus{color:#000}.adaptive-menu-entry{display:block;overflow:hidden;width:100%;min-height:2.25rem;padding:0 1rem;background:#fff0;text-align:left;text-overflow:ellipsis;white-space:nowrap}\n"] }]
29778
+ args: [{ selector: 'qd-page-object-header', changeDetection: ChangeDetectionStrategy.OnPush, host: { '[style.borderBottom]': 'borderBottomStyle' }, standalone: false, template: "<div class=\"loading-overlay\" *ngIf=\"isLoading$ | async\"><qd-spinner></qd-spinner></div>\n\n<div class=\"object-header-top\">\n <header class=\"object-header-title\">{{ config.title.i18n | translate }}</header>\n <div class=\"object-header-toolbar\">\n <button class=\"button-icon\" data-test-id=\"archive-button\">\n <qd-icon\n *ngIf=\"showArchive$ | async\"\n (click)=\"archiveButton?.handler()\"\n class=\"header-action-icon\"\n [icon]=\"'resultNew'\"\n ></qd-icon>\n </button>\n\n <button class=\"button-icon\" data-test-id=\"delete-button\">\n <qd-icon\n class=\"header-action-icon\"\n *ngIf=\"showDelete$ | async\"\n (click)=\"deleteButton?.handler()\"\n [icon]=\"'trash'\"\n ></qd-icon>\n </button>\n\n <button *ngIf=\"showEdit$ | async\" qdButton (click)=\"edit()\" data-test-id=\"edit-button\">\n {{ editButton?.label?.i18n || \"i18n.qd.page.header.edit\" | translate }}\n </button>\n\n <button *ngIf=\"showCancel$ | async\" qdButton qdButtonGhost (click)=\"cancel()\" data-test-id=\"cancel-button\">\n {{ cancelButton.label?.i18n || \"i18n.qd.page.header.cancel\" | translate }}\n </button>\n\n <button\n *ngIf=\"showSave$ | async\"\n qdButton\n (click)=\"save()\"\n data-test-id=\"save-button\"\n [disabled]=\"($canSave | async) === false\"\n >\n {{ saveButton.label?.i18n || \"i18n.qd.page.header.save\" | translate }}\n </button>\n\n <ng-container *ngIf=\"customActions$ | async as customActions\">\n <ng-container *ngIf=\"!adaptiveMode; else adaptive\">\n <button\n [qdMenuButton]=\"customActions\"\n [autoSize]=\"false\"\n [data-test-id]=\"'page-'\"\n *ngIf=\"customActions.actions.length > 1; else singleButton\"\n data-test-id=\"custom-button\"\n >\n {{ config?.customActionsLabel?.i18n || \"i18n.qd.page.header.actions\" | translate }}\n </button>\n\n <ng-template #singleButton>\n <ng-container *ngIf=\"(customActions$ | async).actions[0] as singleAction\">\n <button\n qdButton\n *ngIf=\"singleAction.handler && !singleAction.isHidden\"\n [disabled]=\"singleAction.isDisabled\"\n (click)=\"singleAction.handler()\"\n data-test-id=\"custom-button\"\n >\n {{ singleAction.label?.i18n | translate }}\n </button>\n </ng-container>\n </ng-template>\n </ng-container>\n\n <ng-template #adaptive>\n <button\n *ngIf=\"(customActions$ | async)?.actions.length >= 1\"\n type=\"button\"\n class=\"adaptive-menu-button\"\n [qdPopoverOnClick]=\"menu\"\n [qdPopoverCloseStrategy]=\"'onEveryClick'\"\n [qdPopoverStopPropagation]=\"true\"\n [qdPopoverMinWidth]=\"160\"\n >\n <qd-icon icon=\"overflowMenuVertical\"></qd-icon>\n </button>\n\n <ng-template #menu>\n <button\n *ngFor=\"let action of customActions.actions; let i = index\"\n class=\"adaptive-menu-entry\"\n type=\"button\"\n [disabled]=\"action.isDisabled\"\n (click)=\"action.handler()\"\n >\n {{ action.label.i18n | translate }}\n </button>\n </ng-template>\n </ng-template>\n </ng-container>\n </div>\n</div>\n\n<div class=\"object-header-facets\" [ngClass]=\"{ amongThemselves: adaptiveMode && !facetsCollapsed }\">\n <ng-container *ngIf=\"pageObjectData$ | async as data\">\n <div\n class=\"object-header-facet\"\n *ngFor=\"let facet of headerFacets | slice : 0 : (adaptiveMode && facetsCollapsed ? 2 : headerFacets?.length ?? 0)\"\n [attr.data-test-id]=\"testId + '-facet-' + facet.name\"\n >\n <div class=\"facet-label\">{{ facet.label.i18n | translate }}</div>\n <qd-dynamic-facet class=\"dynamic-facet\" [facet]=\"facet\" [data]=\"data\"></qd-dynamic-facet>\n </div>\n </ng-container>\n\n <ng-container *ngIf=\"!config.hideContexts && contexts$ | async as contexts\">\n <div\n class=\"object-header-facet\"\n *ngFor=\"\n let context of contexts\n | slice : 0 : (adaptiveMode && facetsCollapsed ? 2 - (headerFacets?.length ?? 0) : contexts?.length);\n let index = index;\n let first = first\n \"\n [ngClass]=\"{ hasDivider: headerFacets && first, adaptiveFirst: headerFacets && adaptiveMode && first }\"\n >\n <div class=\"facet-label\">{{ context.label | translate }}</div>\n\n <span *ngIf=\"context.value.length === 0\" class=\"no-selected-context\">{{\n \"i18n.qd.page.context.noSelectionPlaceholder\" | translate\n }}</span>\n\n <span *ngIf=\"context.value.length !== 0\">\n <qd-chip state=\"none\" [close]=\"isContextSelectable\" (closeClickEmitter)=\"clearContext(context.context)\">\n <ng-container *ngFor=\"let val of context.value; let last = last\">\n {{ val.label.i18n | translate }}<span *ngIf=\"!last\">, </span>\n </ng-container>\n </qd-chip>\n </span>\n\n <span *ngIf=\"isContextSelectable && (!facetsCollapsed || !adaptiveMode)\" class=\"select-button\">\n <button\n qdButton\n qdButtonLink\n type=\"button\"\n (click)=\"changeContext(context.context, context.selection, $event)\"\n [data-test-id]=\"'object-header-select-context-' + context.context.id\"\n >\n {{\n context.value.length !== 0\n ? (\"i18n.qd.page.context.button.change\" | translate)\n : (\"i18n.qd.page.context.button.select\" | translate)\n }}\n </button>\n </span>\n </div>\n </ng-container>\n\n <ng-container *ngIf=\"contexts$ | async as togglerContexts\">\n <span\n class=\"facets-toggler\"\n [attr.data-test-id]=\"testId + '-facets-toggler'\"\n *ngIf=\"adaptiveMode && headerFacets?.length + togglerContexts.length > 2\"\n >\n <span *ngIf=\"facetsCollapsed\">+{{ headerFacets.length + togglerContexts.length - 2 }}</span>\n <button qdIconButton [data-test-id]=\"'toggle-button'\">\n <qd-icon [icon]=\"facetsCollapsed ? 'ctrlDown' : 'ctrlTop'\"></qd-icon>\n </button>\n </span>\n </ng-container>\n</div>\n", styles: [":host{display:flex;flex-direction:column;justify-content:space-between;padding:1.25rem;border-bottom:.125rem solid rgb(213,213,213);background:#fff;gap:1.25rem}@media (max-width: 599.98px){:host{padding:1.25rem .9375rem}}:host .object-header-top{display:flex;align-items:flex-start;justify-content:space-between}:host .object-header-top .object-header-title{padding:2px;color:#171717;font-size:1.3125rem;font-weight:500}:host .object-header-top .object-header-toolbar{display:flex;align-items:center;column-gap:.5rem}:host .object-header-top .object-header-toolbar .button-icon{background:none}:host .object-header-top .object-header-toolbar .button-icon+.button-icon{margin-left:-.5rem}:host .object-header-top .object-header-toolbar .header-action-icon{color:#14516f;cursor:pointer;font-size:1.5rem}:host.in-dialog .object-header-toolbar{padding-right:2.5rem}:host .object-header-facets{position:relative;display:flex;flex-wrap:wrap;font-size:1.125rem;gap:1.25rem}:host .object-header-facets .object-header-facet{display:flex;min-width:3.75rem;flex-direction:column}:host .object-header-facets .object-header-facet.hasDivider{padding-left:1.25rem;border-left:.0625rem solid rgb(117,117,117)}:host .object-header-facets .object-header-facet.adaptiveFirst{padding-top:1.25rem}:host .object-header-facets .object-header-facet .facet-label{display:block;margin-bottom:0;color:#757575;font-size:.875rem;padding-bottom:.375rem;font-weight:500}:host .object-header-facets .facets-toggler{position:absolute;top:0;right:0;display:flex;align-items:center;padding-right:.3125rem;font-size:.625rem;line-height:.625rem}:host .object-header-facets.amongThemselves{display:grid}:host .object-header-facets.amongThemselves .object-header-facet.hasDivider{padding-left:0;border-top:.0625rem solid rgb(117,117,117);border-left:none}.loading-overlay{position:absolute;z-index:10000;inset:0;display:flex;align-items:center;justify-content:center;background-color:#0003}.no-selected-context{height:1.625rem;color:#979797;font-size:.875rem;line-height:1.625rem}.select-button{margin-top:.25rem}.adaptive-menu-button{display:flex;background:unset;color:#454545;font-size:2rem}.adaptive-menu-button:hover,.adaptive-menu-button:focus{color:#000}.adaptive-menu-entry{display:block;overflow:hidden;width:100%;min-height:2.25rem;padding:0 1rem;background:#fff0;text-align:left;text-overflow:ellipsis;white-space:nowrap}\n"] }]
29949
29779
  }], ctorParameters: () => [], propDecorators: { config: [{
29950
29780
  type: Input,
29951
29781
  args: [{ required: true }]
@@ -31627,7 +31457,6 @@ class QdPageComponent {
31627
31457
  bottomOffset$ = inject(QD_SAFE_BOTTOM_OFFSET, { optional: true });
31628
31458
  dialogRef = inject(DialogRef, { optional: true });
31629
31459
  navigationInterceptor = inject(QdPageNavigationInterceptorService);
31630
- injector = inject(Injector);
31631
31460
  confirmationDialogService = inject(QdConfirmationDialogOpenerService);
31632
31461
  /**
31633
31462
  * This property defines the configuration for the QdPage component, including the page type,
@@ -31824,10 +31653,7 @@ class QdPageComponent {
31824
31653
  QdPageCommitActionExecutor.execute(action, values, {
31825
31654
  formGroupManager: this.formGroupManagerService,
31826
31655
  navigationInterceptor: this.navigationInterceptor,
31827
- destroyed$: this._destroyed$,
31828
- scheduleFrameworkRebuildStop: () => afterNextRender(() => this.formGroupManagerService.stopAdoptingFrameworkRebuild(), {
31829
- injector: this.injector
31830
- })
31656
+ destroyed$: this._destroyed$
31831
31657
  });
31832
31658
  };
31833
31659
  }
@@ -31939,10 +31765,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
31939
31765
  * - Snapshots store only raw values and are taken by the page/header workflow (e.g. on init, edit, save). The confirmation dialog may restore all registered form groups from the snapshot.
31940
31766
  * - FormArrays are restored with dynamic resizing (length is aligned to snapshot before values are reset).
31941
31767
  *
31942
- * ### **Filling a form right after connecting it**
31943
- * - You can fill a connected form right after registration — e.g. an `effect` that adds controls or pushes `FormArray` rows. This first fill is the form's starting point, not a user change.
31944
- * - So loading data into a fresh form never triggers a false "unsaved changes" prompt. Only later edits count.
31945
- *
31946
31768
  * ### **Page-Dialog (FullWidth) – Unsaved-Changes Close Contract**
31947
31769
  * - When a QdPage is embedded in a full-width page dialog, the directive’s registered form groups are also used to guard dialog closing.
31948
31770
  * - The page dialog can only close (e.g. via ESC or backdrop) if no unsaved changes are detected ($hasValuesChanged() is false).
@@ -31955,10 +31777,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
31955
31777
  * ```html
31956
31778
  * <qd-page [config]="pageConfig">
31957
31779
  * <qd-section [config]="sectionConfig">
31958
- * <qd-grid
31959
- * qdConnectFormStateToPage="myFormGroup"
31960
- * [formGroup]="myFormGroup">
31961
- * My Section
31780
+ * <qd-grid [formGroup]="myFormGroup" qdConnectFormStateToPage="myFormGroup">
31781
+ * My Section
31962
31782
  * </qd-grid>
31963
31783
  * </qd-section>
31964
31784
  * </qd-page>
@@ -31978,36 +31798,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
31978
31798
  class QdConnectFormStateToPageDirective {
31979
31799
  formGroupManagerService = inject(QdFormGroupManagerService, { optional: true });
31980
31800
  formGroupDirective = inject(FormGroupDirective, { host: true, optional: true });
31981
- closeInitialBuildupHandle;
31982
31801
  /**
31983
31802
  * This key is used to manage and track the form state within the page. It has to be unique for each FormGroup Connector. Use the property name of the FormGroup in your component.
31984
31803
  */
31985
31804
  qdConnectFormStateToPage;
31986
31805
  ngOnInit() {
31806
+ if (!this.isValid())
31807
+ return;
31987
31808
  const formGroup = this.formGroupDirective?.control;
31988
- if (!formGroup) {
31809
+ if (formGroup && this.qdConnectFormStateToPage) {
31810
+ this.formGroupManagerService.setFormGroup(this.qdConnectFormStateToPage, formGroup);
31811
+ }
31812
+ }
31813
+ ngOnDestroy() {
31814
+ this.formGroupManagerService?.tryRemoveFormGroup(this.qdConnectFormStateToPage);
31815
+ }
31816
+ isValid() {
31817
+ const hasFormGroup = this.formGroupDirective?.control;
31818
+ if (!hasFormGroup) {
31989
31819
  console.error('Quadrel Framework | QdConnectFormStateToPage - Either a [formGroup] binding or <qd-quick-edit> with a FormGroup is required.');
31990
- return;
31820
+ return false;
31991
31821
  }
31992
31822
  if (!this.formGroupManagerService) {
31993
31823
  console.error('Quadrel Framework | QdConnectFormStateToPage - The connector must be used within <qd-page>.');
31994
- return;
31824
+ return false;
31995
31825
  }
31996
31826
  if (!this.formGroupManagerService.isFormGroupKeyUnique(this.qdConnectFormStateToPage)) {
31997
31827
  console.error(`Quadrel Framework | QdConnectFormStateToPage - The FormGroup key "${this.qdConnectFormStateToPage}" is not unique.`);
31998
- return;
31828
+ return false;
31999
31829
  }
32000
- this.formGroupManagerService.setFormGroup(this.qdConnectFormStateToPage, formGroup);
32001
- this.scheduleInitialBaseline(this.formGroupManagerService);
32002
- }
32003
- ngOnDestroy() {
32004
- if (this.closeInitialBuildupHandle)
32005
- clearTimeout(this.closeInitialBuildupHandle);
32006
- this.formGroupManagerService?.tryRemoveFormGroup(this.qdConnectFormStateToPage);
32007
- }
32008
- scheduleInitialBaseline(manager) {
32009
- const key = this.qdConnectFormStateToPage;
32010
- this.closeInitialBuildupHandle = setTimeout(() => manager.endInitialBuildup(key));
31830
+ return true;
32011
31831
  }
32012
31832
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdConnectFormStateToPageDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
32013
31833
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.18", type: QdConnectFormStateToPageDirective, isStandalone: false, selector: "[qdConnectFormStateToPage]", inputs: { qdConnectFormStateToPage: "qdConnectFormStateToPage" }, ngImport: i0 });