@quadrel-enterprise-ui/framework 20.27.2 → 20.27.3-beta.236.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
|
|
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
|
|
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']
|
|
@@ -11767,41 +11767,47 @@ class QdInputComponent {
|
|
|
11767
11767
|
if (input?.validity?.badInput)
|
|
11768
11768
|
return;
|
|
11769
11769
|
const value = event.target.value;
|
|
11770
|
-
if (this.inputType === 'number' && value && !this.
|
|
11771
|
-
this.control?.setErrors({
|
|
11772
|
-
...this.control.errors,
|
|
11773
|
-
invalidCharacters: this.numberInputService.invalidCharactersErrorKey
|
|
11774
|
-
});
|
|
11770
|
+
if (this.inputType === 'number' && value && !this.handleNumberInput(value))
|
|
11775
11771
|
return;
|
|
11776
|
-
}
|
|
11777
|
-
if (this.inputType === 'number' && value) {
|
|
11778
|
-
const valueWithoutGroups = value.split(this.numberInputService.groupSeparator).join('');
|
|
11779
|
-
const filtered = this.numberInputService.filterValue(valueWithoutGroups);
|
|
11780
|
-
const hasInvalidChars = filtered !== valueWithoutGroups;
|
|
11781
|
-
if (hasInvalidChars) {
|
|
11782
|
-
this._value = { ...this._value, value: filtered };
|
|
11783
|
-
if (this.control) {
|
|
11784
|
-
this.control.setErrors({
|
|
11785
|
-
...this.control.errors,
|
|
11786
|
-
invalidCharacters: this.numberInputService.invalidCharactersErrorKey
|
|
11787
|
-
});
|
|
11788
|
-
}
|
|
11789
|
-
this._isUserTyped = true;
|
|
11790
|
-
this._onTouch();
|
|
11791
|
-
this.emitValue();
|
|
11792
|
-
return;
|
|
11793
|
-
}
|
|
11794
|
-
if (this.control?.errors?.['invalidCharacters']) {
|
|
11795
|
-
const remainingErrors = Object.fromEntries(Object.entries(this.control.errors).filter(([key]) => key !== 'invalidCharacters'));
|
|
11796
|
-
this.control.setErrors(Object.keys(remainingErrors).length ? remainingErrors : null);
|
|
11797
|
-
}
|
|
11798
|
-
}
|
|
11799
11772
|
this._isUserTyped = true;
|
|
11800
11773
|
this._value = { ...this._value, value };
|
|
11774
|
+
this._displayValue = String(value ?? '');
|
|
11801
11775
|
this.loadOptionsForTypedValue(value);
|
|
11802
11776
|
this.emitValue();
|
|
11803
11777
|
this._onTouch();
|
|
11804
11778
|
}
|
|
11779
|
+
handleNumberInput(value) {
|
|
11780
|
+
if (!this.numberInputService.isValidNumber(value)) {
|
|
11781
|
+
this.setInvalidCharactersError();
|
|
11782
|
+
return false;
|
|
11783
|
+
}
|
|
11784
|
+
const valueWithoutGroups = value.split(this.numberInputService.groupSeparator).join('');
|
|
11785
|
+
const filtered = this.numberInputService.filterValue(valueWithoutGroups);
|
|
11786
|
+
if (filtered !== valueWithoutGroups) {
|
|
11787
|
+
this._value = { ...this._value, value: filtered };
|
|
11788
|
+
this.setInvalidCharactersError();
|
|
11789
|
+
this._isUserTyped = true;
|
|
11790
|
+
this._onTouch();
|
|
11791
|
+
this.emitValue();
|
|
11792
|
+
return false;
|
|
11793
|
+
}
|
|
11794
|
+
this.clearInvalidCharactersError();
|
|
11795
|
+
return true;
|
|
11796
|
+
}
|
|
11797
|
+
setInvalidCharactersError() {
|
|
11798
|
+
if (!this.control)
|
|
11799
|
+
return;
|
|
11800
|
+
this.control.setErrors({
|
|
11801
|
+
...this.control.errors,
|
|
11802
|
+
invalidCharacters: this.numberInputService.invalidCharactersErrorKey
|
|
11803
|
+
});
|
|
11804
|
+
}
|
|
11805
|
+
clearInvalidCharactersError() {
|
|
11806
|
+
if (!this.control?.errors?.['invalidCharacters'])
|
|
11807
|
+
return;
|
|
11808
|
+
const remainingErrors = Object.fromEntries(Object.entries(this.control.errors).filter(([key]) => key !== 'invalidCharacters'));
|
|
11809
|
+
this.control.setErrors(Object.keys(remainingErrors).length ? remainingErrors : null);
|
|
11810
|
+
}
|
|
11805
11811
|
handleOptionSelected(option) {
|
|
11806
11812
|
this._value = { ...this._value, value: option.i18n };
|
|
11807
11813
|
this.popover?.close();
|
|
@@ -17015,11 +17021,11 @@ class QdNotificationComponent {
|
|
|
17015
17021
|
return !!this.element.nativeElement.closest('qd-snackbar');
|
|
17016
17022
|
}
|
|
17017
17023
|
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;
|
|
17024
|
+
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
17025
|
}
|
|
17020
17026
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdNotificationComponent, decorators: [{
|
|
17021
17027
|
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;
|
|
17028
|
+
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
17029
|
}], propDecorators: { type: [{
|
|
17024
17030
|
type: Input
|
|
17025
17031
|
}], notification: [{
|
|
@@ -28995,8 +29001,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
|
|
|
28995
29001
|
|
|
28996
29002
|
class QdPageCommitActionExecutor {
|
|
28997
29003
|
static execute(action, values, context) {
|
|
28998
|
-
const { formGroupManager, navigationInterceptor, destroyed$, onAfterSnapshot } = context;
|
|
28999
|
-
const captured = formGroupManager.
|
|
29004
|
+
const { formGroupManager, navigationInterceptor, destroyed$, onAfterSnapshot, scheduleFrameworkRebuildStop } = context;
|
|
29005
|
+
const captured = formGroupManager.createCurrentValuesSnapshot();
|
|
29000
29006
|
let result$;
|
|
29001
29007
|
navigationInterceptor.executeWithBypass(() => {
|
|
29002
29008
|
const handlerResult = action.handler(values);
|
|
@@ -29004,7 +29010,9 @@ class QdPageCommitActionExecutor {
|
|
|
29004
29010
|
result$ = handlerResult;
|
|
29005
29011
|
});
|
|
29006
29012
|
const applySuccess = () => {
|
|
29007
|
-
formGroupManager.
|
|
29013
|
+
formGroupManager.updateSavedSnapshotFromValues(captured);
|
|
29014
|
+
formGroupManager.startAdoptingFrameworkRebuild();
|
|
29015
|
+
scheduleFrameworkRebuildStop?.();
|
|
29008
29016
|
navigationInterceptor.executeWithBypass(() => {
|
|
29009
29017
|
try {
|
|
29010
29018
|
onAfterSnapshot?.();
|
|
@@ -29048,136 +29056,170 @@ class QdPageCommitActionExecutor {
|
|
|
29048
29056
|
}
|
|
29049
29057
|
|
|
29050
29058
|
/**
|
|
29051
|
-
*
|
|
29052
|
-
*
|
|
29053
|
-
*
|
|
29054
|
-
*
|
|
29055
|
-
*
|
|
29056
|
-
*
|
|
29057
|
-
* -
|
|
29058
|
-
* -
|
|
29059
|
-
* -
|
|
29060
|
-
* -
|
|
29061
|
-
*
|
|
29062
|
-
* ####
|
|
29063
|
-
*
|
|
29064
|
-
*
|
|
29065
|
-
*
|
|
29066
|
-
*
|
|
29067
|
-
*
|
|
29068
|
-
*
|
|
29069
|
-
*
|
|
29070
|
-
*
|
|
29071
|
-
*
|
|
29072
|
-
*
|
|
29073
|
-
*
|
|
29074
|
-
* **
|
|
29075
|
-
*
|
|
29076
|
-
*
|
|
29077
|
-
*
|
|
29078
|
-
*
|
|
29079
|
-
*
|
|
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.
|
|
29059
|
+
* Central tracker for all reactive forms on a single QdPage.
|
|
29060
|
+
*
|
|
29061
|
+
* Its main job is to answer one question: **has the user changed anything since the last save?**
|
|
29062
|
+
* The page uses that answer to warn before leaving with unsaved changes.
|
|
29063
|
+
*
|
|
29064
|
+
* #### What it does
|
|
29065
|
+
* - **Registry** — holds every form of the page and gives access to it.
|
|
29066
|
+
* - **Change detection** — keeps a snapshot of the saved values and compares the live form against it.
|
|
29067
|
+
* - **Restore** — resets the forms back to the snapshot when the user discards changes.
|
|
29068
|
+
* - **Validity** — reports whether all forms are valid and can cancel in-flight async validators.
|
|
29069
|
+
*
|
|
29070
|
+
* #### Adopting framework rebuilds
|
|
29071
|
+
* After a save, the consumer often rebuilds the form itself — a `FormArray` cleared and pushed
|
|
29072
|
+
* again, or a control replaced via `setControl`. A plain value compare would wrongly read that as a
|
|
29073
|
+
* user edit. To prevent that:
|
|
29074
|
+
*
|
|
29075
|
+
* - **What it does** — right after each save or snapshot write, the framework briefly *adopts its
|
|
29076
|
+
* own rebuild* (`startAdoptingFrameworkRebuild`) and folds those control replacements into the
|
|
29077
|
+
* snapshot, so they count as already saved.
|
|
29078
|
+
* - **Why it is needed** — otherwise a rebuilt value (text `"99"` becoming number `99`) would look
|
|
29079
|
+
* like an unsaved change.
|
|
29080
|
+
* - **When it ends** — the page stops adopting once the rebuild has rendered
|
|
29081
|
+
* (`stopAdoptingFrameworkRebuild`).
|
|
29082
|
+
* - **User edits stay unsaved** — they always happen after that window, so they are still reported.
|
|
29083
|
+
*
|
|
29084
|
+
* #### Notes
|
|
29085
|
+
* - Snapshots store only **raw values** (`getRawValue()`), never control instances — cheap to keep and compare.
|
|
29086
|
+
* - Restoring a `FormArray` adjusts its length and resets its contents; it never replaces the array instance.
|
|
29087
|
+
* - No structural information is stored in the snapshot, only data.
|
|
29111
29088
|
*/
|
|
29112
29089
|
class QdFormGroupManagerService {
|
|
29113
29090
|
_formGroups = new Map();
|
|
29114
29091
|
_formGroupsSnapshot = new Map();
|
|
29092
|
+
_knownControlInstances = new Map();
|
|
29093
|
+
_isAdoptingFrameworkRebuild = false;
|
|
29094
|
+
_frameworkRebuildSub;
|
|
29115
29095
|
_formGroupsChanged$ = new BehaviorSubject(undefined);
|
|
29096
|
+
_keysInInitialBuildup = new Set();
|
|
29097
|
+
/**
|
|
29098
|
+
* Registers a form under a key so the manager can track it.
|
|
29099
|
+
*
|
|
29100
|
+
* On first registration it also seeds the saved snapshot from the current values, so a freshly
|
|
29101
|
+
* registered form counts as unchanged until the user actually edits it.
|
|
29102
|
+
*/
|
|
29116
29103
|
setFormGroup(key, formGroup) {
|
|
29117
29104
|
this._formGroups.set(key, formGroup);
|
|
29105
|
+
if (!this._formGroupsSnapshot.has(key)) {
|
|
29106
|
+
this._formGroupsSnapshot.set(key, formGroup.getRawValue());
|
|
29107
|
+
this.rememberControlInstances(key, formGroup);
|
|
29108
|
+
this._keysInInitialBuildup.add(key);
|
|
29109
|
+
}
|
|
29118
29110
|
this._formGroupsChanged$.next();
|
|
29119
29111
|
}
|
|
29112
|
+
/**
|
|
29113
|
+
* Returns the form registered under the given key, or `undefined` if none is registered.
|
|
29114
|
+
*/
|
|
29120
29115
|
getFormGroup(key) {
|
|
29121
29116
|
return this._formGroups.get(key);
|
|
29122
29117
|
}
|
|
29118
|
+
/**
|
|
29119
|
+
* Returns all registered forms, keyed by their registration key.
|
|
29120
|
+
*/
|
|
29123
29121
|
getAllFormGroups() {
|
|
29124
29122
|
return this._formGroups;
|
|
29125
29123
|
}
|
|
29126
|
-
|
|
29127
|
-
|
|
29128
|
-
|
|
29129
|
-
}
|
|
29124
|
+
/**
|
|
29125
|
+
* Returns `true` if at least one form is registered.
|
|
29126
|
+
*/
|
|
29130
29127
|
hasFormGroups() {
|
|
29131
29128
|
return this._formGroups.size > 0;
|
|
29132
29129
|
}
|
|
29130
|
+
/**
|
|
29131
|
+
* Returns `true` if no form is registered under the given key yet.
|
|
29132
|
+
*/
|
|
29133
|
+
isFormGroupKeyUnique(key) {
|
|
29134
|
+
return !this._formGroups.has(key);
|
|
29135
|
+
}
|
|
29136
|
+
/**
|
|
29137
|
+
* Returns the current `.value` of every registered form, keyed by registration key.
|
|
29138
|
+
*
|
|
29139
|
+
* Uses Angular's `.value`, so disabled controls are omitted. Use the snapshot methods when you
|
|
29140
|
+
* need the raw values including disabled controls.
|
|
29141
|
+
*/
|
|
29133
29142
|
getAllValues() {
|
|
29134
29143
|
const allValues = {};
|
|
29135
29144
|
this._formGroups.forEach((formGroup, key) => (allValues[key] = formGroup.value));
|
|
29136
29145
|
return allValues;
|
|
29137
29146
|
}
|
|
29138
|
-
|
|
29139
|
-
|
|
29147
|
+
/**
|
|
29148
|
+
* Unregisters the form under the given key and drops its instance tracking.
|
|
29149
|
+
*
|
|
29150
|
+
* Does nothing if no form is registered for the key.
|
|
29151
|
+
*/
|
|
29152
|
+
tryRemoveFormGroup(key) {
|
|
29153
|
+
this._formGroups.delete(key);
|
|
29154
|
+
this._knownControlInstances.delete(key);
|
|
29155
|
+
this._keysInInitialBuildup.delete(key);
|
|
29156
|
+
this._formGroupsChanged$.next();
|
|
29140
29157
|
}
|
|
29141
|
-
|
|
29142
|
-
|
|
29143
|
-
|
|
29144
|
-
|
|
29145
|
-
|
|
29146
|
-
|
|
29147
|
-
|
|
29158
|
+
/**
|
|
29159
|
+
* Ends the initial-buildup phase for a key and fixes its baseline from the form as it is now.
|
|
29160
|
+
*
|
|
29161
|
+
* During that phase the key never counts as changed, so a form filled right after registration
|
|
29162
|
+
* (e.g. by an `effect`) is not flagged as unsaved. This stores the current values as the baseline,
|
|
29163
|
+
* so later edits are measured against the filled form.
|
|
29164
|
+
*
|
|
29165
|
+
* Does nothing if the key is not in its initial-buildup phase.
|
|
29166
|
+
*/
|
|
29167
|
+
endInitialBuildup(key) {
|
|
29168
|
+
if (!this._keysInInitialBuildup.delete(key))
|
|
29169
|
+
return;
|
|
29170
|
+
const formGroup = this._formGroups.get(key);
|
|
29171
|
+
if (formGroup) {
|
|
29172
|
+
this._formGroupsSnapshot.set(key, formGroup.getRawValue());
|
|
29173
|
+
this.rememberControlInstances(key, formGroup);
|
|
29174
|
+
}
|
|
29175
|
+
this._formGroupsChanged$.next();
|
|
29148
29176
|
}
|
|
29177
|
+
/**
|
|
29178
|
+
* Emits whether any registered form differs from its saved snapshot — i.e. whether the user has
|
|
29179
|
+
* unsaved changes.
|
|
29180
|
+
*
|
|
29181
|
+
* Framework-driven rebuilds are folded into the snapshot first while the framework is adopting
|
|
29182
|
+
* them (see the class docs), so they are not reported as changes.
|
|
29183
|
+
*/
|
|
29149
29184
|
$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
|
-
}));
|
|
29185
|
+
return this._formGroupsChanged$.pipe(observeOn(queueScheduler), switchMap$1(() => this.anyRegisteredFormChanged$()));
|
|
29160
29186
|
}
|
|
29161
|
-
|
|
29162
|
-
|
|
29187
|
+
/**
|
|
29188
|
+
* Overwrites the saved snapshot with the **current** form values — for a **synchronous** save
|
|
29189
|
+
* that is already finished.
|
|
29190
|
+
*
|
|
29191
|
+
* - Stores each form's raw values (`getRawValue`) as the new saved snapshot.
|
|
29192
|
+
* - Takes them **directly, without a deep copy** — synchronous means no edits can slip in
|
|
29193
|
+
* between, so the cheap path is safe (only the async path needs the copy).
|
|
29194
|
+
* - Afterwards the current state counts as saved; later edits are measured against it.
|
|
29195
|
+
*
|
|
29196
|
+
* For an **asynchronous** save, use `createCurrentValuesSnapshot()` + `updateSavedSnapshotFromValues()`.
|
|
29197
|
+
*/
|
|
29198
|
+
updateSavedSnapshotFromCurrentValues() {
|
|
29199
|
+
this._formGroups.forEach((fg, key) => {
|
|
29200
|
+
this._formGroupsSnapshot.set(key, fg.getRawValue());
|
|
29201
|
+
this.rememberControlInstances(key, fg);
|
|
29202
|
+
});
|
|
29163
29203
|
}
|
|
29164
29204
|
/**
|
|
29165
|
-
*
|
|
29205
|
+
* Creates an independent snapshot of the **current** form values and returns it — for the
|
|
29206
|
+
* **click moment** of an **asynchronous** save.
|
|
29166
29207
|
*
|
|
29167
|
-
*
|
|
29168
|
-
*
|
|
29169
|
-
* while
|
|
29208
|
+
* - Deep-copies each form's raw values (`structuredClone`), detached from the live form.
|
|
29209
|
+
* - Writes **nothing** internally — the saved snapshot stays unchanged.
|
|
29210
|
+
* - Freezes the click-time state: later edits (while the backend runs) do not change the copy.
|
|
29211
|
+
* - Hand the result to `updateSavedSnapshotFromValues()` once the save succeeds.
|
|
29170
29212
|
*
|
|
29171
29213
|
* Throws if any form value is not structured-cloneable (functions, symbols, DOM nodes).
|
|
29172
29214
|
*/
|
|
29173
|
-
|
|
29215
|
+
createCurrentValuesSnapshot() {
|
|
29174
29216
|
const captured = new Map();
|
|
29175
29217
|
this._formGroups.forEach((fg, key) => {
|
|
29176
29218
|
try {
|
|
29177
29219
|
captured.set(key, structuredClone(fg.getRawValue()));
|
|
29178
29220
|
}
|
|
29179
29221
|
catch (err) {
|
|
29180
|
-
throw new Error(`Quadrel Framework | QdFormGroupManager -
|
|
29222
|
+
throw new Error(`Quadrel Framework | QdFormGroupManager - createCurrentValuesSnapshot() failed for group "${key}". ` +
|
|
29181
29223
|
`Form values must be structured-cloneable. Non-cloneable values like functions, ` +
|
|
29182
29224
|
`symbols, or DOM nodes are not supported. Original error: ${String(err)}`);
|
|
29183
29225
|
}
|
|
@@ -29185,15 +29227,27 @@ class QdFormGroupManagerService {
|
|
|
29185
29227
|
return captured;
|
|
29186
29228
|
}
|
|
29187
29229
|
/**
|
|
29188
|
-
*
|
|
29230
|
+
* Overwrites the saved snapshot with the **given** snapshot — on **success** of an
|
|
29231
|
+
* **asynchronous** save, using the snapshot taken earlier by `createCurrentValuesSnapshot()`.
|
|
29189
29232
|
*
|
|
29190
|
-
*
|
|
29191
|
-
*
|
|
29192
|
-
*
|
|
29193
|
-
*/
|
|
29194
|
-
|
|
29195
|
-
|
|
29233
|
+
* - Stores the passed snapshot as the new saved state.
|
|
29234
|
+
* - Uses the click-time snapshot, **not** the current form — so edits the user made while the
|
|
29235
|
+
* backend was running stay correctly marked as unsaved.
|
|
29236
|
+
*/
|
|
29237
|
+
updateSavedSnapshotFromValues(snapshot) {
|
|
29238
|
+
snapshot.forEach((values, key) => {
|
|
29239
|
+
this._formGroupsSnapshot.set(key, values);
|
|
29240
|
+
const formGroup = this._formGroups.get(key);
|
|
29241
|
+
if (formGroup)
|
|
29242
|
+
this.rememberControlInstances(key, formGroup);
|
|
29243
|
+
});
|
|
29196
29244
|
}
|
|
29245
|
+
/**
|
|
29246
|
+
* Resets every registered form back to its saved snapshot — used when the user discards changes.
|
|
29247
|
+
*
|
|
29248
|
+
* FormArrays are resized to match the snapshot length; pending async validators are cancelled
|
|
29249
|
+
* afterwards so no stale validation result survives the restore.
|
|
29250
|
+
*/
|
|
29197
29251
|
restoreFormGroupsFromSnapshot() {
|
|
29198
29252
|
this._formGroups.forEach((fg, key) => {
|
|
29199
29253
|
const snapshot = this._formGroupsSnapshot.get(key);
|
|
@@ -29204,68 +29258,171 @@ class QdFormGroupManagerService {
|
|
|
29204
29258
|
this.cancelPendingAsyncValidation();
|
|
29205
29259
|
}
|
|
29206
29260
|
/**
|
|
29207
|
-
*
|
|
29261
|
+
* Starts adopting framework rebuilds into the snapshot. While active, a framework-driven control
|
|
29262
|
+
* replacement (a `setControl` on a leaf or a whole `FormArray` rebuilt via clear/push) is folded
|
|
29263
|
+
* into the saved snapshot, so it is not reported as an unsaved change.
|
|
29264
|
+
*
|
|
29265
|
+
* User edits never fall in this short window, so they stay dirty. The framework starts it after a
|
|
29266
|
+
* successful commit and after every snapshot write; the page stops it deterministically once the
|
|
29267
|
+
* rebuild has rendered.
|
|
29268
|
+
*/
|
|
29269
|
+
startAdoptingFrameworkRebuild() {
|
|
29270
|
+
this._isAdoptingFrameworkRebuild = true;
|
|
29271
|
+
this._frameworkRebuildSub?.unsubscribe();
|
|
29272
|
+
const valueStreams = Array.from(this._formGroups.values()).map(fg => fg.valueChanges);
|
|
29273
|
+
if (valueStreams.length)
|
|
29274
|
+
this._frameworkRebuildSub = merge(...valueStreams).subscribe(() => this.adoptFrameworkRebuild());
|
|
29275
|
+
this.adoptFrameworkRebuild();
|
|
29276
|
+
}
|
|
29277
|
+
/**
|
|
29278
|
+
* Stops adopting framework rebuilds. After this, control replacements count as user edits again.
|
|
29279
|
+
*
|
|
29280
|
+
* The page calls this once the framework rebuild has rendered (via `afterNextRender`).
|
|
29281
|
+
*/
|
|
29282
|
+
stopAdoptingFrameworkRebuild() {
|
|
29283
|
+
this._isAdoptingFrameworkRebuild = false;
|
|
29284
|
+
this._frameworkRebuildSub?.unsubscribe();
|
|
29285
|
+
}
|
|
29286
|
+
/**
|
|
29287
|
+
* Emits whether every registered form is currently valid.
|
|
29208
29288
|
*
|
|
29209
|
-
*
|
|
29210
|
-
|
|
29211
|
-
|
|
29289
|
+
* A disabled control without validators is treated as valid.
|
|
29290
|
+
*/
|
|
29291
|
+
$areFormGroupsValid() {
|
|
29292
|
+
return this._formGroupsChanged$.pipe(observeOn(queueScheduler), switchMap$1(() => {
|
|
29293
|
+
if (!this.hasFormGroups())
|
|
29294
|
+
return of(false);
|
|
29295
|
+
const obs = Array.from(this._formGroups.values()).map(fg => fg.statusChanges.pipe(startWith(fg.status), map(() => this.areFormGroupsValid(fg))));
|
|
29296
|
+
return combineLatest(obs).pipe(map(valids => valids.every(Boolean)));
|
|
29297
|
+
}));
|
|
29298
|
+
}
|
|
29299
|
+
/**
|
|
29300
|
+
* Cancels every in-flight (PENDING) async validator across all registered forms, so no stale
|
|
29301
|
+
* validation result can land after a restore or a commit.
|
|
29302
|
+
*
|
|
29303
|
+
* The async validators themselves are kept — only the pending run is dropped. Each phase runs
|
|
29304
|
+
* over **all** pending controls before the next starts (detach → re-evaluate sync-only, no
|
|
29305
|
+
* cascade → re-attach), so validity is never recomputed while a run is still settling.
|
|
29212
29306
|
*/
|
|
29213
29307
|
cancelPendingAsyncValidation() {
|
|
29214
29308
|
this._formGroups.forEach(fg => {
|
|
29215
29309
|
const pendingControls = this.collectPendingControls(fg);
|
|
29216
29310
|
if (pendingControls.length === 0)
|
|
29217
29311
|
return;
|
|
29218
|
-
|
|
29219
|
-
|
|
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
|
-
}
|
|
29312
|
+
pendingControls.forEach(({ control }) => control.clearAsyncValidators());
|
|
29313
|
+
pendingControls.forEach(({ control }) => control.updateValueAndValidity({ onlySelf: true }));
|
|
29314
|
+
pendingControls.forEach(({ control, asyncValidator }) => control.setAsyncValidators(asyncValidator));
|
|
29227
29315
|
});
|
|
29228
29316
|
}
|
|
29229
|
-
|
|
29230
|
-
|
|
29231
|
-
|
|
29232
|
-
|
|
29233
|
-
|
|
29234
|
-
|
|
29235
|
-
|
|
29236
|
-
|
|
29237
|
-
|
|
29238
|
-
|
|
29239
|
-
|
|
29240
|
-
|
|
29317
|
+
anyRegisteredFormChanged$() {
|
|
29318
|
+
if (!this.hasFormGroups())
|
|
29319
|
+
return of(false);
|
|
29320
|
+
const perForm = Array.from(this._formGroups.entries()).map(([key, fg]) => fg.valueChanges.pipe(startWith(null), map(() => this.hasFormChanged(key, fg))));
|
|
29321
|
+
return combineLatest(perForm).pipe(map(flags => flags.some(Boolean)));
|
|
29322
|
+
}
|
|
29323
|
+
hasFormChanged(key, fg) {
|
|
29324
|
+
if (this._keysInInitialBuildup.has(key))
|
|
29325
|
+
return false;
|
|
29326
|
+
const snapshot = this._formGroupsSnapshot.get(key);
|
|
29327
|
+
if (snapshot)
|
|
29328
|
+
this.adoptRebuildInGroup(key, fg, snapshot);
|
|
29329
|
+
return snapshot ? !isEqual$1(fg.getRawValue(), snapshot) : true;
|
|
29330
|
+
}
|
|
29331
|
+
adoptFrameworkRebuild() {
|
|
29332
|
+
this._formGroups.forEach((fg, key) => {
|
|
29333
|
+
const snapshot = this._formGroupsSnapshot.get(key);
|
|
29334
|
+
if (snapshot)
|
|
29335
|
+
this.adoptRebuildInGroup(key, fg, snapshot);
|
|
29241
29336
|
});
|
|
29242
29337
|
}
|
|
29243
|
-
|
|
29244
|
-
const
|
|
29245
|
-
|
|
29246
|
-
|
|
29247
|
-
|
|
29248
|
-
|
|
29249
|
-
|
|
29250
|
-
|
|
29251
|
-
|
|
29252
|
-
|
|
29338
|
+
adoptRebuildInGroup(key, formGroup, snapshot) {
|
|
29339
|
+
const tracked = this._knownControlInstances.get(key) ?? new Map();
|
|
29340
|
+
this.adoptRebuiltControls(formGroup, snapshot, tracked, '');
|
|
29341
|
+
this._knownControlInstances.set(key, tracked);
|
|
29342
|
+
}
|
|
29343
|
+
adoptRebuiltControls(group, snapshot, tracked, prefix) {
|
|
29344
|
+
Object.entries(group.controls).forEach(([name, control]) => {
|
|
29345
|
+
const path = prefix ? `${prefix}.${name}` : name;
|
|
29346
|
+
const isNewKey = !(name in snapshot);
|
|
29347
|
+
if (control instanceof FormArray)
|
|
29348
|
+
this.adoptRebuiltList(control, snapshot, name, isNewKey);
|
|
29349
|
+
else if (control instanceof FormGroup)
|
|
29350
|
+
this.adoptRebuiltGroup(control, snapshot, name, isNewKey, tracked, path);
|
|
29351
|
+
else
|
|
29352
|
+
this.adoptRebuiltField(control, snapshot, name, isNewKey, tracked.get(path));
|
|
29353
|
+
tracked.set(path, control);
|
|
29354
|
+
});
|
|
29355
|
+
}
|
|
29356
|
+
adoptRebuiltList(control, snapshot, name, isNewKey) {
|
|
29357
|
+
if (isNewKey) {
|
|
29358
|
+
this.adoptNewPristineControl(control, snapshot, name);
|
|
29359
|
+
return;
|
|
29253
29360
|
}
|
|
29254
|
-
|
|
29361
|
+
if (!this.isAdoptableArrayRebuild(control, snapshot, name))
|
|
29362
|
+
return;
|
|
29363
|
+
const snapshotArray = snapshot[name];
|
|
29364
|
+
const currentArray = control.getRawValue();
|
|
29365
|
+
if (currentArray.length === snapshotArray.length && !isEqual$1(currentArray, snapshotArray))
|
|
29366
|
+
snapshot[name] = currentArray;
|
|
29367
|
+
}
|
|
29368
|
+
adoptRebuiltGroup(group, snapshot, name, isNewKey, tracked, path) {
|
|
29369
|
+
if (isNewKey)
|
|
29370
|
+
this.adoptNewPristineControl(group, snapshot, name);
|
|
29371
|
+
const nested = snapshot[name];
|
|
29372
|
+
if (this.isPlainObject(nested))
|
|
29373
|
+
this.adoptRebuiltControls(group, nested, tracked, path);
|
|
29374
|
+
}
|
|
29375
|
+
adoptRebuiltField(control, snapshot, name, isNewKey, known) {
|
|
29376
|
+
if (isNewKey)
|
|
29377
|
+
this.adoptNewPristineControl(control, snapshot, name);
|
|
29378
|
+
else if (this.isReplacedPristineControl(control, known))
|
|
29379
|
+
snapshot[name] = control.getRawValue();
|
|
29380
|
+
}
|
|
29381
|
+
adoptNewPristineControl(control, snapshot, name) {
|
|
29382
|
+
if (control.pristine)
|
|
29383
|
+
snapshot[name] = control.getRawValue();
|
|
29384
|
+
}
|
|
29385
|
+
isAdoptableArrayRebuild(control, snapshot, name) {
|
|
29386
|
+
return this._isAdoptingFrameworkRebuild && control.pristine && Array.isArray(snapshot[name]);
|
|
29387
|
+
}
|
|
29388
|
+
isReplacedPristineControl(control, known) {
|
|
29389
|
+
return this._isAdoptingFrameworkRebuild && !!known && known !== control && control.pristine;
|
|
29390
|
+
}
|
|
29391
|
+
rememberControlInstances(key, formGroup) {
|
|
29392
|
+
const tracked = new Map();
|
|
29393
|
+
this.collectControlInstances(formGroup, tracked, '');
|
|
29394
|
+
this._knownControlInstances.set(key, tracked);
|
|
29395
|
+
}
|
|
29396
|
+
collectControlInstances(group, tracked, prefix) {
|
|
29397
|
+
Object.entries(group.controls).forEach(([name, control]) => {
|
|
29398
|
+
const path = prefix ? `${prefix}.${name}` : name;
|
|
29399
|
+
tracked.set(path, control);
|
|
29400
|
+
if (control instanceof FormGroup)
|
|
29401
|
+
this.collectControlInstances(control, tracked, path);
|
|
29402
|
+
});
|
|
29403
|
+
}
|
|
29404
|
+
restoreFormGroup(fg, snapshot) {
|
|
29405
|
+
Object.entries(fg.controls).forEach(([ctrlKey, ctrl]) => this.restoreControl(ctrl, snapshot[ctrlKey]));
|
|
29406
|
+
}
|
|
29407
|
+
restoreControl(ctrl, newValue) {
|
|
29408
|
+
if (ctrl instanceof FormArray && Array.isArray(newValue))
|
|
29409
|
+
this.resetFormArrayToValues(ctrl, newValue);
|
|
29410
|
+
else if (ctrl instanceof FormGroup && this.isPlainObject(newValue))
|
|
29411
|
+
this.restoreFormGroup(ctrl, newValue);
|
|
29412
|
+
else
|
|
29413
|
+
ctrl.reset(newValue);
|
|
29414
|
+
}
|
|
29415
|
+
isPlainObject(value) {
|
|
29416
|
+
return !!value && typeof value === 'object';
|
|
29255
29417
|
}
|
|
29256
29418
|
resetFormArrayToValues(array, newValues) {
|
|
29257
29419
|
if (!Array.isArray(newValues))
|
|
29258
29420
|
return;
|
|
29259
|
-
|
|
29260
|
-
const newLen = newValues.length;
|
|
29261
|
-
if (oldLen === 0 && newLen === 0)
|
|
29421
|
+
if (array.length === 0 && newValues.length === 0)
|
|
29262
29422
|
return;
|
|
29263
|
-
if (
|
|
29423
|
+
if (array.length === 0)
|
|
29264
29424
|
return this.initFormArrayFromValues(array, newValues);
|
|
29265
|
-
|
|
29266
|
-
array.push(this.deepCloneControl(array.at(0)));
|
|
29267
|
-
while (array.length > newLen)
|
|
29268
|
-
array.removeAt(array.length - 1);
|
|
29425
|
+
this.resizeFormArray(array, newValues.length);
|
|
29269
29426
|
array.reset(newValues);
|
|
29270
29427
|
}
|
|
29271
29428
|
initFormArrayFromValues(array, values) {
|
|
@@ -29281,6 +29438,12 @@ class QdFormGroupManagerService {
|
|
|
29281
29438
|
}
|
|
29282
29439
|
array.reset(values);
|
|
29283
29440
|
}
|
|
29441
|
+
resizeFormArray(array, length) {
|
|
29442
|
+
while (array.length < length)
|
|
29443
|
+
array.push(this.deepCloneControl(array.at(0)));
|
|
29444
|
+
while (array.length > length)
|
|
29445
|
+
array.removeAt(array.length - 1);
|
|
29446
|
+
}
|
|
29284
29447
|
deepCloneControl(control) {
|
|
29285
29448
|
if (control instanceof FormControl)
|
|
29286
29449
|
return new FormControl(undefined, control.validator, control.asyncValidator);
|
|
@@ -29298,6 +29461,19 @@ class QdFormGroupManagerService {
|
|
|
29298
29461
|
areFormGroupsValid(fg) {
|
|
29299
29462
|
return Object.values(fg.controls).every(c => (c.disabled && !c.validator && !c.asyncValidator ? true : c.valid));
|
|
29300
29463
|
}
|
|
29464
|
+
collectPendingControls(control) {
|
|
29465
|
+
const result = this.childControlsOf(control).flatMap(child => this.collectPendingControls(child));
|
|
29466
|
+
if (control.status === 'PENDING')
|
|
29467
|
+
result.push({ control, asyncValidator: control.asyncValidator });
|
|
29468
|
+
return result;
|
|
29469
|
+
}
|
|
29470
|
+
childControlsOf(control) {
|
|
29471
|
+
if (control instanceof FormGroup)
|
|
29472
|
+
return Object.values(control.controls);
|
|
29473
|
+
if (control instanceof FormArray)
|
|
29474
|
+
return control.controls;
|
|
29475
|
+
return [];
|
|
29476
|
+
}
|
|
29301
29477
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdFormGroupManagerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
29302
29478
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdFormGroupManagerService });
|
|
29303
29479
|
}
|
|
@@ -29326,7 +29502,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
|
|
|
29326
29502
|
* - The page switches to view mode via `deactivate()`.
|
|
29327
29503
|
*
|
|
29328
29504
|
* The service has no concept of "pending actions" or bypass windows. Framework actions update the
|
|
29329
|
-
* saved snapshot directly via `QdFormGroupManagerService.
|
|
29505
|
+
* saved snapshot directly via `QdFormGroupManagerService.updateSavedSnapshotFromValues(...)`; cancel-discard
|
|
29330
29506
|
* flows reset the form to the saved snapshot via `restoreFormGroupsFromSnapshot()`. Either way, the
|
|
29331
29507
|
* interceptor only observes the resulting form state — there is no time window where a bypass is active.
|
|
29332
29508
|
*/
|
|
@@ -29476,6 +29652,7 @@ class QdPageObjectHeaderComponent {
|
|
|
29476
29652
|
confirmationDialogService = inject(QdConfirmationDialogOpenerService);
|
|
29477
29653
|
contextService = inject(QdContextService);
|
|
29478
29654
|
resolverTriggerService = inject(QdResolverTriggerService);
|
|
29655
|
+
injector = inject(Injector);
|
|
29479
29656
|
pageStoreService = inject(QdPageStoreService);
|
|
29480
29657
|
config;
|
|
29481
29658
|
hasNavigation = false;
|
|
@@ -29596,7 +29773,7 @@ class QdPageObjectHeaderComponent {
|
|
|
29596
29773
|
this.setupResolverTrigger();
|
|
29597
29774
|
this.updateCustomActions();
|
|
29598
29775
|
this.subscribeToMetadataStream();
|
|
29599
|
-
this.formGroupManagerService.
|
|
29776
|
+
this.formGroupManagerService.updateSavedSnapshotFromCurrentValues();
|
|
29600
29777
|
this.initContexts();
|
|
29601
29778
|
}
|
|
29602
29779
|
ngOnChanges(changes) {
|
|
@@ -29621,7 +29798,7 @@ class QdPageObjectHeaderComponent {
|
|
|
29621
29798
|
return this.config.headerFacets != null;
|
|
29622
29799
|
}
|
|
29623
29800
|
edit() {
|
|
29624
|
-
this.formGroupManagerService.
|
|
29801
|
+
this.formGroupManagerService.updateSavedSnapshotFromCurrentValues();
|
|
29625
29802
|
this.pageStoreService.toggleViewonly(false);
|
|
29626
29803
|
if (this.editButton?.handler)
|
|
29627
29804
|
this.editButton.handler();
|
|
@@ -29651,7 +29828,8 @@ class QdPageObjectHeaderComponent {
|
|
|
29651
29828
|
onAfterSnapshot: () => {
|
|
29652
29829
|
this.formGroupManagerService.cancelPendingAsyncValidation();
|
|
29653
29830
|
this.pageStoreService.toggleViewonly(true);
|
|
29654
|
-
}
|
|
29831
|
+
},
|
|
29832
|
+
scheduleFrameworkRebuildStop: () => afterNextRender(() => this.formGroupManagerService.stopAdoptingFrameworkRebuild(), { injector: this.injector })
|
|
29655
29833
|
});
|
|
29656
29834
|
}
|
|
29657
29835
|
changeContext(context, selection, event) {
|
|
@@ -29679,7 +29857,7 @@ class QdPageObjectHeaderComponent {
|
|
|
29679
29857
|
.pipe(takeUntil(this._destroyed$), filter(shouldTrigger => shouldTrigger), tap(() => this._isLoadingSubject.next(true)), switchMap(() => this.pageObjectResolver.resolve()), tap(objectData => {
|
|
29680
29858
|
this._pageObjectDataSubject.next({ ...objectData, ...this._pendingMetadata });
|
|
29681
29859
|
this._pendingMetadata = {};
|
|
29682
|
-
}), tap(() => this._isLoadingSubject.next(false)), tap(() => this.formGroupManagerService.
|
|
29860
|
+
}), tap(() => this._isLoadingSubject.next(false)), tap(() => this.formGroupManagerService.updateSavedSnapshotFromCurrentValues()))
|
|
29683
29861
|
.subscribe();
|
|
29684
29862
|
}
|
|
29685
29863
|
initContexts() {
|
|
@@ -31455,6 +31633,7 @@ class QdPageComponent {
|
|
|
31455
31633
|
bottomOffset$ = inject(QD_SAFE_BOTTOM_OFFSET, { optional: true });
|
|
31456
31634
|
dialogRef = inject(DialogRef, { optional: true });
|
|
31457
31635
|
navigationInterceptor = inject(QdPageNavigationInterceptorService);
|
|
31636
|
+
injector = inject(Injector);
|
|
31458
31637
|
confirmationDialogService = inject(QdConfirmationDialogOpenerService);
|
|
31459
31638
|
/**
|
|
31460
31639
|
* This property defines the configuration for the QdPage component, including the page type,
|
|
@@ -31651,7 +31830,10 @@ class QdPageComponent {
|
|
|
31651
31830
|
QdPageCommitActionExecutor.execute(action, values, {
|
|
31652
31831
|
formGroupManager: this.formGroupManagerService,
|
|
31653
31832
|
navigationInterceptor: this.navigationInterceptor,
|
|
31654
|
-
destroyed$: this._destroyed
|
|
31833
|
+
destroyed$: this._destroyed$,
|
|
31834
|
+
scheduleFrameworkRebuildStop: () => afterNextRender(() => this.formGroupManagerService.stopAdoptingFrameworkRebuild(), {
|
|
31835
|
+
injector: this.injector
|
|
31836
|
+
})
|
|
31655
31837
|
});
|
|
31656
31838
|
};
|
|
31657
31839
|
}
|
|
@@ -31763,6 +31945,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
|
|
|
31763
31945
|
* - 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
31946
|
* - FormArrays are restored with dynamic resizing (length is aligned to snapshot before values are reset).
|
|
31765
31947
|
*
|
|
31948
|
+
* ### **Filling a form right after connecting it**
|
|
31949
|
+
* - 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.
|
|
31950
|
+
* - So loading data into a fresh form never triggers a false "unsaved changes" prompt. Only later edits count.
|
|
31951
|
+
*
|
|
31766
31952
|
* ### **Page-Dialog (FullWidth) – Unsaved-Changes Close Contract**
|
|
31767
31953
|
* - 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
31954
|
* - The page dialog can only close (e.g. via ESC or backdrop) if no unsaved changes are detected ($hasValuesChanged() is false).
|
|
@@ -31775,8 +31961,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
|
|
|
31775
31961
|
* ```html
|
|
31776
31962
|
* <qd-page [config]="pageConfig">
|
|
31777
31963
|
* <qd-section [config]="sectionConfig">
|
|
31778
|
-
* <qd-grid
|
|
31779
|
-
*
|
|
31964
|
+
* <qd-grid
|
|
31965
|
+
* qdConnectFormStateToPage="myFormGroup"
|
|
31966
|
+
* [formGroup]="myFormGroup">
|
|
31967
|
+
* My Section
|
|
31780
31968
|
* </qd-grid>
|
|
31781
31969
|
* </qd-section>
|
|
31782
31970
|
* </qd-page>
|
|
@@ -31796,36 +31984,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
|
|
|
31796
31984
|
class QdConnectFormStateToPageDirective {
|
|
31797
31985
|
formGroupManagerService = inject(QdFormGroupManagerService, { optional: true });
|
|
31798
31986
|
formGroupDirective = inject(FormGroupDirective, { host: true, optional: true });
|
|
31987
|
+
closeInitialBuildupHandle;
|
|
31799
31988
|
/**
|
|
31800
31989
|
* 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
31990
|
*/
|
|
31802
31991
|
qdConnectFormStateToPage;
|
|
31803
31992
|
ngOnInit() {
|
|
31804
|
-
if (!this.isValid())
|
|
31805
|
-
return;
|
|
31806
31993
|
const formGroup = this.formGroupDirective?.control;
|
|
31807
|
-
if (formGroup
|
|
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) {
|
|
31994
|
+
if (!formGroup) {
|
|
31817
31995
|
console.error('Quadrel Framework | QdConnectFormStateToPage - Either a [formGroup] binding or <qd-quick-edit> with a FormGroup is required.');
|
|
31818
|
-
return
|
|
31996
|
+
return;
|
|
31819
31997
|
}
|
|
31820
31998
|
if (!this.formGroupManagerService) {
|
|
31821
31999
|
console.error('Quadrel Framework | QdConnectFormStateToPage - The connector must be used within <qd-page>.');
|
|
31822
|
-
return
|
|
32000
|
+
return;
|
|
31823
32001
|
}
|
|
31824
32002
|
if (!this.formGroupManagerService.isFormGroupKeyUnique(this.qdConnectFormStateToPage)) {
|
|
31825
32003
|
console.error(`Quadrel Framework | QdConnectFormStateToPage - The FormGroup key "${this.qdConnectFormStateToPage}" is not unique.`);
|
|
31826
|
-
return
|
|
32004
|
+
return;
|
|
31827
32005
|
}
|
|
31828
|
-
|
|
32006
|
+
this.formGroupManagerService.setFormGroup(this.qdConnectFormStateToPage, formGroup);
|
|
32007
|
+
this.scheduleInitialBaseline(this.formGroupManagerService);
|
|
32008
|
+
}
|
|
32009
|
+
ngOnDestroy() {
|
|
32010
|
+
if (this.closeInitialBuildupHandle)
|
|
32011
|
+
clearTimeout(this.closeInitialBuildupHandle);
|
|
32012
|
+
this.formGroupManagerService?.tryRemoveFormGroup(this.qdConnectFormStateToPage);
|
|
32013
|
+
}
|
|
32014
|
+
scheduleInitialBaseline(manager) {
|
|
32015
|
+
const key = this.qdConnectFormStateToPage;
|
|
32016
|
+
this.closeInitialBuildupHandle = setTimeout(() => manager.endInitialBuildup(key));
|
|
31829
32017
|
}
|
|
31830
32018
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdConnectFormStateToPageDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
31831
32019
|
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.18", type: QdConnectFormStateToPageDirective, isStandalone: false, selector: "[qdConnectFormStateToPage]", inputs: { qdConnectFormStateToPage: "qdConnectFormStateToPage" }, ngImport: i0 });
|