@masterteam/work-center 0.0.82 → 0.0.83
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.
|
@@ -15,8 +15,8 @@ import { SkeletonModule } from 'primeng/skeleton';
|
|
|
15
15
|
import { catchError, EMPTY, map, take, firstValueFrom, of } from 'rxjs';
|
|
16
16
|
import { ModalRef } from '@masterteam/components/dialog';
|
|
17
17
|
import { DrawerController } from '@masterteam/components/dynamic-drawer';
|
|
18
|
-
import { ClientForm, ClientFormWarningsDialog
|
|
19
|
-
import { HttpClient } from '@angular/common/http';
|
|
18
|
+
import { ClientForm, ClientFormWarningsDialog } from '@masterteam/forms/client-form';
|
|
19
|
+
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
|
20
20
|
import { Router, ActivatedRoute } from '@angular/router';
|
|
21
21
|
import { Drawer } from '@masterteam/components/drawer';
|
|
22
22
|
import { RuntimeActionContextStore, RuntimeActionRunner, RUNTIME_ACTION_CONFIRM_DIALOG, hasActionFormFields, buildActionPayload, resolveActionLabel } from '@masterteam/components/runtime-action';
|
|
@@ -1322,20 +1322,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
1322
1322
|
|
|
1323
1323
|
/**
|
|
1324
1324
|
* The channel between the client form in the modal body and the action footer,
|
|
1325
|
-
* route-scoped alongside `RuntimeActionContextStore`. It carries
|
|
1325
|
+
* route-scoped alongside `RuntimeActionContextStore`. It carries two things
|
|
1326
1326
|
* in both directions:
|
|
1327
1327
|
*
|
|
1328
|
-
* - the form's submit context, for the validate pre-flight;
|
|
1329
1328
|
* - whether the form is locally complete, so the footer can gate its actions;
|
|
1330
1329
|
* - the failure messages of the last action, so they can be rendered above the
|
|
1331
1330
|
* form instead of squeezed beside the buttons.
|
|
1332
1331
|
*/
|
|
1333
1332
|
class WorkCenterActionFormStore {
|
|
1334
|
-
formContext = signal(null, ...(ngDevMode ? [{ debugName: "formContext" }] : /* istanbul ignore next */ []));
|
|
1335
1333
|
formIncomplete = signal(false, ...(ngDevMode ? [{ debugName: "formIncomplete" }] : /* istanbul ignore next */ []));
|
|
1336
1334
|
actionErrors = signal([], ...(ngDevMode ? [{ debugName: "actionErrors" }] : /* istanbul ignore next */ []));
|
|
1337
1335
|
errorHosted = signal(false, ...(ngDevMode ? [{ debugName: "errorHosted" }] : /* istanbul ignore next */ []));
|
|
1338
|
-
context = this.formContext.asReadonly();
|
|
1339
1336
|
/**
|
|
1340
1337
|
* True while the on-screen form fails its own local validation — a required
|
|
1341
1338
|
* field left empty, a formula error. The footer disables the actions that
|
|
@@ -1353,9 +1350,6 @@ class WorkCenterActionFormStore {
|
|
|
1353
1350
|
* claims them, so an item type with no form still reports a failure.
|
|
1354
1351
|
*/
|
|
1355
1352
|
hasErrorHost = this.errorHosted.asReadonly();
|
|
1356
|
-
setContext(context) {
|
|
1357
|
-
this.formContext.set(context);
|
|
1358
|
-
}
|
|
1359
1353
|
setIncomplete(incomplete) {
|
|
1360
1354
|
this.formIncomplete.set(incomplete);
|
|
1361
1355
|
}
|
|
@@ -2421,22 +2415,6 @@ class WorkCenterProcessRequestType {
|
|
|
2421
2415
|
effect(() => {
|
|
2422
2416
|
this.actionContext?.setFormSource(this.readOnly() ? null : (this.clientForm() ?? null));
|
|
2423
2417
|
});
|
|
2424
|
-
// See the equivalent effect in `WorkCenterProcessStepType`. Gated on
|
|
2425
|
-
// `isReadonly` rather than `readOnly`, since the request form is only
|
|
2426
|
-
// editable while the request sits in `Returned`.
|
|
2427
|
-
effect(() => {
|
|
2428
|
-
this.formStore?.setContext(this.isReadonly() || !this.canRenderForm()
|
|
2429
|
-
? null
|
|
2430
|
-
: {
|
|
2431
|
-
moduleKey: this.moduleKey(),
|
|
2432
|
-
operationKey: this.operationKey(),
|
|
2433
|
-
moduleId: this.moduleId(),
|
|
2434
|
-
levelId: this.levelId(),
|
|
2435
|
-
levelDataId: this.levelDataId(),
|
|
2436
|
-
moduleDataId: this.moduleDataId(),
|
|
2437
|
-
requestSchemaId: this.requestSchemaId(),
|
|
2438
|
-
});
|
|
2439
|
-
});
|
|
2440
2418
|
// `formControl.status` is not a signal, so mirror it into one — without
|
|
2441
2419
|
// this the footer would keep its stale enabled/disabled state while the
|
|
2442
2420
|
// approver fills the form in.
|
|
@@ -2466,7 +2444,6 @@ class WorkCenterProcessRequestType {
|
|
|
2466
2444
|
}
|
|
2467
2445
|
ngOnDestroy() {
|
|
2468
2446
|
this.actionContext?.setFormSource(null);
|
|
2469
|
-
this.formStore?.setContext(null);
|
|
2470
2447
|
this.formStore?.setIncomplete(false);
|
|
2471
2448
|
this.formStore?.setErrorHost(false);
|
|
2472
2449
|
}
|
|
@@ -2613,23 +2590,6 @@ class WorkCenterProcessStepType {
|
|
|
2613
2590
|
effect(() => {
|
|
2614
2591
|
this.actionContext?.setFormSource(this.readOnly() ? null : (this.clientForm() ?? null));
|
|
2615
2592
|
});
|
|
2616
|
-
// Publishes what the footer's `process-submit/validate` pre-flight needs.
|
|
2617
|
-
// Only the editable case is published: on a read-only form the approver
|
|
2618
|
-
// cannot fix anything, so blocking them on a rule failure would strand the
|
|
2619
|
-
// action instead of helping.
|
|
2620
|
-
effect(() => {
|
|
2621
|
-
this.formStore?.setContext(this.readOnly() || !this.canRenderForm()
|
|
2622
|
-
? null
|
|
2623
|
-
: {
|
|
2624
|
-
moduleKey: this.moduleKey(),
|
|
2625
|
-
operationKey: this.operationKey(),
|
|
2626
|
-
moduleId: this.moduleId(),
|
|
2627
|
-
levelId: this.levelId(),
|
|
2628
|
-
levelDataId: this.levelDataId(),
|
|
2629
|
-
moduleDataId: this.moduleDataId(),
|
|
2630
|
-
requestSchemaId: this.requestSchemaId(),
|
|
2631
|
-
});
|
|
2632
|
-
});
|
|
2633
2593
|
// `formControl.status` is not a signal, so mirror it into one — without
|
|
2634
2594
|
// this the footer would keep its stale enabled/disabled state while the
|
|
2635
2595
|
// approver fills the form in.
|
|
@@ -2659,7 +2619,6 @@ class WorkCenterProcessStepType {
|
|
|
2659
2619
|
}
|
|
2660
2620
|
ngOnDestroy() {
|
|
2661
2621
|
this.actionContext?.setFormSource(null);
|
|
2662
|
-
this.formStore?.setContext(null);
|
|
2663
2622
|
this.formStore?.setIncomplete(false);
|
|
2664
2623
|
this.formStore?.setErrorHost(false);
|
|
2665
2624
|
}
|
|
@@ -3142,18 +3101,65 @@ class WorkCenterFormValidationError extends Error {
|
|
|
3142
3101
|
}
|
|
3143
3102
|
}
|
|
3144
3103
|
|
|
3104
|
+
const FORM_VALIDATION_WARNINGS = 'FormValidationWarnings';
|
|
3145
3105
|
/**
|
|
3146
|
-
* Renders what a process action's validation has to say, before it runs
|
|
3147
|
-
* after it fails
|
|
3148
|
-
*
|
|
3149
|
-
* Actions do not negotiate over warnings. A Work Center action is one HTTP
|
|
3150
|
-
* request: the `process-submit/validate` pre-flight asks about non-blocking
|
|
3151
|
-
* failures up front (`confirmWarnings`), and whatever the action itself answers
|
|
3152
|
-
* is final — a failure is reported, never acknowledged and retried.
|
|
3106
|
+
* Renders what a process action's validation has to say, before it runs
|
|
3107
|
+
* (`confirmWarnings`, driven by the pre-flight) and after it fails
|
|
3108
|
+
* (`resolveErrorMessages`, `recover`).
|
|
3153
3109
|
*/
|
|
3154
3110
|
class WorkCenterActionValidationHandler {
|
|
3155
3111
|
modal = inject(ModalService);
|
|
3112
|
+
runner = inject(RuntimeActionRunner);
|
|
3156
3113
|
transloco = inject(TranslocoService);
|
|
3114
|
+
/**
|
|
3115
|
+
* Recovers the backend's `FormValidationWarnings` fallback.
|
|
3116
|
+
*
|
|
3117
|
+
* The backend revalidates immediately before it changes workflow state, so a
|
|
3118
|
+
* clean pre-flight is not a promise: configuration or persisted values may
|
|
3119
|
+
* have moved, the workflow may have advanced, or the receipt may have gone
|
|
3120
|
+
* stale. When that happens the action is refused with `400` and a
|
|
3121
|
+
* **replacement** token.
|
|
3122
|
+
*
|
|
3123
|
+
* These are warnings the user has not seen — that is the whole reason the
|
|
3124
|
+
* pre-flight's answer no longer holds — so they are shown and confirmed like
|
|
3125
|
+
* any other, and only then does the action go out again carrying the new
|
|
3126
|
+
* token. Declining reports the action as `cancelled`, and the workflow stays
|
|
3127
|
+
* where it was.
|
|
3128
|
+
*
|
|
3129
|
+
* `FormValidationErrors` — the blocking kind — are never recovered here.
|
|
3130
|
+
*/
|
|
3131
|
+
async recover(action, error, submittedPayload, defaultAfterSuccess) {
|
|
3132
|
+
const validationError = readProcessActionValidationError(error);
|
|
3133
|
+
if (validationError?.kind !== FORM_VALIDATION_WARNINGS ||
|
|
3134
|
+
!validationError.requiresAcknowledgement ||
|
|
3135
|
+
!validationError.validationWarningsToken ||
|
|
3136
|
+
validationError.validationWarningsToken ===
|
|
3137
|
+
submittedPayload?.['validationWarningsToken']) {
|
|
3138
|
+
return null;
|
|
3139
|
+
}
|
|
3140
|
+
const confirmed = await this.confirmWarnings(this.resolveValidationMessages(validationError));
|
|
3141
|
+
if (!confirmed) {
|
|
3142
|
+
return {
|
|
3143
|
+
status: 'cancelled',
|
|
3144
|
+
actionKey: action.actionKey,
|
|
3145
|
+
afterSuccess: null,
|
|
3146
|
+
};
|
|
3147
|
+
}
|
|
3148
|
+
const retryPayload = {
|
|
3149
|
+
...(submittedPayload ?? {}),
|
|
3150
|
+
validationWarningsToken: validationError.validationWarningsToken,
|
|
3151
|
+
};
|
|
3152
|
+
// This descriptor is intentionally ephemeral, and the retry is a new
|
|
3153
|
+
// request with its own idempotency key. A later action rebuilds its payload
|
|
3154
|
+
// from the current form, so a token can never outlive the values it was
|
|
3155
|
+
// issued for.
|
|
3156
|
+
return this.runner.execute({
|
|
3157
|
+
...action,
|
|
3158
|
+
needConfirmation: false,
|
|
3159
|
+
payloadKeys: Object.keys(retryPayload),
|
|
3160
|
+
payloadTemplate: retryPayload,
|
|
3161
|
+
}, { defaultAfterSuccess, silent: true });
|
|
3162
|
+
}
|
|
3157
3163
|
/**
|
|
3158
3164
|
* One entry per failing rule, so the footer can list them instead of
|
|
3159
3165
|
* rendering one newline-joined blob. A single-entry array is the normal
|
|
@@ -3192,10 +3198,10 @@ class WorkCenterActionValidationHandler {
|
|
|
3192
3198
|
return messages;
|
|
3193
3199
|
}
|
|
3194
3200
|
/**
|
|
3195
|
-
*
|
|
3196
|
-
* `
|
|
3197
|
-
*
|
|
3198
|
-
*
|
|
3201
|
+
* Shared by the `actions/validate` pre-flight in
|
|
3202
|
+
* `WorkCenterActionFormValidator` and by the `recover` fallback above. In the
|
|
3203
|
+
* normal flow only the pre-flight raises it; a second prompt means the
|
|
3204
|
+
* backend came back with warnings the pre-flight had not seen.
|
|
3199
3205
|
*
|
|
3200
3206
|
* Uses the client form's own warnings dialog so an approver sees the exact
|
|
3201
3207
|
* same prompt the normal submit path raises — same layout, same wording
|
|
@@ -3243,6 +3249,8 @@ function readProcessActionValidationError(error) {
|
|
|
3243
3249
|
: [];
|
|
3244
3250
|
return {
|
|
3245
3251
|
kind,
|
|
3252
|
+
requiresAcknowledgement: details?.['requiresAcknowledgement'] === true,
|
|
3253
|
+
validationWarningsToken: readNonEmptyString$1(details?.['validationWarningsToken']),
|
|
3246
3254
|
message: readNonEmptyString$1(errors?.['message']),
|
|
3247
3255
|
results,
|
|
3248
3256
|
};
|
|
@@ -3289,75 +3297,108 @@ function readNonEmptyString$1(value) {
|
|
|
3289
3297
|
return normalized.length ? normalized : null;
|
|
3290
3298
|
}
|
|
3291
3299
|
|
|
3292
|
-
const PROCEED = {
|
|
3293
|
-
|
|
3294
|
-
|
|
3300
|
+
const PROCEED = {
|
|
3301
|
+
status: 'proceed',
|
|
3302
|
+
validationWarningsToken: null,
|
|
3303
|
+
};
|
|
3295
3304
|
/**
|
|
3296
|
-
*
|
|
3297
|
-
*
|
|
3305
|
+
* The backend action names this endpoint accepts, keyed by the runtime
|
|
3306
|
+
* descriptor's `actionKey`. Reject / Return / Terminate are deliberately
|
|
3307
|
+
* absent: they are not validated against the approval form and must never be
|
|
3308
|
+
* pre-flighted.
|
|
3309
|
+
*/
|
|
3310
|
+
const PREFLIGHT_ACTIONS = {
|
|
3311
|
+
approve: 'Approve',
|
|
3312
|
+
resubmit: 'Resubmit',
|
|
3313
|
+
};
|
|
3314
|
+
const SILENT_MESSAGE_HEADERS = new HttpHeaders({ noMessage: 'true' });
|
|
3315
|
+
/**
|
|
3316
|
+
* Runs the action-aware validation pre-flight before Approve and Resubmit.
|
|
3317
|
+
*
|
|
3318
|
+
* The endpoint is `POST processes/{requestId}/actions/validate`, a sibling of
|
|
3319
|
+
* the action itself. It is deliberately **not** `process-submit/validate`:
|
|
3320
|
+
* that one answers for a create/edit submit and carries no active request,
|
|
3321
|
+
* step, action or effective approval form, so it validates the wrong scope for
|
|
3322
|
+
* an approval.
|
|
3298
3323
|
*
|
|
3299
|
-
*
|
|
3300
|
-
* rule failures, so an approver never sees the warnings that the same values
|
|
3301
|
-
* would raise on the normal submit path. This pre-flight reproduces the
|
|
3302
|
-
* client-form submit ceremony — blocking failures stop the action, warnings
|
|
3303
|
-
* raise the acknowledge-and-continue prompt — without submitting anything.
|
|
3324
|
+
* Three outcomes, all HTTP `200`:
|
|
3304
3325
|
*
|
|
3305
|
-
*
|
|
3306
|
-
*
|
|
3307
|
-
*
|
|
3326
|
+
* - `Blocked` — the action is never sent, and the messages go to the footer;
|
|
3327
|
+
* - `Warning` — the messages are shown, and a confirmed prompt hands the
|
|
3328
|
+
* `validationWarningsToken` to the action that follows;
|
|
3329
|
+
* - `Valid` — straight through, no token.
|
|
3330
|
+
*
|
|
3331
|
+
* The action endpoint stays authoritative and revalidates immediately before it
|
|
3332
|
+
* changes workflow state, so a pre-flight that cannot be reached lets the action
|
|
3333
|
+
* through rather than stranding it. See
|
|
3334
|
+
* `WorkCenterActionValidationHandler.recover` for what happens when that second
|
|
3335
|
+
* opinion disagrees.
|
|
3308
3336
|
*/
|
|
3309
3337
|
class WorkCenterActionFormValidator {
|
|
3310
|
-
|
|
3338
|
+
http = inject(HttpClient);
|
|
3311
3339
|
actionContext = inject(RuntimeActionContextStore);
|
|
3312
|
-
contextStore = inject(WorkCenterActionFormStore);
|
|
3313
3340
|
warnings = inject(WorkCenterActionValidationHandler);
|
|
3314
3341
|
transloco = inject(TranslocoService);
|
|
3315
3342
|
async validate(action) {
|
|
3316
|
-
const
|
|
3317
|
-
|
|
3343
|
+
const actionName = PREFLIGHT_ACTIONS[normalizeActionKey(action.actionKey)];
|
|
3344
|
+
const url = resolveValidateUrl(action);
|
|
3345
|
+
if (!actionName || !url) {
|
|
3318
3346
|
return PROCEED;
|
|
3319
3347
|
}
|
|
3320
|
-
const
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3348
|
+
const validation = await this.runValidation(url, {
|
|
3349
|
+
stepId: resolveStepId(action),
|
|
3350
|
+
action: actionName,
|
|
3351
|
+
// An action form with no editable fields sends `[]`, which the backend
|
|
3352
|
+
// accepts — a read-only approver is still validated.
|
|
3353
|
+
values: this.actionContext.resolveFormValues(),
|
|
3354
|
+
});
|
|
3325
3355
|
// A transport failure must not strand an otherwise valid action: the
|
|
3326
3356
|
// pre-flight is advisory and the action endpoint validates again anyway.
|
|
3327
3357
|
if (!validation) {
|
|
3328
3358
|
return PROCEED;
|
|
3329
3359
|
}
|
|
3330
|
-
const failures = (validation.results ?? []).filter(
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3360
|
+
const failures = (validation.validation?.results ?? []).filter((result) => !result.passed);
|
|
3361
|
+
if (validation.status === 'Blocked' ||
|
|
3362
|
+
!validation.canProceed ||
|
|
3363
|
+
validation.validation?.hasBlockingFailures) {
|
|
3334
3364
|
return {
|
|
3335
3365
|
status: 'blocked',
|
|
3336
|
-
error:
|
|
3337
|
-
? messages
|
|
3338
|
-
: [
|
|
3339
|
-
this.transloco.translate('workCenter.actions.validationBlocked'),
|
|
3340
|
-
]),
|
|
3366
|
+
error: this.blockedError(validation, failures),
|
|
3341
3367
|
};
|
|
3342
3368
|
}
|
|
3343
|
-
if (
|
|
3369
|
+
if (validation.status !== 'Warning' &&
|
|
3370
|
+
!validation.requiresAcknowledgement) {
|
|
3344
3371
|
return PROCEED;
|
|
3345
3372
|
}
|
|
3346
3373
|
const confirmed = await this.warnings.confirmWarnings(this.resolveMessages(failures));
|
|
3347
|
-
return confirmed
|
|
3374
|
+
return confirmed
|
|
3375
|
+
? {
|
|
3376
|
+
status: 'proceed',
|
|
3377
|
+
validationWarningsToken: readNonEmptyString(validation.validationWarningsToken),
|
|
3378
|
+
}
|
|
3379
|
+
: { status: 'cancelled' };
|
|
3348
3380
|
}
|
|
3349
|
-
async runValidation(
|
|
3350
|
-
const response = await firstValueFrom(this.
|
|
3351
|
-
.
|
|
3381
|
+
async runValidation(url, request) {
|
|
3382
|
+
const response = await firstValueFrom(this.http
|
|
3383
|
+
.post(url, request, { headers: SILENT_MESSAGE_HEADERS })
|
|
3352
3384
|
.pipe(catchError(() => of(null))));
|
|
3353
|
-
return response?.data ?? null;
|
|
3385
|
+
return response?.code === 1 ? (response.data ?? null) : null;
|
|
3386
|
+
}
|
|
3387
|
+
blockedError(validation, failures) {
|
|
3388
|
+
const messages = this.resolveMessages(failures);
|
|
3389
|
+
if (messages.length) {
|
|
3390
|
+
return new WorkCenterFormValidationError(messages);
|
|
3391
|
+
}
|
|
3392
|
+
return new WorkCenterFormValidationError([
|
|
3393
|
+
readNonEmptyString(validation.message) ??
|
|
3394
|
+
this.transloco.translate('workCenter.actions.validationBlocked'),
|
|
3395
|
+
]);
|
|
3354
3396
|
}
|
|
3355
3397
|
resolveMessages(results) {
|
|
3356
3398
|
const messages = [];
|
|
3357
3399
|
const seen = new Set();
|
|
3358
3400
|
for (const result of results) {
|
|
3359
|
-
const message = resolveLocalizedText(result.message, this.transloco.getActiveLang())
|
|
3360
|
-
readNonEmptyString(result.errorMessage);
|
|
3401
|
+
const message = resolveLocalizedText(result.message, this.transloco.getActiveLang());
|
|
3361
3402
|
if (!message || seen.has(message))
|
|
3362
3403
|
continue;
|
|
3363
3404
|
seen.add(message);
|
|
@@ -3372,51 +3413,31 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
3372
3413
|
type: Injectable
|
|
3373
3414
|
}] });
|
|
3374
3415
|
/**
|
|
3375
|
-
*
|
|
3376
|
-
*
|
|
3377
|
-
*
|
|
3416
|
+
* The pre-flight is the action's own sibling route, so it is derived from the
|
|
3417
|
+
* descriptor's URL rather than rebuilt from a `requestId` the FE would have to
|
|
3418
|
+
* source somewhere else — `.../processes/{requestId}/actions/approve` becomes
|
|
3419
|
+
* `.../processes/{requestId}/actions/validate`. That also keeps both calls on
|
|
3420
|
+
* whatever base the backend handed out.
|
|
3378
3421
|
*/
|
|
3379
|
-
function
|
|
3380
|
-
const
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
request.moduleId = context.moduleId;
|
|
3387
|
-
if (context.levelId != null)
|
|
3388
|
-
request.levelId = context.levelId;
|
|
3389
|
-
if (context.levelDataId != null)
|
|
3390
|
-
request.levelDataId = context.levelDataId;
|
|
3391
|
-
if (context.moduleDataId != null)
|
|
3392
|
-
request.moduleDataId = context.moduleDataId;
|
|
3393
|
-
if (context.requestSchemaId != null) {
|
|
3394
|
-
request.requestSchemaId = context.requestSchemaId;
|
|
3395
|
-
}
|
|
3396
|
-
const recordId = context.moduleDataId ??
|
|
3397
|
-
(context.moduleKey === 'LevelData' && context.operationKey !== 'Create'
|
|
3398
|
-
? context.levelDataId
|
|
3399
|
-
: undefined);
|
|
3400
|
-
if (recordId != null)
|
|
3401
|
-
request.recordId = recordId;
|
|
3402
|
-
return request;
|
|
3403
|
-
}
|
|
3404
|
-
/**
|
|
3405
|
-
* Only actions that carry the edited form values are worth validating —
|
|
3406
|
-
* Delete, Close and friends submit nothing the rules can read.
|
|
3407
|
-
*/
|
|
3408
|
-
function submitsFormValues(action) {
|
|
3409
|
-
return ((action.payloadKeys ?? []).includes(FORM_VALUES_PAYLOAD_KEY) ||
|
|
3410
|
-
(!!action.payloadTemplate &&
|
|
3411
|
-
FORM_VALUES_PAYLOAD_KEY in action.payloadTemplate));
|
|
3422
|
+
function resolveValidateUrl(action) {
|
|
3423
|
+
const url = action.url?.trim().split(/[?#]/)[0].replace(/\/+$/, '');
|
|
3424
|
+
if (!url) {
|
|
3425
|
+
return null;
|
|
3426
|
+
}
|
|
3427
|
+
const lastSlash = url.lastIndexOf('/');
|
|
3428
|
+
return lastSlash > 0 ? `${url.slice(0, lastSlash)}/validate` : null;
|
|
3412
3429
|
}
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3430
|
+
/** The same resolution the action payload uses for its own `stepId`. */
|
|
3431
|
+
function resolveStepId(action) {
|
|
3432
|
+
const templateStepId = action.payloadTemplate?.['stepId'];
|
|
3433
|
+
if (typeof templateStepId === 'number' ||
|
|
3434
|
+
typeof templateStepId === 'string') {
|
|
3435
|
+
return templateStepId;
|
|
3436
|
+
}
|
|
3437
|
+
return action.stepIds?.[0] ?? null;
|
|
3417
3438
|
}
|
|
3418
|
-
function
|
|
3419
|
-
return
|
|
3439
|
+
function normalizeActionKey(value) {
|
|
3440
|
+
return typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
3420
3441
|
}
|
|
3421
3442
|
function readNonEmptyString(value) {
|
|
3422
3443
|
if (typeof value !== 'string')
|
|
@@ -3426,18 +3447,20 @@ function readNonEmptyString(value) {
|
|
|
3426
3447
|
}
|
|
3427
3448
|
|
|
3428
3449
|
/**
|
|
3429
|
-
* Runs a Work Center footer action as
|
|
3450
|
+
* Runs a Work Center footer action, normally as a single HTTP request.
|
|
3430
3451
|
*
|
|
3431
|
-
* Everything negotiable happens before
|
|
3432
|
-
*
|
|
3433
|
-
*
|
|
3434
|
-
* the
|
|
3435
|
-
*
|
|
3452
|
+
* Everything negotiable happens before it: the `actions/validate` pre-flight
|
|
3453
|
+
* blocks the action or takes the warning acknowledgement, then the reason/note
|
|
3454
|
+
* dialog collects its fields, and the acknowledgement receipt rides along on
|
|
3455
|
+
* the request itself. A second request happens only when the backend's own
|
|
3456
|
+
* revalidation disagrees with the pre-flight — see
|
|
3457
|
+
* `WorkCenterActionValidationHandler.recover`.
|
|
3436
3458
|
*/
|
|
3437
3459
|
class WorkCenterRuntimeActionExecutor {
|
|
3438
3460
|
runner = inject(RuntimeActionRunner);
|
|
3439
3461
|
actionContext = inject(RuntimeActionContextStore);
|
|
3440
3462
|
modal = inject(ModalService);
|
|
3463
|
+
validation = inject(WorkCenterActionValidationHandler);
|
|
3441
3464
|
formValidation = inject(WorkCenterActionFormValidator);
|
|
3442
3465
|
confirmDialogClass = inject(RUNTIME_ACTION_CONFIRM_DIALOG, {
|
|
3443
3466
|
optional: true,
|
|
@@ -3462,20 +3485,33 @@ class WorkCenterRuntimeActionExecutor {
|
|
|
3462
3485
|
error: preflight.error,
|
|
3463
3486
|
};
|
|
3464
3487
|
}
|
|
3488
|
+
// The receipt for the warnings the user just acknowledged. It goes out with
|
|
3489
|
+
// the action itself, so an acknowledged Approve is a single request.
|
|
3490
|
+
const warningsToken = preflight.validationWarningsToken;
|
|
3465
3491
|
if (action.needConfirmation &&
|
|
3466
3492
|
hasActionFormFields(action) &&
|
|
3467
3493
|
this.confirmDialogClass) {
|
|
3468
|
-
return this.executeFormAction(action, defaultAfterSuccess);
|
|
3494
|
+
return this.executeFormAction(action, defaultAfterSuccess, warningsToken);
|
|
3469
3495
|
}
|
|
3470
|
-
|
|
3496
|
+
let submittedPayload;
|
|
3497
|
+
let result = await this.runner.execute(action, {
|
|
3471
3498
|
defaultAfterSuccess,
|
|
3472
3499
|
silent: true,
|
|
3473
3500
|
externalPayloadResolvers: {
|
|
3474
3501
|
values: () => this.actionContext.resolveFormValues(),
|
|
3475
3502
|
},
|
|
3503
|
+
transformPayload: (payload) => {
|
|
3504
|
+
submittedPayload = withWarningsToken(payload, warningsToken);
|
|
3505
|
+
return submittedPayload;
|
|
3506
|
+
},
|
|
3476
3507
|
});
|
|
3508
|
+
if (result.status === 'error') {
|
|
3509
|
+
result =
|
|
3510
|
+
(await this.validation.recover(action, result.error, submittedPayload, defaultAfterSuccess)) ?? result;
|
|
3511
|
+
}
|
|
3512
|
+
return result;
|
|
3477
3513
|
}
|
|
3478
|
-
executeFormAction(action, defaultAfterSuccess) {
|
|
3514
|
+
executeFormAction(action, defaultAfterSuccess, warningsToken) {
|
|
3479
3515
|
return new Promise((resolve) => {
|
|
3480
3516
|
let pendingRun = null;
|
|
3481
3517
|
let finalResult = null;
|
|
@@ -3486,19 +3522,27 @@ class WorkCenterRuntimeActionExecutor {
|
|
|
3486
3522
|
inputValues: {
|
|
3487
3523
|
action,
|
|
3488
3524
|
onSubmit: async (formValue) => {
|
|
3489
|
-
const
|
|
3525
|
+
const submittedPayload = withWarningsToken(buildActionPayload(action, formValue, {
|
|
3490
3526
|
values: () => this.actionContext.resolveFormValues(),
|
|
3491
|
-
})
|
|
3527
|
+
}), warningsToken);
|
|
3528
|
+
const executablePayload = submittedPayload ?? {};
|
|
3492
3529
|
const executableAction = {
|
|
3493
3530
|
...action,
|
|
3494
3531
|
needConfirmation: false,
|
|
3495
3532
|
payloadKeys: Object.keys(executablePayload),
|
|
3496
3533
|
payloadTemplate: executablePayload,
|
|
3497
3534
|
};
|
|
3498
|
-
pendingRun =
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3535
|
+
pendingRun = (async () => {
|
|
3536
|
+
let result = await this.runner.execute(executableAction, {
|
|
3537
|
+
defaultAfterSuccess,
|
|
3538
|
+
silent: true,
|
|
3539
|
+
});
|
|
3540
|
+
if (result.status === 'error') {
|
|
3541
|
+
result =
|
|
3542
|
+
(await this.validation.recover(action, result.error, submittedPayload, defaultAfterSuccess)) ?? result;
|
|
3543
|
+
}
|
|
3544
|
+
return result;
|
|
3545
|
+
})();
|
|
3502
3546
|
finalResult = await pendingRun;
|
|
3503
3547
|
// Every backend outcome leaves the action dialog. The footer then
|
|
3504
3548
|
// refreshes on success or displays the actionable validation error.
|
|
@@ -3539,6 +3583,17 @@ class WorkCenterRuntimeActionExecutor {
|
|
|
3539
3583
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: WorkCenterRuntimeActionExecutor, decorators: [{
|
|
3540
3584
|
type: Injectable
|
|
3541
3585
|
}] });
|
|
3586
|
+
/**
|
|
3587
|
+
* Attaches the pre-flight's acknowledgement receipt to the payload the action
|
|
3588
|
+
* is about to send. Without a receipt — a `Valid` pre-flight, or one that never
|
|
3589
|
+
* ran — the payload goes out exactly as built.
|
|
3590
|
+
*/
|
|
3591
|
+
function withWarningsToken(payload, warningsToken) {
|
|
3592
|
+
if (!warningsToken || !payload) {
|
|
3593
|
+
return payload;
|
|
3594
|
+
}
|
|
3595
|
+
return { ...payload, validationWarningsToken: warningsToken };
|
|
3596
|
+
}
|
|
3542
3597
|
|
|
3543
3598
|
const DEFAULT_GENERAL_TASK_MODAL_STYLE_CLASS = '!w-[70vw] !h-[90vh]';
|
|
3544
3599
|
class WorkCenterItemModalFooterActions {
|