@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, ClientFormApiService } from '@masterteam/forms/client-form';
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 three things
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 and
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
- * The one place a Work Center action prompts about warnings: the
3196
- * `process-submit/validate` pre-flight in `WorkCenterActionFormValidator`,
3197
- * before the action runs. Nothing on the action's own response path opens
3198
- * this dialog.
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 = { status: 'proceed' };
3293
- /** The payload key carrying the edited client-form values. */
3294
- const FORM_VALUES_PAYLOAD_KEY = 'values';
3300
+ const PROCEED = {
3301
+ status: 'proceed',
3302
+ validationWarningsToken: null,
3303
+ };
3295
3304
  /**
3296
- * Runs `process-submit/validate` before a Work Center action that submits
3297
- * edited client-form values (Approve, Resubmit, …).
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
- * Why this exists: the process action endpoints answer `200` for non-blocking
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
- * It is advisory only — the action endpoint stays authoritative but it is the
3306
- * single place a Work Center action ever asks about warnings. Nothing is
3307
- * negotiated afterwards: the action runs once and its answer stands.
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
- api = inject(ClientFormApiService);
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 context = this.contextStore.context();
3317
- if (!context || !submitsFormValues(action)) {
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 fields = this.actionContext.resolveFormValues();
3321
- if (!fields.length) {
3322
- return PROCEED;
3323
- }
3324
- const validation = await this.runValidation(context, fields);
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(shouldSurface);
3331
- const blocking = failures.filter(isBlocking);
3332
- if (validation.hasBlockingFailures || blocking.length) {
3333
- const messages = this.resolveMessages(blocking.length ? blocking : failures);
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: new WorkCenterFormValidationError(messages.length
3337
- ? messages
3338
- : [
3339
- this.transloco.translate('workCenter.actions.validationBlocked'),
3340
- ]),
3366
+ error: this.blockedError(validation, failures),
3341
3367
  };
3342
3368
  }
3343
- if (!failures.length) {
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 ? PROCEED : { status: 'cancelled' };
3374
+ return confirmed
3375
+ ? {
3376
+ status: 'proceed',
3377
+ validationWarningsToken: readNonEmptyString(validation.validationWarningsToken),
3378
+ }
3379
+ : { status: 'cancelled' };
3348
3380
  }
3349
- async runValidation(context, fields) {
3350
- const response = await firstValueFrom(this.api
3351
- .validate(buildValidationRequest(context, fields), true)
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
- * Mirrors `buildSubmitRequest` in `ClientForm`, minus the pieces a pre-flight
3376
- * must never send (`templateId`) or cannot know from outside the form
3377
- * (`IsSteps`, which only applies to stepped/wizard forms).
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 buildValidationRequest(context, fields) {
3380
- const request = {
3381
- moduleKey: context.moduleKey,
3382
- operationKey: context.operationKey,
3383
- fields,
3384
- };
3385
- if (context.moduleId != null)
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
- // These two mirror `process-validation.util` in `@masterteam/forms/client-form`,
3414
- // which the package does not export. Keep them in step with it.
3415
- function shouldSurface(result) {
3416
- return !result.passed || result.isEvaluationError === true;
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 isBlocking(result) {
3419
- return (result.isEvaluationError === true || (!result.passed && result.isBlocking));
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 **one** HTTP request.
3450
+ * Runs a Work Center footer action, normally as a single HTTP request.
3430
3451
  *
3431
- * Everything negotiable happens before that request: the
3432
- * `process-submit/validate` pre-flight blocks the action or takes the
3433
- * acknowledgement, then the reason/note dialog collects its fields. Whatever
3434
- * the action endpoint answers is final the footer refreshes on success or
3435
- * lists the failure.
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
- return this.runner.execute(action, {
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 executablePayload = buildActionPayload(action, formValue, {
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 = this.runner.execute(executableAction, {
3499
- defaultAfterSuccess,
3500
- silent: true,
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 {