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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { 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';
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';
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 >\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 });
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 });
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 >\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"] }]
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"] }]
1558
1558
  }], propDecorators: { body: [{
1559
1559
  type: ViewChild,
1560
1560
  args: ['body']
@@ -17015,11 +17015,11 @@ class QdNotificationComponent {
17015
17015
  return !!this.element.nativeElement.closest('qd-snackbar');
17016
17016
  }
17017
17017
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdNotificationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
17018
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: QdNotificationComponent, isStandalone: false, selector: "qd-notification", inputs: { type: "type", notification: "notification" }, host: { properties: { "attr.class": "this.typeClassName" }, classAttribute: "qd-notification" }, ngImport: i0, template: "<qd-icon *ngIf=\"closeable\" [icon]=\"'timesLarge'\" class=\"closer\" (click)=\"remove()\"></qd-icon>\n<qd-icon [icon]=\"this.getIcon()\" class=\"icon\"></qd-icon>\n\n<ng-content select=\"qd-notification-content\"></ng-content>\n\n<div class=\"title\" *ngIf=\"notification?.title\">{{ notification.title.i18n | translate }}</div>\n<div class=\"message\">\n <span>{{ notification?.i18n | translate }}</span>\n <a class=\"link\" *ngIf=\"notification?.link\" (click)=\"handleLinkClick()\">{{ notification.link.i18n | translate }}</a>\n</div>\n", styles: [":host{position:relative;z-index:9999!important;display:block;width:100%;padding:1rem 2rem .5rem 2.75rem;border-width:.0625rem;border-style:solid;border-left-width:.25rem;margin-bottom:.5rem;background:#fff;box-shadow:0 0 .625rem #979797;font-size:.875rem;gap:.5rem;line-height:1rem}:host.qd-notification-info{border-color:#069}:host.qd-notification-info .icon,:host.qd-notification-info .title{color:#069}:host.qd-notification-warning{border-color:#ff9b00}:host.qd-notification-warning .icon,:host.qd-notification-warning .title{color:#ff9b00}:host.qd-notification-critical{border-color:#c70023}:host.qd-notification-critical .icon,:host.qd-notification-critical .title{color:#c70023}:host.qd-notification-success{border-color:#00813a}:host.qd-notification-success .icon,:host.qd-notification-success .title{color:#00813a}:host .icon{position:absolute;top:.9375rem;left:1rem;font-size:1.25rem}:host .closer{position:absolute;top:1rem;right:.75rem;color:#171717;cursor:pointer;font-size:.8125rem;font-weight:600}:host .title{margin-bottom:.375rem;font-weight:500}:host .message{margin-bottom:.25rem;white-space:pre-wrap}:host .link{margin-bottom:.375rem;margin-left:.375rem;color:#069;cursor:pointer}:host .link:hover{text-decoration:underline}:host:host-context(qd-snackbar) .link{display:block;margin-top:.375rem;margin-left:0}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: QdIconComponent, selector: "qd-icon", inputs: ["icon", "status"] }, { kind: "pipe", type: i3.TranslatePipe, name: "translate" }] });
17018
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.18", type: QdNotificationComponent, isStandalone: false, selector: "qd-notification", inputs: { type: "type", notification: "notification" }, host: { properties: { "attr.class": "this.typeClassName" }, classAttribute: "qd-notification" }, ngImport: i0, template: "<qd-icon *ngIf=\"closeable\" [icon]=\"'timesLarge'\" class=\"closer\" (click)=\"remove()\"></qd-icon>\n<qd-icon [icon]=\"this.getIcon()\" class=\"icon\"></qd-icon>\n\n<ng-content select=\"qd-notification-content\"></ng-content>\n\n<div class=\"title\" *ngIf=\"notification?.title\">{{ notification.title.i18n | translate }}</div>\n<div class=\"message\">\n <span>{{ notification?.i18n | translate }}</span>\n <a class=\"link\" *ngIf=\"notification?.link\" (click)=\"handleLinkClick()\">{{ notification.link.i18n | translate }}</a>\n</div>\n", styles: [":host{position:relative;display:block;width:100%;padding:1rem 2rem .5rem 2.75rem;border-width:.0625rem;border-style:solid;border-left-width:.25rem;margin-bottom:.5rem;background:#fff;box-shadow:0 0 .625rem #979797;font-size:.875rem;gap:.5rem;line-height:1rem}:host.qd-notification-info{border-color:#069}:host.qd-notification-info .icon,:host.qd-notification-info .title{color:#069}:host.qd-notification-warning{border-color:#ff9b00}:host.qd-notification-warning .icon,:host.qd-notification-warning .title{color:#ff9b00}:host.qd-notification-critical{border-color:#c70023}:host.qd-notification-critical .icon,:host.qd-notification-critical .title{color:#c70023}:host.qd-notification-success{border-color:#00813a}:host.qd-notification-success .icon,:host.qd-notification-success .title{color:#00813a}:host .icon{position:absolute;top:.9375rem;left:1rem;font-size:1.25rem}:host .closer{position:absolute;top:1rem;right:.75rem;color:#171717;cursor:pointer;font-size:.8125rem;font-weight:600}:host .title{margin-bottom:.375rem;font-weight:500}:host .message{margin-bottom:.25rem;white-space:pre-wrap}:host .link{margin-bottom:.375rem;margin-left:.375rem;color:#069;cursor:pointer}:host .link:hover{text-decoration:underline}:host:host-context(qd-snackbar){z-index:9999!important}:host:host-context(qd-snackbar) .link{display:block;margin-top:.375rem;margin-left:0}\n"], dependencies: [{ kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: QdIconComponent, selector: "qd-icon", inputs: ["icon", "status"] }, { kind: "pipe", type: i3.TranslatePipe, name: "translate" }] });
17019
17019
  }
17020
17020
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdNotificationComponent, decorators: [{
17021
17021
  type: Component,
17022
- args: [{ selector: 'qd-notification', host: { class: 'qd-notification' }, standalone: false, template: "<qd-icon *ngIf=\"closeable\" [icon]=\"'timesLarge'\" class=\"closer\" (click)=\"remove()\"></qd-icon>\n<qd-icon [icon]=\"this.getIcon()\" class=\"icon\"></qd-icon>\n\n<ng-content select=\"qd-notification-content\"></ng-content>\n\n<div class=\"title\" *ngIf=\"notification?.title\">{{ notification.title.i18n | translate }}</div>\n<div class=\"message\">\n <span>{{ notification?.i18n | translate }}</span>\n <a class=\"link\" *ngIf=\"notification?.link\" (click)=\"handleLinkClick()\">{{ notification.link.i18n | translate }}</a>\n</div>\n", styles: [":host{position:relative;z-index:9999!important;display:block;width:100%;padding:1rem 2rem .5rem 2.75rem;border-width:.0625rem;border-style:solid;border-left-width:.25rem;margin-bottom:.5rem;background:#fff;box-shadow:0 0 .625rem #979797;font-size:.875rem;gap:.5rem;line-height:1rem}:host.qd-notification-info{border-color:#069}:host.qd-notification-info .icon,:host.qd-notification-info .title{color:#069}:host.qd-notification-warning{border-color:#ff9b00}:host.qd-notification-warning .icon,:host.qd-notification-warning .title{color:#ff9b00}:host.qd-notification-critical{border-color:#c70023}:host.qd-notification-critical .icon,:host.qd-notification-critical .title{color:#c70023}:host.qd-notification-success{border-color:#00813a}:host.qd-notification-success .icon,:host.qd-notification-success .title{color:#00813a}:host .icon{position:absolute;top:.9375rem;left:1rem;font-size:1.25rem}:host .closer{position:absolute;top:1rem;right:.75rem;color:#171717;cursor:pointer;font-size:.8125rem;font-weight:600}:host .title{margin-bottom:.375rem;font-weight:500}:host .message{margin-bottom:.25rem;white-space:pre-wrap}:host .link{margin-bottom:.375rem;margin-left:.375rem;color:#069;cursor:pointer}:host .link:hover{text-decoration:underline}:host:host-context(qd-snackbar) .link{display:block;margin-top:.375rem;margin-left:0}\n"] }]
17022
+ args: [{ selector: 'qd-notification', host: { class: 'qd-notification' }, standalone: false, template: "<qd-icon *ngIf=\"closeable\" [icon]=\"'timesLarge'\" class=\"closer\" (click)=\"remove()\"></qd-icon>\n<qd-icon [icon]=\"this.getIcon()\" class=\"icon\"></qd-icon>\n\n<ng-content select=\"qd-notification-content\"></ng-content>\n\n<div class=\"title\" *ngIf=\"notification?.title\">{{ notification.title.i18n | translate }}</div>\n<div class=\"message\">\n <span>{{ notification?.i18n | translate }}</span>\n <a class=\"link\" *ngIf=\"notification?.link\" (click)=\"handleLinkClick()\">{{ notification.link.i18n | translate }}</a>\n</div>\n", styles: [":host{position:relative;display:block;width:100%;padding:1rem 2rem .5rem 2.75rem;border-width:.0625rem;border-style:solid;border-left-width:.25rem;margin-bottom:.5rem;background:#fff;box-shadow:0 0 .625rem #979797;font-size:.875rem;gap:.5rem;line-height:1rem}:host.qd-notification-info{border-color:#069}:host.qd-notification-info .icon,:host.qd-notification-info .title{color:#069}:host.qd-notification-warning{border-color:#ff9b00}:host.qd-notification-warning .icon,:host.qd-notification-warning .title{color:#ff9b00}:host.qd-notification-critical{border-color:#c70023}:host.qd-notification-critical .icon,:host.qd-notification-critical .title{color:#c70023}:host.qd-notification-success{border-color:#00813a}:host.qd-notification-success .icon,:host.qd-notification-success .title{color:#00813a}:host .icon{position:absolute;top:.9375rem;left:1rem;font-size:1.25rem}:host .closer{position:absolute;top:1rem;right:.75rem;color:#171717;cursor:pointer;font-size:.8125rem;font-weight:600}:host .title{margin-bottom:.375rem;font-weight:500}:host .message{margin-bottom:.25rem;white-space:pre-wrap}:host .link{margin-bottom:.375rem;margin-left:.375rem;color:#069;cursor:pointer}:host .link:hover{text-decoration:underline}:host:host-context(qd-snackbar){z-index:9999!important}:host:host-context(qd-snackbar) .link{display:block;margin-top:.375rem;margin-left:0}\n"] }]
17023
17023
  }], propDecorators: { type: [{
17024
17024
  type: Input
17025
17025
  }], notification: [{
@@ -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 } = context;
28999
- const captured = formGroupManager.captureFormValues();
28998
+ const { formGroupManager, navigationInterceptor, destroyed$, onAfterSnapshot, scheduleFrameworkRebuildStop } = context;
28999
+ const captured = formGroupManager.createCurrentValuesSnapshot();
29000
29000
  let result$;
29001
29001
  navigationInterceptor.executeWithBypass(() => {
29002
29002
  const handlerResult = action.handler(values);
@@ -29004,7 +29004,9 @@ class QdPageCommitActionExecutor {
29004
29004
  result$ = handlerResult;
29005
29005
  });
29006
29006
  const applySuccess = () => {
29007
- formGroupManager.setFormGroupsSnapshot(captured);
29007
+ formGroupManager.updateSavedSnapshotFromValues(captured);
29008
+ formGroupManager.startAdoptingFrameworkRebuild();
29009
+ scheduleFrameworkRebuildStop?.();
29008
29010
  navigationInterceptor.executeWithBypass(() => {
29009
29011
  try {
29010
29012
  onAfterSnapshot?.();
@@ -29048,136 +29050,170 @@ class QdPageCommitActionExecutor {
29048
29050
  }
29049
29051
 
29050
29052
  /**
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.
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.
29111
29082
  */
29112
29083
  class QdFormGroupManagerService {
29113
29084
  _formGroups = new Map();
29114
29085
  _formGroupsSnapshot = new Map();
29086
+ _knownControlInstances = new Map();
29087
+ _isAdoptingFrameworkRebuild = false;
29088
+ _frameworkRebuildSub;
29115
29089
  _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
+ */
29116
29097
  setFormGroup(key, formGroup) {
29117
29098
  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
+ }
29118
29104
  this._formGroupsChanged$.next();
29119
29105
  }
29106
+ /**
29107
+ * Returns the form registered under the given key, or `undefined` if none is registered.
29108
+ */
29120
29109
  getFormGroup(key) {
29121
29110
  return this._formGroups.get(key);
29122
29111
  }
29112
+ /**
29113
+ * Returns all registered forms, keyed by their registration key.
29114
+ */
29123
29115
  getAllFormGroups() {
29124
29116
  return this._formGroups;
29125
29117
  }
29126
- tryRemoveFormGroup(key) {
29127
- this._formGroups.delete(key);
29128
- this._formGroupsChanged$.next();
29129
- }
29118
+ /**
29119
+ * Returns `true` if at least one form is registered.
29120
+ */
29130
29121
  hasFormGroups() {
29131
29122
  return this._formGroups.size > 0;
29132
29123
  }
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
+ */
29133
29136
  getAllValues() {
29134
29137
  const allValues = {};
29135
29138
  this._formGroups.forEach((formGroup, key) => (allValues[key] = formGroup.value));
29136
29139
  return allValues;
29137
29140
  }
29138
- isFormGroupKeyUnique(key) {
29139
- return !this._formGroups.has(key);
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();
29140
29151
  }
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
- }));
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();
29148
29170
  }
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
+ */
29149
29178
  $hasValuesChanged() {
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
- }));
29179
+ return this._formGroupsChanged$.pipe(observeOn(queueScheduler), switchMap$1(() => this.anyRegisteredFormChanged$()));
29160
29180
  }
29161
- takeFormGroupsSnapshot() {
29162
- this._formGroups.forEach((fg, key) => this._formGroupsSnapshot.set(key, fg.getRawValue()));
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
+ });
29163
29197
  }
