@quadrel-enterprise-ui/framework 20.10.0 → 20.10.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.
|
@@ -11402,7 +11402,7 @@ class QdDatepickerComponent {
|
|
|
11402
11402
|
_onChange = () => { };
|
|
11403
11403
|
_onTouched = () => { };
|
|
11404
11404
|
ngOnInit() {
|
|
11405
|
-
this.language
|
|
11405
|
+
this.language = this.translateService?.currentLang ?? DEFAULT_LANGUAGE;
|
|
11406
11406
|
this.subscribeToLanguageChange();
|
|
11407
11407
|
this.subscribeToActionEmitterEvents();
|
|
11408
11408
|
this.initControl();
|
|
@@ -27062,6 +27062,60 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
|
|
|
27062
27062
|
type: Input
|
|
27063
27063
|
}] } });
|
|
27064
27064
|
|
|
27065
|
+
class QdPageCommitActionExecutor {
|
|
27066
|
+
static execute(action, values, context) {
|
|
27067
|
+
const { formGroupManager, navigationInterceptor, destroyed$, onAfterSnapshot } = context;
|
|
27068
|
+
const captured = formGroupManager.captureFormValues();
|
|
27069
|
+
let result$;
|
|
27070
|
+
navigationInterceptor.executeWithBypass(() => {
|
|
27071
|
+
const handlerResult = action.handler(values);
|
|
27072
|
+
if (isObservable(handlerResult))
|
|
27073
|
+
result$ = handlerResult;
|
|
27074
|
+
});
|
|
27075
|
+
const applySuccess = () => {
|
|
27076
|
+
formGroupManager.setFormGroupsSnapshot(captured);
|
|
27077
|
+
navigationInterceptor.executeWithBypass(() => {
|
|
27078
|
+
try {
|
|
27079
|
+
onAfterSnapshot?.();
|
|
27080
|
+
}
|
|
27081
|
+
catch (err) {
|
|
27082
|
+
console.error('QD-UI | QdPage - internal onAfterSnapshot hook threw after form was marked as saved.', err);
|
|
27083
|
+
}
|
|
27084
|
+
try {
|
|
27085
|
+
action.onSuccess?.();
|
|
27086
|
+
}
|
|
27087
|
+
catch (err) {
|
|
27088
|
+
console.error('QD-UI | QdPage - onSuccess callback threw after form was marked as saved.', err);
|
|
27089
|
+
}
|
|
27090
|
+
});
|
|
27091
|
+
};
|
|
27092
|
+
if (result$) {
|
|
27093
|
+
result$.pipe(take(1), takeUntil(destroyed$)).subscribe({
|
|
27094
|
+
next: ok => {
|
|
27095
|
+
if (ok)
|
|
27096
|
+
applySuccess();
|
|
27097
|
+
},
|
|
27098
|
+
error: err => this.handleError(action, err)
|
|
27099
|
+
});
|
|
27100
|
+
}
|
|
27101
|
+
else {
|
|
27102
|
+
applySuccess();
|
|
27103
|
+
}
|
|
27104
|
+
}
|
|
27105
|
+
static handleError(action, err) {
|
|
27106
|
+
if (!action.onError) {
|
|
27107
|
+
console.error('QD-UI | QdPage - Commit action observable errored — form was not marked as saved. Provide an `onError` hook on the action, or handle errors in the handler.', err);
|
|
27108
|
+
return;
|
|
27109
|
+
}
|
|
27110
|
+
try {
|
|
27111
|
+
action.onError(err);
|
|
27112
|
+
}
|
|
27113
|
+
catch (callbackErr) {
|
|
27114
|
+
console.error('QD-UI | QdPage - onError callback threw.', callbackErr);
|
|
27115
|
+
}
|
|
27116
|
+
}
|
|
27117
|
+
}
|
|
27118
|
+
|
|
27065
27119
|
/**
|
|
27066
27120
|
* The QdFormGroupManagerService provides centralized registration, snapshotting, and value tracking
|
|
27067
27121
|
* for multiple Angular `FormGroup` instances. It supports efficient state restoration, including dynamic
|
|
@@ -27176,6 +27230,39 @@ class QdFormGroupManagerService {
|
|
|
27176
27230
|
takeFormGroupsSnapshot() {
|
|
27177
27231
|
this._formGroups.forEach((fg, key) => this._formGroupsSnapshot.set(key, fg.getRawValue()));
|
|
27178
27232
|
}
|
|
27233
|
+
/**
|
|
27234
|
+
* Captures a deep-cloned snapshot of all current form values without mutating internal state.
|
|
27235
|
+
*
|
|
27236
|
+
* Used by framework orchestration code that needs to remember the "at click time" state of a form
|
|
27237
|
+
* so the snapshot can later be set to exactly those values — even if the user edits the form
|
|
27238
|
+
* while an async commit action is pending.
|
|
27239
|
+
*
|
|
27240
|
+
* Throws if any form value is not structured-cloneable (functions, symbols, DOM nodes).
|
|
27241
|
+
*/
|
|
27242
|
+
captureFormValues() {
|
|
27243
|
+
const captured = new Map();
|
|
27244
|
+
this._formGroups.forEach((fg, key) => {
|
|
27245
|
+
try {
|
|
27246
|
+
captured.set(key, structuredClone(fg.getRawValue()));
|
|
27247
|
+
}
|
|
27248
|
+
catch (err) {
|
|
27249
|
+
throw new Error(`QD-UI | QdFormGroupManager - captureFormValues() failed for group "${key}". ` +
|
|
27250
|
+
`Form values must be structured-cloneable. Non-cloneable values like functions, ` +
|
|
27251
|
+
`symbols, or DOM nodes are not supported. Original error: ${String(err)}`);
|
|
27252
|
+
}
|
|
27253
|
+
});
|
|
27254
|
+
return captured;
|
|
27255
|
+
}
|
|
27256
|
+
/**
|
|
27257
|
+
* Replaces the internal saved snapshot with the provided values.
|
|
27258
|
+
*
|
|
27259
|
+
* Intended to be paired with `captureFormValues()` so that the snapshot can be set to the
|
|
27260
|
+
* values present when a commit action was initiated — independent of whether the user edited
|
|
27261
|
+
* the form in the meantime.
|
|
27262
|
+
*/
|
|
27263
|
+
setFormGroupsSnapshot(values) {
|
|
27264
|
+
values.forEach((snapshot, key) => this._formGroupsSnapshot.set(key, snapshot));
|
|
27265
|
+
}
|
|
27179
27266
|
restoreFormGroupsFromSnapshot() {
|
|
27180
27267
|
this._formGroups.forEach((fg, key) => {
|
|
27181
27268
|
const snapshot = this._formGroupsSnapshot.get(key);
|
|
@@ -27185,20 +27272,6 @@ class QdFormGroupManagerService {
|
|
|
27185
27272
|
});
|
|
27186
27273
|
this.cancelPendingAsyncValidation();
|
|
27187
27274
|
}
|
|
27188
|
-
restoreFormGroup(fg, snapshot) {
|
|
27189
|
-
Object.entries(fg.controls).forEach(([ctrlKey, ctrl]) => {
|
|
27190
|
-
const newValue = snapshot[ctrlKey];
|
|
27191
|
-
if (ctrl instanceof FormArray && Array.isArray(newValue)) {
|
|
27192
|
-
this.resetFormArrayToValues(ctrl, newValue);
|
|
27193
|
-
}
|
|
27194
|
-
else if (ctrl instanceof FormGroup && newValue && typeof newValue === 'object') {
|
|
27195
|
-
this.restoreFormGroup(ctrl, newValue);
|
|
27196
|
-
}
|
|
27197
|
-
else {
|
|
27198
|
-
ctrl.reset(newValue);
|
|
27199
|
-
}
|
|
27200
|
-
});
|
|
27201
|
-
}
|
|
27202
27275
|
/**
|
|
27203
27276
|
* Cancels any in-flight async validators on all registered form groups.
|
|
27204
27277
|
*
|
|
@@ -27222,6 +27295,20 @@ class QdFormGroupManagerService {
|
|
|
27222
27295
|
}
|
|
27223
27296
|
});
|
|
27224
27297
|
}
|
|
27298
|
+
restoreFormGroup(fg, snapshot) {
|
|
27299
|
+
Object.entries(fg.controls).forEach(([ctrlKey, ctrl]) => {
|
|
27300
|
+
const newValue = snapshot[ctrlKey];
|
|
27301
|
+
if (ctrl instanceof FormArray && Array.isArray(newValue)) {
|
|
27302
|
+
this.resetFormArrayToValues(ctrl, newValue);
|
|
27303
|
+
}
|
|
27304
|
+
else if (ctrl instanceof FormGroup && newValue && typeof newValue === 'object') {
|
|
27305
|
+
this.restoreFormGroup(ctrl, newValue);
|
|
27306
|
+
}
|
|
27307
|
+
else {
|
|
27308
|
+
ctrl.reset(newValue);
|
|
27309
|
+
}
|
|
27310
|
+
});
|
|
27311
|
+
}
|
|
27225
27312
|
collectPendingControls(control) {
|
|
27226
27313
|
const result = [];
|
|
27227
27314
|
if (control instanceof FormGroup) {
|
|
@@ -27287,6 +27374,122 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
|
|
|
27287
27374
|
type: Injectable
|
|
27288
27375
|
}] });
|
|
27289
27376
|
|
|
27377
|
+
/**
|
|
27378
|
+
* Intercepts router navigation when unsaved form changes exist on a QdPage.
|
|
27379
|
+
*
|
|
27380
|
+
* Provided per `QdPageComponent`. Activated automatically when the page enters an editable state
|
|
27381
|
+
* (create pages or inspect pages in edit mode). Deactivated when the page returns to view mode.
|
|
27382
|
+
*
|
|
27383
|
+
* #### When navigation is intercepted
|
|
27384
|
+
*
|
|
27385
|
+
* - A `NavigationStart` event occurs (browser back, shell back button, or programmatic navigation).
|
|
27386
|
+
* - The registered form groups report unsaved changes via `QdFormGroupManagerService.$hasValuesChanged()`.
|
|
27387
|
+
*
|
|
27388
|
+
* The navigation is cancelled and a confirmation dialog is shown. The user can either discard
|
|
27389
|
+
* changes and proceed or stay on the page.
|
|
27390
|
+
*
|
|
27391
|
+
* #### When navigation is allowed
|
|
27392
|
+
*
|
|
27393
|
+
* - No unsaved changes exist — the form is either pristine or the framework has updated the
|
|
27394
|
+
* saved snapshot after a successful commit action (Submit, Save, SaveDraft).
|
|
27395
|
+
* - The page switches to view mode via `deactivate()`.
|
|
27396
|
+
*
|
|
27397
|
+
* The service has no concept of "pending actions" or bypass windows. Framework actions update the
|
|
27398
|
+
* saved snapshot directly via `QdFormGroupManagerService.setFormGroupsSnapshot(...)`; cancel-discard
|
|
27399
|
+
* flows reset the form to the saved snapshot via `restoreFormGroupsFromSnapshot()`. Either way, the
|
|
27400
|
+
* interceptor only observes the resulting form state — there is no time window where a bypass is active.
|
|
27401
|
+
*/
|
|
27402
|
+
class QdPageNavigationInterceptorService {
|
|
27403
|
+
router = inject(Router);
|
|
27404
|
+
formGroupManager = inject(QdFormGroupManagerService);
|
|
27405
|
+
confirmationDialog = inject(QdConfirmationDialogOpenerService);
|
|
27406
|
+
_destroy$ = new Subject();
|
|
27407
|
+
_deactivate$ = new Subject();
|
|
27408
|
+
/**
|
|
27409
|
+
* Guards the re-dispatched navigation issued after the user confirms discard. While the dialog
|
|
27410
|
+
* is open and while the confirmed target is being re-dispatched, the router events must not
|
|
27411
|
+
* re-enter the interception pipeline — otherwise the dialog would open recursively.
|
|
27412
|
+
*/
|
|
27413
|
+
_bypassInterception = false;
|
|
27414
|
+
/**
|
|
27415
|
+
* URL of the navigation target accepted by the user in the most recent discard-confirm dialog.
|
|
27416
|
+
* Set by `navigateToConfirmedTarget()` and consumed once by `shouldIntercept()` when the
|
|
27417
|
+
* corresponding `NavigationStart` arrives.
|
|
27418
|
+
*/
|
|
27419
|
+
_confirmedTargetUrl;
|
|
27420
|
+
_confirmationMessage;
|
|
27421
|
+
ngOnDestroy() {
|
|
27422
|
+
this._confirmedTargetUrl = undefined;
|
|
27423
|
+
this._destroy$.next();
|
|
27424
|
+
this._destroy$.complete();
|
|
27425
|
+
this._deactivate$.complete();
|
|
27426
|
+
}
|
|
27427
|
+
/**
|
|
27428
|
+
* Starts intercepting navigation events. Replaces any previously active listener.
|
|
27429
|
+
*/
|
|
27430
|
+
activate(confirmationMessage) {
|
|
27431
|
+
this._confirmationMessage = confirmationMessage;
|
|
27432
|
+
this._deactivate$.next();
|
|
27433
|
+
this.listenForUnsavedNavigationAttempts();
|
|
27434
|
+
}
|
|
27435
|
+
/**
|
|
27436
|
+
* Stops intercepting and clears the pending confirmed-target bypass.
|
|
27437
|
+
*/
|
|
27438
|
+
deactivate() {
|
|
27439
|
+
this._deactivate$.next();
|
|
27440
|
+
this._confirmedTargetUrl = undefined;
|
|
27441
|
+
}
|
|
27442
|
+
/**
|
|
27443
|
+
* Runs the given action while temporarily suppressing navigation interception.
|
|
27444
|
+
*
|
|
27445
|
+
* Used by framework commit-action orchestration to wrap synchronous handler invocations and
|
|
27446
|
+
* post-commit `onSuccess` callbacks: any router navigation that occurs during `action()` is
|
|
27447
|
+
* passed through without raising the unsaved-changes dialog. Restoration happens in a `finally`
|
|
27448
|
+
* block, so the flag is released even if `action()` throws.
|
|
27449
|
+
*/
|
|
27450
|
+
executeWithBypass(action) {
|
|
27451
|
+
this._bypassInterception = true;
|
|
27452
|
+
try {
|
|
27453
|
+
action();
|
|
27454
|
+
}
|
|
27455
|
+
finally {
|
|
27456
|
+
this._bypassInterception = false;
|
|
27457
|
+
}
|
|
27458
|
+
}
|
|
27459
|
+
listenForUnsavedNavigationAttempts() {
|
|
27460
|
+
this.router.events
|
|
27461
|
+
.pipe(filter$1((event) => event instanceof NavigationStart), filter$1(() => !this._bypassInterception), filter$1(event => this.shouldIntercept(event)), concatMap$1(event => this.checkForPendingChanges(event)), filter$1(({ hasChanges }) => hasChanges), exhaustMap(({ event }) => this.cancelNavigationAndConfirm(event)), filter$1(({ confirmed }) => confirmed), takeUntil$1(this._deactivate$), takeUntil$1(this._destroy$))
|
|
27462
|
+
.subscribe(({ targetUrl }) => this.navigateToConfirmedTarget(targetUrl));
|
|
27463
|
+
}
|
|
27464
|
+
shouldIntercept(event) {
|
|
27465
|
+
if (this._confirmedTargetUrl !== undefined && this._confirmedTargetUrl === event.url) {
|
|
27466
|
+
this._confirmedTargetUrl = undefined;
|
|
27467
|
+
return false;
|
|
27468
|
+
}
|
|
27469
|
+
return true;
|
|
27470
|
+
}
|
|
27471
|
+
checkForPendingChanges(event) {
|
|
27472
|
+
return this.formGroupManager.$hasValuesChanged().pipe(take$1(1), map$1(hasChanges => ({ event, hasChanges })));
|
|
27473
|
+
}
|
|
27474
|
+
cancelNavigationAndConfirm(event) {
|
|
27475
|
+
this._bypassInterception = true;
|
|
27476
|
+
void this.router.navigateByUrl(this.router.url, { skipLocationChange: true });
|
|
27477
|
+
return this.confirmationDialog
|
|
27478
|
+
.showDiscardConfirmDialog(this._confirmationMessage, 'page-navigation-confirmation')
|
|
27479
|
+
.pipe(map$1(confirmed => ({ confirmed, targetUrl: event.url })), finalize(() => (this._bypassInterception = false)));
|
|
27480
|
+
}
|
|
27481
|
+
navigateToConfirmedTarget(targetUrl) {
|
|
27482
|
+
this._bypassInterception = false;
|
|
27483
|
+
this._confirmedTargetUrl = targetUrl;
|
|
27484
|
+
void this.router.navigateByUrl(targetUrl);
|
|
27485
|
+
}
|
|
27486
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPageNavigationInterceptorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
27487
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPageNavigationInterceptorService });
|
|
27488
|
+
}
|
|
27489
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPageNavigationInterceptorService, decorators: [{
|
|
27490
|
+
type: Injectable
|
|
27491
|
+
}] });
|
|
27492
|
+
|
|
27290
27493
|
class QdResolverTriggerService {
|
|
27291
27494
|
route = inject(ActivatedRoute, { optional: true });
|
|
27292
27495
|
shouldTriggerResolver(triggerOn) {
|
|
@@ -27336,6 +27539,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
|
|
|
27336
27539
|
class QdPageObjectHeaderComponent {
|
|
27337
27540
|
pageObjectResolver = inject(QD_PAGE_OBJECT_RESOLVER_TOKEN, { optional: true });
|
|
27338
27541
|
formGroupManagerService = inject(QdFormGroupManagerService, { optional: true });
|
|
27542
|
+
navigationInterceptor = inject(QdPageNavigationInterceptorService);
|
|
27339
27543
|
dialogComponent = inject(QdDialogComponent, { optional: true });
|
|
27340
27544
|
dialog = inject(QdDialogService);
|
|
27341
27545
|
confirmationDialogService = inject(QdConfirmationDialogOpenerService);
|
|
@@ -27500,13 +27704,18 @@ class QdPageObjectHeaderComponent {
|
|
|
27500
27704
|
});
|
|
27501
27705
|
}
|
|
27502
27706
|
save() {
|
|
27503
|
-
const
|
|
27504
|
-
|
|
27505
|
-
|
|
27506
|
-
|
|
27507
|
-
|
|
27508
|
-
|
|
27509
|
-
|
|
27707
|
+
const saveAction = this.saveButton;
|
|
27708
|
+
if (!saveAction)
|
|
27709
|
+
return;
|
|
27710
|
+
QdPageCommitActionExecutor.execute(saveAction, this.formGroupManagerService.getAllValues(), {
|
|
27711
|
+
formGroupManager: this.formGroupManagerService,
|
|
27712
|
+
navigationInterceptor: this.navigationInterceptor,
|
|
27713
|
+
destroyed$: this._destroyed$,
|
|
27714
|
+
onAfterSnapshot: () => {
|
|
27715
|
+
this.formGroupManagerService.cancelPendingAsyncValidation();
|
|
27716
|
+
this.pageStoreService.toggleViewonly(true);
|
|
27717
|
+
}
|
|
27718
|
+
});
|
|
27510
27719
|
}
|
|
27511
27720
|
changeContext(context, selection, event) {
|
|
27512
27721
|
event.stopPropagation();
|
|
@@ -27534,7 +27743,7 @@ class QdPageObjectHeaderComponent {
|
|
|
27534
27743
|
.subscribe();
|
|
27535
27744
|
}
|
|
27536
27745
|
initContexts() {
|
|
27537
|
-
this.contexts$ = this.contextService.contexts$.pipe(map(contexts => contexts.filter(context => context.selection || this.config.pageType === 'overview')), map(contexts => contexts
|
|
27746
|
+
this.contexts$ = this.contextService.contexts$.pipe(map(contexts => contexts.filter(context => context.selection || this.config.pageType === 'overview')), map(contexts => contexts.map(({ selection, context }) => ({
|
|
27538
27747
|
label: context.label.i18n,
|
|
27539
27748
|
value: Array.isArray(selection?.value)
|
|
27540
27749
|
? selection?.value.filter(item => item !== null && item !== undefined)
|
|
@@ -27545,7 +27754,7 @@ class QdPageObjectHeaderComponent {
|
|
|
27545
27754
|
selection: selection?.value ?? []
|
|
27546
27755
|
}))));
|
|
27547
27756
|
this.contexts$
|
|
27548
|
-
.pipe(takeUntil(this._destroyed$), tap(contexts => (this._availableContexts = contexts
|
|
27757
|
+
.pipe(takeUntil(this._destroyed$), tap(contexts => (this._availableContexts = contexts.length)))
|
|
27549
27758
|
.subscribe();
|
|
27550
27759
|
}
|
|
27551
27760
|
updateCustomActions() {
|
|
@@ -28564,126 +28773,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImpo
|
|
|
28564
28773
|
type: Output
|
|
28565
28774
|
}] } });
|
|
28566
28775
|
|
|
28567
|
-
/**
|
|
28568
|
-
* Intercepts router navigation when unsaved form changes exist on a QdPage.
|
|
28569
|
-
*
|
|
28570
|
-
* Provided per `QdPageComponent`. Activated automatically when the page enters an editable state
|
|
28571
|
-
* (create pages or inspect pages in edit mode). Deactivated when the page returns to view mode.
|
|
28572
|
-
*
|
|
28573
|
-
* #### When navigation is intercepted
|
|
28574
|
-
*
|
|
28575
|
-
* - The user has unsaved form changes (tracked via `QdFormGroupManagerService`).
|
|
28576
|
-
* - A `NavigationStart` event occurs (browser back, shell back button, or programmatic navigation).
|
|
28577
|
-
*
|
|
28578
|
-
* The current navigation is cancelled, and a confirmation dialog is shown. The user can either
|
|
28579
|
-
* discard changes and proceed or cancel and stay on the page.
|
|
28580
|
-
*
|
|
28581
|
-
* #### When navigation is allowed
|
|
28582
|
-
*
|
|
28583
|
-
* - No unsaved changes exist — navigation proceeds silently.
|
|
28584
|
-
* - A framework action (Submit, SaveDraft) wraps its handler via `executeWithBypass()`.
|
|
28585
|
-
* - A confirmed discard sets `allowNextNavigation()` before the cancel handler navigates.
|
|
28586
|
-
* - The page switches to view mode via `deactivate()`.
|
|
28587
|
-
*
|
|
28588
|
-
* Custom actions defined by the application are not bypassed. If a custom action navigates away
|
|
28589
|
-
* while unsaved changes exist, the confirmation dialog is shown.
|
|
28590
|
-
*/
|
|
28591
|
-
class QdPageNavigationInterceptorService {
|
|
28592
|
-
router = inject(Router);
|
|
28593
|
-
formGroupManager = inject(QdFormGroupManagerService);
|
|
28594
|
-
confirmationDialog = inject(QdConfirmationDialogOpenerService);
|
|
28595
|
-
_destroy$ = new Subject();
|
|
28596
|
-
_deactivate$ = new Subject();
|
|
28597
|
-
_bypassInterception = false;
|
|
28598
|
-
_allowedTargetUrls = new Set();
|
|
28599
|
-
_confirmationMessage;
|
|
28600
|
-
ngOnDestroy() {
|
|
28601
|
-
this._allowedTargetUrls.clear();
|
|
28602
|
-
this._destroy$.next();
|
|
28603
|
-
this._destroy$.complete();
|
|
28604
|
-
this._deactivate$.complete();
|
|
28605
|
-
}
|
|
28606
|
-
/**
|
|
28607
|
-
* Starts intercepting navigation events. Replaces any previously active listener.
|
|
28608
|
-
*/
|
|
28609
|
-
activate(confirmationMessage) {
|
|
28610
|
-
this._confirmationMessage = confirmationMessage;
|
|
28611
|
-
this._deactivate$.next();
|
|
28612
|
-
this.listenForUnsavedNavigationAttempts();
|
|
28613
|
-
}
|
|
28614
|
-
/**
|
|
28615
|
-
* Stops intercepting and clears all pending URL bypasses.
|
|
28616
|
-
*/
|
|
28617
|
-
deactivate() {
|
|
28618
|
-
this._deactivate$.next();
|
|
28619
|
-
this._allowedTargetUrls.clear();
|
|
28620
|
-
}
|
|
28621
|
-
/**
|
|
28622
|
-
* Whitelists the next navigation so it bypasses interception.
|
|
28623
|
-
* Without a URL, any next navigation is bypassed (wildcard). With a URL, only that
|
|
28624
|
-
* specific navigation is bypassed — non-matching navigations are still intercepted.
|
|
28625
|
-
*/
|
|
28626
|
-
allowNextNavigation(targetUrl) {
|
|
28627
|
-
this._allowedTargetUrls.add(targetUrl ?? '*');
|
|
28628
|
-
}
|
|
28629
|
-
/**
|
|
28630
|
-
* Executes the callback with interception temporarily disabled.
|
|
28631
|
-
* The callback must navigate synchronously — async navigation after the callback returns
|
|
28632
|
-
* will not be bypassed. This works because Angular's router emits NavigationStart
|
|
28633
|
-
* synchronously within the navigateByUrl() / navigate() call.
|
|
28634
|
-
*/
|
|
28635
|
-
executeWithBypass(fn) {
|
|
28636
|
-
this._bypassInterception = true;
|
|
28637
|
-
try {
|
|
28638
|
-
fn();
|
|
28639
|
-
}
|
|
28640
|
-
finally {
|
|
28641
|
-
this._bypassInterception = false;
|
|
28642
|
-
}
|
|
28643
|
-
}
|
|
28644
|
-
listenForUnsavedNavigationAttempts() {
|
|
28645
|
-
this.router.events
|
|
28646
|
-
.pipe(filter$1((event) => event instanceof NavigationStart), filter$1(() => !this._bypassInterception), filter$1(event => this.shouldIntercept(event)), concatMap$1(event => this.checkForPendingChanges(event)), filter$1(({ hasChanges }) => hasChanges), exhaustMap(({ event }) => this.cancelNavigationAndConfirm(event)), filter$1(({ confirmed }) => confirmed), takeUntil$1(this._deactivate$), takeUntil$1(this._destroy$))
|
|
28647
|
-
.subscribe(({ targetUrl }) => this.navigateToConfirmedTarget(targetUrl));
|
|
28648
|
-
}
|
|
28649
|
-
shouldIntercept(event) {
|
|
28650
|
-
if (this._allowedTargetUrls.has('*')) {
|
|
28651
|
-
this._allowedTargetUrls.clear();
|
|
28652
|
-
return false;
|
|
28653
|
-
}
|
|
28654
|
-
if (this._allowedTargetUrls.has(event.url)) {
|
|
28655
|
-
this._allowedTargetUrls.delete(event.url);
|
|
28656
|
-
return false;
|
|
28657
|
-
}
|
|
28658
|
-
return true;
|
|
28659
|
-
}
|
|
28660
|
-
checkForPendingChanges(event) {
|
|
28661
|
-
return this.formGroupManager.$hasValuesChanged().pipe(take$1(1), map$1(hasChanges => ({ event, hasChanges })));
|
|
28662
|
-
}
|
|
28663
|
-
cancelNavigationAndConfirm(event) {
|
|
28664
|
-
this._bypassInterception = true;
|
|
28665
|
-
void this.router.navigateByUrl(this.router.url, { skipLocationChange: true });
|
|
28666
|
-
return this.confirmationDialog
|
|
28667
|
-
.showDiscardConfirmDialog(this._confirmationMessage, 'page-navigation-confirmation')
|
|
28668
|
-
.pipe(map$1(confirmed => ({ confirmed, targetUrl: event.url })), finalize(() => (this._bypassInterception = false)));
|
|
28669
|
-
}
|
|
28670
|
-
navigateToConfirmedTarget(targetUrl) {
|
|
28671
|
-
this._bypassInterception = false;
|
|
28672
|
-
this._allowedTargetUrls.add(targetUrl);
|
|
28673
|
-
void this.router.navigateByUrl(targetUrl);
|
|
28674
|
-
}
|
|
28675
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPageNavigationInterceptorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
28676
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPageNavigationInterceptorService });
|
|
28677
|
-
}
|
|
28678
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.18", ngImport: i0, type: QdPageNavigationInterceptorService, decorators: [{
|
|
28679
|
-
type: Injectable
|
|
28680
|
-
}] });
|
|
28681
|
-
|
|
28682
28776
|
class QdPageSubmitActionService {
|
|
28683
28777
|
footerService = inject(QdPageFooterService);
|
|
28684
28778
|
formGroupManagerService = inject(QdFormGroupManagerService);
|
|
28779
|
+
navigationInterceptor = inject(QdPageNavigationInterceptorService);
|
|
28685
28780
|
_labelI18n = 'i18n.qd.page.footer.submit';
|
|
28686
|
-
|
|
28781
|
+
_submitAction;
|
|
28687
28782
|
_isVisible = true;
|
|
28688
28783
|
_destroyed$ = new Subject();
|
|
28689
28784
|
_cancelTrackFormValidity$ = new Subject();
|
|
@@ -28697,9 +28792,9 @@ class QdPageSubmitActionService {
|
|
|
28697
28792
|
this._isVisible = isVisible;
|
|
28698
28793
|
this.configureSubmitAction(pageTypeConfig.submit);
|
|
28699
28794
|
}
|
|
28700
|
-
configureSubmitAction(
|
|
28701
|
-
this._labelI18n =
|
|
28702
|
-
this.
|
|
28795
|
+
configureSubmitAction(action) {
|
|
28796
|
+
this._labelI18n = action.label?.i18n || 'i18n.qd.page.footer.submit';
|
|
28797
|
+
this._submitAction = action;
|
|
28703
28798
|
this.registerSubmitAction();
|
|
28704
28799
|
this.trackFormValidity();
|
|
28705
28800
|
}
|
|
@@ -28709,7 +28804,7 @@ class QdPageSubmitActionService {
|
|
|
28709
28804
|
key: 'submit',
|
|
28710
28805
|
action: {
|
|
28711
28806
|
titleI18n: this._labelI18n,
|
|
28712
|
-
handler: this.
|
|
28807
|
+
handler: (...args) => this.executeSubmit(...args),
|
|
28713
28808
|
isDisabled: true,
|
|
28714
28809
|
isVisible: this._isVisible,
|
|
28715
28810
|
type: QdFooterActionType.Primary
|
|
@@ -28717,12 +28812,16 @@ class QdPageSubmitActionService {
|
|
|
28717
28812
|
}
|
|
28718
28813
|
]);
|
|
28719
28814
|
}
|
|
28720
|
-
|
|
28721
|
-
|
|
28722
|
-
|
|
28723
|
-
|
|
28724
|
-
|
|
28725
|
-
|
|
28815
|
+
executeSubmit(...args) {
|
|
28816
|
+
const action = this._submitAction;
|
|
28817
|
+
if (!action)
|
|
28818
|
+
return;
|
|
28819
|
+
const values = this.formGroupManagerService.hasFormGroups() ? this.formGroupManagerService.getAllValues() : args;
|
|
28820
|
+
QdPageCommitActionExecutor.execute(action, values, {
|
|
28821
|
+
formGroupManager: this.formGroupManagerService,
|
|
28822
|
+
navigationInterceptor: this.navigationInterceptor,
|
|
28823
|
+
destroyed$: this._destroyed$
|
|
28824
|
+
});
|
|
28726
28825
|
}
|
|
28727
28826
|
trackFormValidity() {
|
|
28728
28827
|
this._cancelTrackFormValidity$.next();
|
|
@@ -28737,7 +28836,7 @@ class QdPageSubmitActionService {
|
|
|
28737
28836
|
key: 'submit',
|
|
28738
28837
|
action: {
|
|
28739
28838
|
titleI18n: this._labelI18n,
|
|
28740
|
-
handler: this.
|
|
28839
|
+
handler: (...args) => this.executeSubmit(...args),
|
|
28741
28840
|
isDisabled: !isValid,
|
|
28742
28841
|
isVisible: this._isVisible,
|
|
28743
28842
|
type: QdFooterActionType.Primary
|
|
@@ -28792,6 +28891,22 @@ const SAFE_BOTTOM_OFFSET_PX = 64;
|
|
|
28792
28891
|
*
|
|
28793
28892
|
* Please check the relevant interfaces for each page type: `QdPageConfigOverview`, `QdPageConfigCreate`, `QdPageConfigInspect`, and `QdPageConfigCustom`.
|
|
28794
28893
|
*
|
|
28894
|
+
* #### **Commit Actions**
|
|
28895
|
+
*
|
|
28896
|
+
* Commit actions (`submit`, `save`, `saveDraft`) can either run synchronously (handler returns `void`) or wait on an async result (handler returns `Observable<boolean>`). The framework waits for the first emission, advances the saved-state baseline on `true`, and exposes race-free hooks for side effects so navigation after a successful commit does not trigger the unsaved-changes dialog.
|
|
28897
|
+
*
|
|
28898
|
+
* `onSuccess` runs when the handler emits `true` (after the baseline has advanced), `onError` runs when the handler observable errors — e.g. a failed HTTP request that is not caught inside the pipeline.
|
|
28899
|
+
*
|
|
28900
|
+
* ```ts
|
|
28901
|
+
* submit: {
|
|
28902
|
+
* handler: (formValues) => this.api.create(formValues).pipe(map(() => true)),
|
|
28903
|
+
* onSuccess: () => this.router.navigateByUrl('/'),
|
|
28904
|
+
* onError: (err) => this.notifications.add('', { type: 'critical', i18n: 'i18n.myApp.create.failed' })
|
|
28905
|
+
* }
|
|
28906
|
+
* ```
|
|
28907
|
+
*
|
|
28908
|
+
* The same mechanism applies to `save` and `saveDraft`. For the full contract (success vs. planned `false` vs. error, anti-patterns, `discardUnsavedChanges()`), see `QdPageCommitAction` and its action-specific extensions `QdPageSaveAction`, `QdPageSaveDraftAction`, `QdPageCreateSubmitAction`, and `QdPageInspectSubmitAction`.
|
|
28909
|
+
*
|
|
28795
28910
|
* #### **Validation/Parameterization**
|
|
28796
28911
|
*
|
|
28797
28912
|
* Validation and parameterization are covered in a dedicated chapter in the Storybook. Please check the "Validation" section for more information.
|
|
@@ -28861,7 +28976,15 @@ const SAFE_BOTTOM_OFFSET_PX = 64;
|
|
|
28861
28976
|
* handler: () => handleCancel()
|
|
28862
28977
|
* },
|
|
28863
28978
|
* save: {
|
|
28864
|
-
* handler: (
|
|
28979
|
+
* handler: () => saveApi.save(form.value).pipe(
|
|
28980
|
+
* tap(result => notifications.success('Saved')),
|
|
28981
|
+
* map(() => true),
|
|
28982
|
+
* catchError(err => {
|
|
28983
|
+
* notifications.showError(err);
|
|
28984
|
+
* return of(false);
|
|
28985
|
+
* })
|
|
28986
|
+
* ),
|
|
28987
|
+
* onSuccess: () => router.navigate(['/overview'])
|
|
28865
28988
|
* }
|
|
28866
28989
|
* }
|
|
28867
28990
|
* };
|
|
@@ -28979,7 +29102,8 @@ const SAFE_BOTTOM_OFFSET_PX = 64;
|
|
|
28979
29102
|
* pageType: 'create',
|
|
28980
29103
|
* pageTypeConfig: {
|
|
28981
29104
|
* submit: {
|
|
28982
|
-
* handler: (formValues) =>
|
|
29105
|
+
* handler: (formValues) => createApi.create(formValues).pipe(map(() => true)),
|
|
29106
|
+
* onSuccess: () => router.navigate(['/items'])
|
|
28983
29107
|
* }
|
|
28984
29108
|
* }
|
|
28985
29109
|
* };
|
|
@@ -29072,7 +29196,14 @@ const SAFE_BOTTOM_OFFSET_PX = 64;
|
|
|
29072
29196
|
* handler: () => handleCancel()
|
|
29073
29197
|
* },
|
|
29074
29198
|
* save: {
|
|
29075
|
-
* handler: (formValues) =>
|
|
29199
|
+
* handler: (formValues) => saveApi.save(formValues).pipe(
|
|
29200
|
+
* map(() => true),
|
|
29201
|
+
* catchError(err => {
|
|
29202
|
+
* notifications.showError(err);
|
|
29203
|
+
* return of(false);
|
|
29204
|
+
* })
|
|
29205
|
+
* ),
|
|
29206
|
+
* onSuccess: () => router.navigate(['/overview'])
|
|
29076
29207
|
* }
|
|
29077
29208
|
* }
|
|
29078
29209
|
* };
|
|
@@ -29216,7 +29347,7 @@ class QdPageComponent {
|
|
|
29216
29347
|
if (this.config.pageType === 'create' && this.config?.pageTypeConfig?.cancel !== undefined)
|
|
29217
29348
|
this.handleCancelActionWithFormChanges();
|
|
29218
29349
|
if (this.config.pageType === 'create' && this.config?.pageTypeConfig?.saveDraft !== undefined)
|
|
29219
|
-
this.initSaveDraftFooterAction();
|
|
29350
|
+
this.initSaveDraftFooterAction(this.config.pageTypeConfig.saveDraft);
|
|
29220
29351
|
if (this.config.pageType === 'inspect')
|
|
29221
29352
|
this.pageStoreService.isViewonly$
|
|
29222
29353
|
.pipe(takeUntil(this._destroyed$))
|
|
@@ -29230,6 +29361,34 @@ class QdPageComponent {
|
|
|
29230
29361
|
this._destroyed$.next();
|
|
29231
29362
|
this._destroyed$.complete();
|
|
29232
29363
|
}
|
|
29364
|
+
/**
|
|
29365
|
+
* @description Resets all registered form groups to their last saved snapshot, marking the page
|
|
29366
|
+
* as no longer dirty.
|
|
29367
|
+
*
|
|
29368
|
+
* Intended for explicit consumer-driven discard scenarios where you want subsequent navigation
|
|
29369
|
+
* to bypass the unsaved-changes dialog — for example inside a commit action's `onError` hook
|
|
29370
|
+
* after an auth-expiry response, where the user must be redirected to the login page regardless
|
|
29371
|
+
* of unsaved edits.
|
|
29372
|
+
*
|
|
29373
|
+
* Prefer this over wiring custom bypass logic; it keeps the discard explicit and auditable in
|
|
29374
|
+
* application code.
|
|
29375
|
+
*
|
|
29376
|
+
* @example
|
|
29377
|
+
* ```ts
|
|
29378
|
+
* save: {
|
|
29379
|
+
* handler: (values) => api.save(values).pipe(map(() => true)),
|
|
29380
|
+
* onError: (err) => {
|
|
29381
|
+
* if (err.status === 401) {
|
|
29382
|
+
* this.pageComponent.discardUnsavedChanges();
|
|
29383
|
+
* this.router.navigateByUrl('/login');
|
|
29384
|
+
* }
|
|
29385
|
+
* }
|
|
29386
|
+
* }
|
|
29387
|
+
* ```
|
|
29388
|
+
*/
|
|
29389
|
+
discardUnsavedChanges() {
|
|
29390
|
+
this.formGroupManagerService.restoreFormGroupsFromSnapshot();
|
|
29391
|
+
}
|
|
29233
29392
|
checkConfigValidity() {
|
|
29234
29393
|
if (!this.config)
|
|
29235
29394
|
console.warn('QdUi | QdPageComponent - To configure the page you should provide a valid config.');
|
|
@@ -29242,11 +29401,14 @@ class QdPageComponent {
|
|
|
29242
29401
|
const action = pageTypeConfig[actionKey];
|
|
29243
29402
|
if (!action)
|
|
29244
29403
|
continue;
|
|
29404
|
+
const handler = actionKey === 'cancel'
|
|
29405
|
+
? () => action.handler()
|
|
29406
|
+
: this.generateCommitActionHandler(action);
|
|
29245
29407
|
actions.push({
|
|
29246
29408
|
actionKey,
|
|
29247
29409
|
partialAction: {
|
|
29248
29410
|
...(action?.label?.i18n ? { titleI18n: action.label.i18n } : {}),
|
|
29249
|
-
handler
|
|
29411
|
+
handler
|
|
29250
29412
|
}
|
|
29251
29413
|
});
|
|
29252
29414
|
}
|
|
@@ -29263,25 +29425,20 @@ class QdPageComponent {
|
|
|
29263
29425
|
partialAction: {
|
|
29264
29426
|
handler: hasChanged
|
|
29265
29427
|
? () => this.setupCancelConfirmation()
|
|
29266
|
-
: () =>
|
|
29267
|
-
this.navigationInterceptor.executeWithBypass(() => {
|
|
29268
|
-
this.config?.pageTypeConfig?.cancel?.handler();
|
|
29269
|
-
});
|
|
29270
|
-
}
|
|
29428
|
+
: () => this.config?.pageTypeConfig?.cancel?.handler()
|
|
29271
29429
|
}
|
|
29272
29430
|
}
|
|
29273
29431
|
]);
|
|
29274
29432
|
});
|
|
29275
29433
|
}
|
|
29276
|
-
initSaveDraftFooterAction() {
|
|
29277
|
-
const pageTypeConfig = this.config.pageTypeConfig;
|
|
29434
|
+
initSaveDraftFooterAction(saveDraft) {
|
|
29278
29435
|
this.footerService.setActions([
|
|
29279
29436
|
{
|
|
29280
29437
|
key: 'saveDraft',
|
|
29281
29438
|
action: {
|
|
29282
|
-
titleI18n:
|
|
29439
|
+
titleI18n: saveDraft.label?.i18n ?? 'i18n.qd.page.footer.saveDraft',
|
|
29283
29440
|
type: QdFooterActionType.Secondary,
|
|
29284
|
-
handler: this.
|
|
29441
|
+
handler: this.generateCommitActionHandler(saveDraft),
|
|
29285
29442
|
isVisible: true,
|
|
29286
29443
|
isDisabled: false
|
|
29287
29444
|
}
|
|
@@ -29308,12 +29465,14 @@ class QdPageComponent {
|
|
|
29308
29465
|
if (this._isInitialized)
|
|
29309
29466
|
this.operationModeChanged.emit(mode);
|
|
29310
29467
|
}
|
|
29311
|
-
|
|
29468
|
+
generateCommitActionHandler(action) {
|
|
29312
29469
|
return (...args) => {
|
|
29313
|
-
if (!handler)
|
|
29314
|
-
return;
|
|
29315
29470
|
const values = this.formGroupManagerService.hasFormGroups() ? this.formGroupManagerService.getAllValues() : args;
|
|
29316
|
-
|
|
29471
|
+
QdPageCommitActionExecutor.execute(action, values, {
|
|
29472
|
+
formGroupManager: this.formGroupManagerService,
|
|
29473
|
+
navigationInterceptor: this.navigationInterceptor,
|
|
29474
|
+
destroyed$: this._destroyed$
|
|
29475
|
+
});
|
|
29317
29476
|
};
|
|
29318
29477
|
}
|
|
29319
29478
|
setupCancelConfirmation() {
|
|
@@ -29322,8 +29481,8 @@ class QdPageComponent {
|
|
|
29322
29481
|
.showDiscardConfirmDialog(cancelConfig?.confirmationMessage, this.testId + '-cancel-confirmation')
|
|
29323
29482
|
.pipe(filter$1(result => result), takeUntil(this._destroyed$))
|
|
29324
29483
|
.subscribe(() => {
|
|
29484
|
+
this.formGroupManagerService.restoreFormGroupsFromSnapshot();
|
|
29325
29485
|
cancelConfig?.handler?.();
|
|
29326
|
-
this.navigationInterceptor.allowNextNavigation();
|
|
29327
29486
|
});
|
|
29328
29487
|
}
|
|
29329
29488
|
initSubmitValidation() {
|
|
@@ -29331,7 +29490,7 @@ class QdPageComponent {
|
|
|
29331
29490
|
this.formGroupManagerService
|
|
29332
29491
|
.$areFormGroupsValid()
|
|
29333
29492
|
.pipe(takeUntil(this._cancelSubmitValidation$), takeUntil(this._destroyed$), tap(isValid => {
|
|
29334
|
-
const submitDisabledInfoText = this.config.
|
|
29493
|
+
const submitDisabledInfoText = this.config.pageTypeConfig?.submit?.disabledInfo;
|
|
29335
29494
|
this.footerService.updateActions([
|
|
29336
29495
|
{
|
|
29337
29496
|
actionKey: 'submit',
|