29164
29198
  /**
29165
- * Captures a deep-cloned snapshot of all current form values without mutating internal state.
29199
+ * Creates an independent snapshot of the **current** form values and returns it — for the
29200
+ * **click moment** of an **asynchronous** save.
29166
29201
  *
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.
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.
29170
29206
  *
29171
29207
  * Throws if any form value is not structured-cloneable (functions, symbols, DOM nodes).
29172
29208
  */
29173
- captureFormValues() {
29209
+ createCurrentValuesSnapshot() {
29174
29210
  const captured = new Map();
29175
29211
  this._formGroups.forEach((fg, key) => {
29176
29212
  try {
29177
29213
  captured.set(key, structuredClone(fg.getRawValue()));
29178
29214
  }
29179
29215
  catch (err) {
29180
- throw new Error(`Quadrel Framework | QdFormGroupManager - captureFormValues() failed for group "${key}". ` +
29216
+ throw new Error(`Quadrel Framework | QdFormGroupManager - createCurrentValuesSnapshot() failed for group "${key}". ` +
29181
29217
  `Form values must be structured-cloneable. Non-cloneable values like functions, ` +
29182
29218
  `symbols, or DOM nodes are not supported. Original error: ${String(err)}`);
29183
29219
  }
@@ -29185,15 +29221,27 @@ class QdFormGroupManagerService {
29185
29221
  return captured;
29186
29222
  }
29187
29223
  /**
29188
- * Replaces the internal saved snapshot with the provided values.
29224
+ * Overwrites the saved snapshot with the **given** snapshot — on **success** of an
29225
+ * **asynchronous** save, using the snapshot taken earlier by `createCurrentValuesSnapshot()`.
29189
29226
  *
29190
- * Intended to be paired with `captureFormValues()` so that the snapshot can be set to the
29191
- * values present when a commit action was initiatedindependent of whether the user edited
29192
- * the form in the meantime.
29193
- */
29194
- setFormGroupsSnapshot(values) {
29195
- values.forEach((snapshot, key) => this._formGroupsSnapshot.set(key, snapshot));
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
+ });
29196
29238
  }
29239
+ /**
29240
+ * Resets every registered form back to its saved snapshot — used when the user discards changes.
29241
+ *
29242
+ * FormArrays are resized to match the snapshot length; pending async validators are cancelled
29243
+ * afterwards so no stale validation result survives the restore.
29244
+ */
29197
29245
  restoreFormGroupsFromSnapshot() {
29198
29246
  this._formGroups.forEach((fg, key) => {
29199
29247
  const snapshot = this._formGroupsSnapshot.get(key);
@@ -29204,68 +29252,171 @@ class QdFormGroupManagerService {
29204
29252
  this.cancelPendingAsyncValidation();
29205
29253
  }
29206
29254
  /**
29207
- * Cancels any in-flight async validators on all registered form groups.
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.
29208
29258
  *
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
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.
29212
29300
  */
29213
29301
  cancelPendingAsyncValidation() {
29214
29302
  this._formGroups.forEach(fg => {
29215
29303
  const pendingControls = this.collectPendingControls(fg);
29216
29304
  if (pendingControls.length === 0)
29217
29305
  return;
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
- }
29306
+ pendingControls.forEach(({ control }) => control.clearAsyncValidators());
29307
+ pendingControls.forEach(({ control }) => control.updateValueAndValidity({ onlySelf: true }));
29308
+ pendingControls.forEach(({ control, asyncValidator }) => control.setAsyncValidators(asyncValidator));
29227
29309
  });
29228
29310
  }
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
- }
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);
29241
29330
  });
29242
29331
  }
29243
- collectPendingControls(control) {
29244
- const result = [];
29245
- if (control instanceof FormGroup) {
29246
- Object.values(control.controls).forEach(c => result.push(...this.collectPendingControls(c)));
29247
- }
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 });
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);
29348
+ });
29349
+ }
29350
+ adoptRebuiltList(control, snapshot, name, isNewKey) {
29351
+ if (isNewKey) {
29352
+ this.adoptNewPristineControl(control, snapshot, name);
29353
+ return;
29253
29354
  }
29254
- return result;
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';
29255
29411
  }
29256
29412
  resetFormArrayToValues(array, newValues) {
29257
29413
  if (!Array.isArray(newValues))
29258
29414
  return;
29259
- const oldLen = array.length;
29260
- const newLen = newValues.length;
29261
- if (oldLen === 0 && newLen === 0)
29415
+ if (array.length === 0 && newValues.length === 0)
29262
29416
  return;
29263
- if (oldLen === 0 && newLen > 0)
29417
+ if (array.length === 0)
29264
29418
  return this.initFormArrayFromValues(array, newValues);
29265
- while (array.length < newLen)
29266
- array.push(this.deepCloneControl(array.at(0)));
29267
- while (array.length > newLen)
29268
- array.removeAt(array.length - 1);
29419
+ this.resizeFormArray(array, newValues.length);
29269
29420
  array.reset(newValues);
29270
29421
  }
29271
29422
  initFormArrayFromValues(array, values) {
@@ -29281,6 +29432,12 @@ class QdFormGroupManagerService {
29281
29432
  }
29282
29433
  array.reset(values);
29283
29434
  }
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
+ }
29284
29441
  deepCloneControl(control) {
29285
29442
  if (control instanceof FormControl)
29286
29443
  return new FormControl(undefined, control.validator, control.asyncValidator);
@@ -29298,6 +29455,19 @@ class QdFormGroupManagerService {
29298
29455
  areFormGroupsValid(fg) {
29299
29456
  return Object.values(fg.controls).every(c => (c.disabled && !c.validator && !c.asyncValidator ? true : c.valid));
29300
29457
  }
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
+ }
29301
29471
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdFormGroupManagerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
29302
29472
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdFormGroupManagerService });
29303
29473
  }
@@ -29326,7 +29496,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
29326
29496
  * - The page switches to view mode via `deactivate()`.
29327
29497
  *
29328
29498
  * The service has no concept of "pending actions" or bypass windows. Framework actions update the
29329
- * saved snapshot directly via `QdFormGroupManagerService.setFormGroupsSnapshot(...)`; cancel-discard
29499
+ * saved snapshot directly via `QdFormGroupManagerService.updateSavedSnapshotFromValues(...)`; cancel-discard
29330
29500
  * flows reset the form to the saved snapshot via `restoreFormGroupsFromSnapshot()`. Either way, the
29331
29501
  * interceptor only observes the resulting form state — there is no time window where a bypass is active.
29332
29502
  */
@@ -29476,6 +29646,7 @@ class QdPageObjectHeaderComponent {
29476
29646
  confirmationDialogService = inject(QdConfirmationDialogOpenerService);
29477
29647
  contextService = inject(QdContextService);
29478
29648
  resolverTriggerService = inject(QdResolverTriggerService);
29649
+ injector = inject(Injector);
29479
29650
  pageStoreService = inject(QdPageStoreService);
29480
29651
  config;
29481
29652
  hasNavigation = false;
@@ -29596,7 +29767,7 @@ class QdPageObjectHeaderComponent {
29596
29767
  this.setupResolverTrigger();
29597
29768
  this.updateCustomActions();
29598
29769
  this.subscribeToMetadataStream();
29599
- this.formGroupManagerService.takeFormGroupsSnapshot();
29770
+ this.formGroupManagerService.updateSavedSnapshotFromCurrentValues();
29600
29771
  this.initContexts();
29601
29772
  }
29602
29773
  ngOnChanges(changes) {
@@ -29621,7 +29792,7 @@ class QdPageObjectHeaderComponent {
29621
29792
  return this.config.headerFacets != null;
29622
29793
  }
29623
29794
  edit() {
29624
- this.formGroupManagerService.takeFormGroupsSnapshot();
29795
+ this.formGroupManagerService.updateSavedSnapshotFromCurrentValues();
29625
29796
  this.pageStoreService.toggleViewonly(false);
29626
29797
  if (this.editButton?.handler)
29627
29798
  this.editButton.handler();
@@ -29651,7 +29822,8 @@ class QdPageObjectHeaderComponent {
29651
29822
  onAfterSnapshot: () => {
29652
29823
  this.formGroupManagerService.cancelPendingAsyncValidation();
29653
29824
  this.pageStoreService.toggleViewonly(true);
29654
- }
29825
+ },
29826
+ scheduleFrameworkRebuildStop: () => afterNextRender(() => this.formGroupManagerService.stopAdoptingFrameworkRebuild(), { injector: this.injector })
29655
29827
  });
29656
29828
  }
29657
29829
  changeContext(context, selection, event) {
@@ -29679,7 +29851,7 @@ class QdPageObjectHeaderComponent {
29679
29851
  .pipe(takeUntil(this._destroyed$), filter(shouldTrigger => shouldTrigger), tap(() => this._isLoadingSubject.next(true)), switchMap(() => this.pageObjectResolver.resolve()), tap(objectData => {
29680
29852
  this._pageObjectDataSubject.next({ ...objectData, ...this._pendingMetadata });
29681
29853
  this._pendingMetadata = {};
29682
- }), tap(() => this._isLoadingSubject.next(false)), tap(() => this.formGroupManagerService.takeFormGroupsSnapshot()))
29854
+ }), tap(() => this._isLoadingSubject.next(false)), tap(() => this.formGroupManagerService.updateSavedSnapshotFromCurrentValues()))
29683
29855
  .subscribe();
29684
29856
  }
29685
29857
  initContexts() {
@@ -31455,6 +31627,7 @@ class QdPageComponent {
31455
31627
  bottomOffset$ = inject(QD_SAFE_BOTTOM_OFFSET, { optional: true });
31456
31628
  dialogRef = inject(DialogRef, { optional: true });
31457
31629
  navigationInterceptor = inject(QdPageNavigationInterceptorService);
31630
+ injector = inject(Injector);
31458
31631
  confirmationDialogService = inject(QdConfirmationDialogOpenerService);
31459
31632
  /**
31460
31633
  * This property defines the configuration for the QdPage component, including the page type,
@@ -31651,7 +31824,10 @@ class QdPageComponent {
31651
31824
  QdPageCommitActionExecutor.execute(action, values, {
31652
31825
  formGroupManager: this.formGroupManagerService,
31653
31826
  navigationInterceptor: this.navigationInterceptor,
31654
- destroyed$: this._destroyed$
31827
+ destroyed$: this._destroyed$,
31828
+ scheduleFrameworkRebuildStop: () => afterNextRender(() => this.formGroupManagerService.stopAdoptingFrameworkRebuild(), {
31829
+ injector: this.injector
31830
+ })
31655
31831
  });
31656
31832
  };
31657
31833
  }
@@ -31763,6 +31939,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
31763
31939
  * - 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.
31764
31940
  * - FormArrays are restored with dynamic resizing (length is aligned to snapshot before values are reset).
31765
31941
  *
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
+ *
31766
31946
  * ### **Page-Dialog (FullWidth) – Unsaved-Changes Close Contract**
31767
31947
  * - When a QdPage is embedded in a full-width page dialog, the directive’s registered form groups are also used to guard dialog closing.
31768
31948
  * - The page dialog can only close (e.g. via ESC or backdrop) if no unsaved changes are detected ($hasValuesChanged() is false).
@@ -31775,8 +31955,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
31775
31955
  * ```html
31776
31956
  * <qd-page [config]="pageConfig">
31777
31957
  * <qd-section [config]="sectionConfig">
31778
- * <qd-grid [formGroup]="myFormGroup" qdConnectFormStateToPage="myFormGroup">
31779
- * My Section
31958
+ * <qd-grid
31959
+ * qdConnectFormStateToPage="myFormGroup"
31960
+ * [formGroup]="myFormGroup">
31961
+ * My Section
31780
31962
  * </qd-grid>
31781
31963
  * </qd-section>
31782
31964
  * </qd-page>
@@ -31796,36 +31978,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
31796
31978
  class QdConnectFormStateToPageDirective {
31797
31979
  formGroupManagerService = inject(QdFormGroupManagerService, { optional: true });
31798
31980
  formGroupDirective = inject(FormGroupDirective, { host: true, optional: true });
31981
+ closeInitialBuildupHandle;
31799
31982
  /**
31800
31983
  * 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.
31801
31984
  */
31802
31985
  qdConnectFormStateToPage;
31803
31986
  ngOnInit() {
31804
- if (!this.isValid())
31805
- return;
31806
31987
  const formGroup = this.formGroupDirective?.control;
31807
- if (formGroup && this.qdConnectFormStateToPage) {
31808
- this.formGroupManagerService.setFormGroup(this.qdConnectFormStateToPage, formGroup);
31809
- }
31810
- }
31811
- ngOnDestroy() {
31812
- this.formGroupManagerService?.tryRemoveFormGroup(this.qdConnectFormStateToPage);
31813
- }
31814
- isValid() {
31815
- const hasFormGroup = this.formGroupDirective?.control;
31816
- if (!hasFormGroup) {
31988
+ if (!formGroup) {
31817
31989
  console.error('Quadrel Framework | QdConnectFormStateToPage - Either a [formGroup] binding or <qd-quick-edit> with a FormGroup is required.');
31818
- return false;
31990
+ return;
31819
31991
  }
31820
31992
  if (!this.formGroupManagerService) {
31821
31993
  console.error('Quadrel Framework | QdConnectFormStateToPage - The connector must be used within <qd-page>.');
31822
- return false;
31994
+ return;
31823
31995
  }
31824
31996
  if (!this.formGroupManagerService.isFormGroupKeyUnique(this.qdConnectFormStateToPage)) {
31825
31997
  console.error(`Quadrel Framework | QdConnectFormStateToPage - The FormGroup key "${this.qdConnectFormStateToPage}" is not unique.`);
31826
- return false;
31998
+ return;
31827
31999
  }
31828
- return true;
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));
31829
32011
  }
31830
32012
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdConnectFormStateToPageDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
31831
32013
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.18", type: QdConnectFormStateToPageDirective, isStandalone: false, selector: "[qdConnectFormStateToPage]", inputs: { qdConnectFormStateToPage: "qdConnectFormStateToPage" }, ngImport: i0 });