@epistola.app/valtimo-plugin 0.7.0 → 0.8.0

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.
Files changed (24) hide show
  1. package/fesm2022/epistola.app-valtimo-plugin.mjs +660 -458
  2. package/fesm2022/epistola.app-valtimo-plugin.mjs.map +1 -1
  3. package/lib/components/check-job-status-configuration/check-job-status-configuration.component.d.ts +4 -1
  4. package/lib/components/download-document-configuration/download-document-configuration.component.d.ts +8 -1
  5. package/lib/components/epistola-admin-page/epistola-admin-page.component.d.ts +17 -1
  6. package/lib/components/epistola-document/epistola-document.component.d.ts +65 -0
  7. package/lib/components/{epistola-download/epistola-download.formio.d.ts → epistola-document/epistola-document.formio.d.ts} +2 -2
  8. package/lib/components/epistola-document-preview/epistola-document-preview.component.d.ts +13 -25
  9. package/lib/components/epistola-retry-form/epistola-retry-form.component.d.ts +3 -7
  10. package/lib/epistola.module.d.ts +4 -5
  11. package/lib/models/admin.d.ts +28 -0
  12. package/lib/models/config.d.ts +7 -11
  13. package/lib/services/epistola-admin.service.d.ts +14 -1
  14. package/lib/services/epistola-plugin.service.d.ts +46 -7
  15. package/lib/services/epistola-task-context.interceptor.d.ts +29 -0
  16. package/lib/services/epistola-task-context.matcher.d.ts +19 -0
  17. package/lib/services/epistola-task-context.service.d.ts +26 -0
  18. package/lib/services/index.d.ts +2 -0
  19. package/package.json +1 -1
  20. package/public_api.d.ts +2 -4
  21. package/sbom.json +1 -0
  22. package/lib/components/epistola-download/epistola-download.component.d.ts +0 -24
  23. package/lib/components/epistola-preview-button/epistola-preview-button.component.d.ts +0 -35
  24. package/lib/components/epistola-preview-button/epistola-preview-button.formio.d.ts +0 -4
@@ -1,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { Injectable, EventEmitter, Output, Input, Component, ChangeDetectionStrategy, inject, NgModule, ENVIRONMENT_INITIALIZER, Injector } from '@angular/core';
3
3
  import * as i1 from '@angular/common/http';
4
- import { HttpHeaders, HttpClientModule } from '@angular/common/http';
4
+ import { HttpHeaders, HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
5
5
  import * as i2 from '@valtimo/shared';
6
6
  import { ROLE_ADMIN } from '@valtimo/shared';
7
7
  import { of, BehaviorSubject, combineLatest, take, Subject, debounceTime, takeUntil, merge } from 'rxjs';
@@ -16,12 +16,12 @@ import * as i2$2 from '@angular/forms';
16
16
  import { FormsModule } from '@angular/forms';
17
17
  import * as _jsonata from 'jsonata';
18
18
  import * as i2$3 from '@valtimo/process-link';
19
- import * as i7 from '@formio/angular';
19
+ import * as i2$4 from '@angular/platform-browser';
20
+ import * as i5 from '@formio/angular';
20
21
  import { FormioModule } from '@formio/angular';
21
- import * as i4 from '@angular/platform-browser';
22
- import * as i2$4 from '@angular/router';
22
+ import * as i2$5 from '@angular/router';
23
23
  import { RouterModule, Router } from '@angular/router';
24
- import * as i5 from 'carbon-components-angular/tabs';
24
+ import * as i5$1 from 'carbon-components-angular/tabs';
25
25
  import { TabsModule } from 'carbon-components-angular/tabs';
26
26
  import * as i6 from 'carbon-components-angular/tag';
27
27
  import { TagModule } from 'carbon-components-angular/tag';
@@ -77,6 +77,23 @@ class EpistolaAdminService {
77
77
  getPendingJobs() {
78
78
  return this.http.get(`${this.apiEndpoint}/pending`);
79
79
  }
80
+ /**
81
+ * Manually reconcile a stuck Epistola catch event by querying Epistola for the
82
+ * job's current status and re-running message correlation. Returns 200 with
83
+ * `correlated=true` on success, 409 with `correlated=false` when the job is
84
+ * still in flight, or surfaces a validation error when the execution can't be
85
+ * recovered (no subscription, missing variable, unknown tenant).
86
+ */
87
+ reconcilePending(executionId) {
88
+ return this.http.post(`${this.apiEndpoint}/pending/${encodeURIComponent(executionId)}/reconcile`, null);
89
+ }
90
+ /**
91
+ * Get the latest BPMN race-safety validation violations across deployed
92
+ * process definitions. Empty list = healthy.
93
+ */
94
+ getValidationViolations() {
95
+ return this.http.get(`${this.apiEndpoint}/validations`);
96
+ }
80
97
  /**
81
98
  * Export a single process link as a .process-link.json file.
82
99
  */
@@ -220,9 +237,11 @@ class EpistolaPluginService {
220
237
  }
221
238
  /**
222
239
  * Get a dynamically generated Formio form for retrying a failed document generation.
240
+ *
241
+ * @param taskId Operaton user task id (required — backend authorizes via OperatonTask:VIEW)
223
242
  */
224
- getRetryForm(processInstanceId, documentId, sourceActivityId) {
225
- const params = { processInstanceId };
243
+ getRetryForm(taskId, processInstanceId, documentId, sourceActivityId) {
244
+ const params = { taskId, processInstanceId };
226
245
  if (documentId) {
227
246
  params['documentId'] = documentId;
228
247
  }
@@ -245,24 +264,36 @@ class EpistolaPluginService {
245
264
  return this.http.post(`${this.apiEndpoint}/validate-jsonata`, request);
246
265
  }
247
266
  /**
248
- * Discover all previewable document sources for a given Valtimo document.
267
+ * Preview a document by dry-running the {@code generate-document} process
268
+ * link. Returns the rendered PDF as a {@link Blob}.
269
+ *
270
+ * <p>The {@code X-Skip-Interceptor: 422} header tells the global Valtimo
271
+ * error interceptor to skip its toast for validation failures so the
272
+ * caller can render an inline error message.
249
273
  */
250
- getPreviewSources(documentId) {
251
- return this.http.get(`${this.apiEndpoint}/preview-sources`, {
252
- params: { documentId },
274
+ previewToBlob(request) {
275
+ return this.http.post(`${this.apiEndpoint}/preview`, request, {
276
+ responseType: 'blob',
277
+ headers: new HttpHeaders().set('X-Skip-Interceptor', '422'),
253
278
  });
254
279
  }
255
280
  /**
256
- * Preview a document by dry-running the generate-document process link.
257
- * Returns the resolved data as a mock preview (Phase 1).
281
+ * Stream an already-generated Epistola PDF for the caller's task. The
282
+ * backend resolves the Epistola document id and tenant id from the named
283
+ * process variables on the task's process instance, so the wire never
284
+ * carries a raw PDF id.
258
285
  */
259
- previewDocument(documentId, processDefinitionKey, sourceActivityId, processInstanceId, overrides) {
260
- return this.http.post(`${this.apiEndpoint}/preview`, {
261
- documentId,
262
- processDefinitionKey,
263
- sourceActivityId,
264
- processInstanceId: processInstanceId || null,
265
- overrides: overrides || null,
286
+ downloadDocumentBlob(request) {
287
+ const params = new URLSearchParams({
288
+ taskId: request.taskId,
289
+ caseDocumentId: request.caseDocumentId,
290
+ documentVariable: request.documentVariable,
291
+ tenantIdVariable: request.tenantIdVariable,
292
+ filename: request.filename,
293
+ disposition: request.disposition,
294
+ });
295
+ return this.http.get(`${this.apiEndpoint}/documents/download?${params.toString()}`, {
296
+ responseType: 'blob',
266
297
  });
267
298
  }
268
299
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaPluginService, deps: [{ token: i1.HttpClient }, { token: i2.ConfigService }], target: i0.ɵɵFactoryTarget.Injectable });
@@ -272,6 +303,101 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
272
303
  type: Injectable
273
304
  }], ctorParameters: () => [{ type: i1.HttpClient }, { type: i2.ConfigService }] });
274
305
 
306
+ /**
307
+ * Holds the currently-open Operaton user task instance id so that custom
308
+ * Formio components rendered inside a Valtimo task form (preview, download,
309
+ * retry-form) can include it in backend requests for PBAC.
310
+ *
311
+ * Populated by {@link EpistolaTaskContextInterceptor}, which sniffs Valtimo's
312
+ * canonical "load process link for task" GET (`/api/v2/process-link/task/{taskId}`)
313
+ * — the request always fires when a task opens, before the form renders.
314
+ *
315
+ * <p><b>Why this exists:</b> Valtimo 13.21 does not expose the active task
316
+ * instance id through any service that custom Formio components can inject
317
+ * (`FormIoStateService` carries documentId and processInstanceId only;
318
+ * `TaskDetailContentComponent.taskInstanceId$` is private to that component
319
+ * and Formio elements live in their own injector tree). This service is a
320
+ * workaround until upstream exposes `taskInstanceId` via `FormIoStateService`.
321
+ */
322
+ class EpistolaTaskContextService {
323
+ _taskInstanceId$ = new BehaviorSubject(null);
324
+ taskInstanceId$ = this._taskInstanceId$.asObservable();
325
+ get taskInstanceId() {
326
+ return this._taskInstanceId$.value;
327
+ }
328
+ setTaskInstanceId(id) {
329
+ this._taskInstanceId$.next(id);
330
+ }
331
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
332
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextService, providedIn: 'root' });
333
+ }
334
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextService, decorators: [{
335
+ type: Injectable,
336
+ args: [{ providedIn: 'root' }]
337
+ }] });
338
+
339
+ /**
340
+ * Pure helpers for {@link EpistolaTaskContextInterceptor}, extracted so they
341
+ * can be unit-tested without pulling in {@code @angular/core} (which ts-jest
342
+ * cannot transform without {@code jest-preset-angular}).
343
+ */
344
+ /**
345
+ * Pattern Valtimo uses for the canonical task-open call:
346
+ * {@code GET /api/v2/process-link/task/{taskInstanceId}}.
347
+ *
348
+ * Captures the {@code taskInstanceId} (UUID v4-style 36-character hyphenated
349
+ * hex string). Anchored at the end of the URL or at a query-string delimiter
350
+ * so we don't accidentally match a longer trailing segment.
351
+ */
352
+ const TASK_PROCESS_LINK_PATTERN = /\/api\/v2\/process-link\/task\/([0-9a-fA-F-]{36})(?:\?|$)/;
353
+ /**
354
+ * Returns the captured {@code taskInstanceId} from a Valtimo task-open
355
+ * request URL, or {@code null} if the request does not match.
356
+ */
357
+ function extractTaskInstanceIdFromUrl(method, url) {
358
+ if (method !== 'GET')
359
+ return null;
360
+ const match = TASK_PROCESS_LINK_PATTERN.exec(url);
361
+ return match ? match[1] : null;
362
+ }
363
+
364
+ /**
365
+ * Sniffs Valtimo's task-open signal and pushes the active taskInstanceId into
366
+ * {@link EpistolaTaskContextService}. The signal is the canonical
367
+ * {@code GET /api/v2/process-link/task/{taskId}} call that
368
+ * {@code TaskDetailContentComponent.loadTaskDetails(...)} fires unconditionally
369
+ * before any task form is rendered (see @valtimo/task internals).
370
+ *
371
+ * <p>This interceptor does <b>not</b> modify the outgoing request. It only
372
+ * captures the taskId from the URL.
373
+ *
374
+ * <p>Workaround for Valtimo 13.21 not exposing taskInstanceId through any
375
+ * injectable service. Remove once upstream adds e.g.
376
+ * {@code FormIoStateService.setTaskInstanceId(...)}.
377
+ *
378
+ * <p>The actual URL-matching logic lives in
379
+ * {@link extractTaskInstanceIdFromUrl} so it can be unit-tested without an
380
+ * Angular harness.
381
+ */
382
+ class EpistolaTaskContextInterceptor {
383
+ taskContext;
384
+ constructor(taskContext) {
385
+ this.taskContext = taskContext;
386
+ }
387
+ intercept(request, next) {
388
+ const taskId = extractTaskInstanceIdFromUrl(request.method, request.url);
389
+ if (taskId !== null && taskId !== this.taskContext.taskInstanceId) {
390
+ this.taskContext.setTaskInstanceId(taskId);
391
+ }
392
+ return next.handle(request);
393
+ }
394
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextInterceptor, deps: [{ token: EpistolaTaskContextService }], target: i0.ɵɵFactoryTarget.Injectable });
395
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextInterceptor });
396
+ }
397
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextInterceptor, decorators: [{
398
+ type: Injectable
399
+ }], ctorParameters: () => [{ type: EpistolaTaskContextService }] });
400
+
275
401
  class EpistolaConfigurationComponent {
276
402
  save$;
277
403
  disabled$;
@@ -1983,9 +2109,17 @@ class CheckJobStatusConfigurationComponent {
1983
2109
  saveSubscription;
1984
2110
  formValue$ = new BehaviorSubject(null);
1985
2111
  valid$ = new BehaviorSubject(false);
2112
+ /** Resolved synchronously before the v-form renders — see download-document-configuration for the why. */
2113
+ resolvedPrefill = {};
2114
+ prefillResolved$ = new BehaviorSubject(false);
1986
2115
  safeDisabled$;
1987
2116
  ngOnInit() {
1988
2117
  this.safeDisabled$ = this.disabled$.pipe(startWith(true), delay(0));
2118
+ const prefill$ = this.prefillConfiguration$ ?? of({});
2119
+ prefill$.pipe(take(1)).subscribe((prefill) => {
2120
+ this.resolvedPrefill = prefill ?? {};
2121
+ this.prefillResolved$.next(true);
2122
+ });
1989
2123
  this.openSaveSubscription();
1990
2124
  }
1991
2125
  ngOnDestroy() {
@@ -2013,11 +2147,11 @@ class CheckJobStatusConfigurationComponent {
2013
2147
  });
2014
2148
  }
2015
2149
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: CheckJobStatusConfigurationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2016
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: CheckJobStatusConfigurationComponent, isStandalone: true, selector: "epistola-check-job-status-configuration", inputs: { save$: "save$", disabled$: "disabled$", pluginId: "pluginId", prefillConfiguration$: "prefillConfiguration$" }, outputs: { valid: "valid", configuration: "configuration" }, ngImport: i0, template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: safeDisabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n } as obs\"\n>\n <v-input\n name=\"requestIdVariable\"\n [title]=\"'requestIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'requestIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.requestIdVariable || 'epistolaRequestId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"statusVariable\"\n [title]=\"'statusVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'statusVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.statusVariable || 'epistolaStatus'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"documentIdVariable\"\n [title]=\"'documentIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.documentIdVariable || 'epistolaDocumentId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"errorMessageVariable\"\n [title]=\"'errorMessageVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'errorMessageVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.errorMessageVariable || 'epistolaErrorMessage'\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n</v-form>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: FormModule }, { kind: "component", type: i3.FormComponent, selector: "v-form", inputs: ["className"], outputs: ["valueChange"] }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i3.InputComponent, selector: "v-input", inputs: ["name", "type", "title", "titleTranslationKey", "defaultValue", "widthPx", "fullWidth", "margin", "smallMargin", "disabled", "step", "min", "maxLength", "tooltip", "required", "hideNumberSpinBox", "smallLabel", "rows", "clear$", "carbonTheme", "placeholder", "dataTestId", "trim", "presetsTitle", "presetOptions"], outputs: ["valueChange"] }] });
2150
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: CheckJobStatusConfigurationComponent, isStandalone: true, selector: "epistola-check-job-status-configuration", inputs: { save$: "save$", disabled$: "disabled$", pluginId: "pluginId", prefillConfiguration$: "prefillConfiguration$" }, outputs: { valid: "valid", configuration: "configuration" }, ngImport: i0, template: "<v-form (valueChange)=\"formValueChange($event)\" *ngIf=\"prefillResolved$ | async\">\n <v-input\n name=\"requestIdVariable\"\n [title]=\"'requestIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'requestIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.requestIdVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"statusVariable\"\n [title]=\"'statusVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'statusVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.statusVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"documentIdVariable\"\n [title]=\"'documentIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.documentIdVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"errorMessageVariable\"\n [title]=\"'errorMessageVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'errorMessageVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.errorMessageVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"false\"\n >\n </v-input>\n</v-form>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: FormModule }, { kind: "component", type: i3.FormComponent, selector: "v-form", inputs: ["className"], outputs: ["valueChange"] }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i3.InputComponent, selector: "v-input", inputs: ["name", "type", "title", "titleTranslationKey", "defaultValue", "widthPx", "fullWidth", "margin", "smallMargin", "disabled", "step", "min", "maxLength", "tooltip", "required", "hideNumberSpinBox", "smallLabel", "rows", "clear$", "carbonTheme", "placeholder", "dataTestId", "trim", "presetsTitle", "presetOptions"], outputs: ["valueChange"] }] });
2017
2151
  }
2018
2152
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: CheckJobStatusConfigurationComponent, decorators: [{
2019
2153
  type: Component,
2020
- args: [{ selector: 'epistola-check-job-status-configuration', standalone: true, imports: [CommonModule, PluginTranslatePipeModule, FormModule, InputModule], template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: safeDisabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n } as obs\"\n>\n <v-input\n name=\"requestIdVariable\"\n [title]=\"'requestIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'requestIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.requestIdVariable || 'epistolaRequestId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"statusVariable\"\n [title]=\"'statusVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'statusVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.statusVariable || 'epistolaStatus'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"documentIdVariable\"\n [title]=\"'documentIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.documentIdVariable || 'epistolaDocumentId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"errorMessageVariable\"\n [title]=\"'errorMessageVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'errorMessageVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.errorMessageVariable || 'epistolaErrorMessage'\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n</v-form>\n" }]
2154
+ args: [{ selector: 'epistola-check-job-status-configuration', standalone: true, imports: [CommonModule, PluginTranslatePipeModule, FormModule, InputModule], template: "<v-form (valueChange)=\"formValueChange($event)\" *ngIf=\"prefillResolved$ | async\">\n <v-input\n name=\"requestIdVariable\"\n [title]=\"'requestIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'requestIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.requestIdVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"statusVariable\"\n [title]=\"'statusVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'statusVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.statusVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"documentIdVariable\"\n [title]=\"'documentIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.documentIdVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"errorMessageVariable\"\n [title]=\"'errorMessageVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'errorMessageVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.errorMessageVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"false\"\n >\n </v-input>\n</v-form>\n" }]
2021
2155
  }], propDecorators: { save$: [{
2022
2156
  type: Input
2023
2157
  }], disabled$: [{
@@ -2042,9 +2176,21 @@ class DownloadDocumentConfigurationComponent {
2042
2176
  saveSubscription;
2043
2177
  formValue$ = new BehaviorSubject(null);
2044
2178
  valid$ = new BehaviorSubject(false);
2179
+ /**
2180
+ * Resolved prefill — populated synchronously before the v-form renders. Avoids the
2181
+ * v-input `[defaultValue]` async-binding race that otherwise drops one of the
2182
+ * fields when prefill arrives after mount.
2183
+ */
2184
+ resolvedPrefill = {};
2185
+ prefillResolved$ = new BehaviorSubject(false);
2045
2186
  safeDisabled$;
2046
2187
  ngOnInit() {
2047
2188
  this.safeDisabled$ = this.disabled$.pipe(startWith(true), delay(0));
2189
+ const prefill$ = this.prefillConfiguration$ ?? of({});
2190
+ prefill$.pipe(take(1)).subscribe((prefill) => {
2191
+ this.resolvedPrefill = prefill ?? {};
2192
+ this.prefillResolved$.next(true);
2193
+ });
2048
2194
  this.openSaveSubscription();
2049
2195
  }
2050
2196
  ngOnDestroy() {
@@ -2056,7 +2202,7 @@ class DownloadDocumentConfigurationComponent {
2056
2202
  this.handleValid(formValue);
2057
2203
  }
2058
2204
  handleValid(formValue) {
2059
- const valid = !!(formValue?.documentIdVariable && formValue?.contentVariable);
2205
+ const valid = !!(formValue?.documentVariable && formValue?.contentVariable);
2060
2206
  this.valid$.next(valid);
2061
2207
  this.valid.emit(valid);
2062
2208
  }
@@ -2072,11 +2218,11 @@ class DownloadDocumentConfigurationComponent {
2072
2218
  });
2073
2219
  }
2074
2220
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DownloadDocumentConfigurationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2075
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: DownloadDocumentConfigurationComponent, isStandalone: true, selector: "epistola-download-document-configuration", inputs: { save$: "save$", disabled$: "disabled$", pluginId: "pluginId", prefillConfiguration$: "prefillConfiguration$" }, outputs: { valid: "valid", configuration: "configuration" }, ngImport: i0, template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: safeDisabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n } as obs\"\n>\n <v-input\n name=\"documentIdVariable\"\n [title]=\"'documentIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.documentIdVariable || 'epistolaDocumentId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"contentVariable\"\n [title]=\"'contentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'contentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.contentVariable || 'documentContent'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: FormModule }, { kind: "component", type: i3.FormComponent, selector: "v-form", inputs: ["className"], outputs: ["valueChange"] }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i3.InputComponent, selector: "v-input", inputs: ["name", "type", "title", "titleTranslationKey", "defaultValue", "widthPx", "fullWidth", "margin", "smallMargin", "disabled", "step", "min", "maxLength", "tooltip", "required", "hideNumberSpinBox", "smallLabel", "rows", "clear$", "carbonTheme", "placeholder", "dataTestId", "trim", "presetsTitle", "presetOptions"], outputs: ["valueChange"] }] });
2221
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: DownloadDocumentConfigurationComponent, isStandalone: true, selector: "epistola-download-document-configuration", inputs: { save$: "save$", disabled$: "disabled$", pluginId: "pluginId", prefillConfiguration$: "prefillConfiguration$" }, outputs: { valid: "valid", configuration: "configuration" }, ngImport: i0, template: "<v-form (valueChange)=\"formValueChange($event)\" *ngIf=\"prefillResolved$ | async\">\n <v-input\n name=\"documentVariable\"\n [title]=\"'documentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.documentVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"contentVariable\"\n [title]=\"'contentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'contentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.contentVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: FormModule }, { kind: "component", type: i3.FormComponent, selector: "v-form", inputs: ["className"], outputs: ["valueChange"] }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i3.InputComponent, selector: "v-input", inputs: ["name", "type", "title", "titleTranslationKey", "defaultValue", "widthPx", "fullWidth", "margin", "smallMargin", "disabled", "step", "min", "maxLength", "tooltip", "required", "hideNumberSpinBox", "smallLabel", "rows", "clear$", "carbonTheme", "placeholder", "dataTestId", "trim", "presetsTitle", "presetOptions"], outputs: ["valueChange"] }] });
2076
2222
  }
2077
2223
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DownloadDocumentConfigurationComponent, decorators: [{
2078
2224
  type: Component,
2079
- args: [{ selector: 'epistola-download-document-configuration', standalone: true, imports: [CommonModule, PluginTranslatePipeModule, FormModule, InputModule], template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: safeDisabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n } as obs\"\n>\n <v-input\n name=\"documentIdVariable\"\n [title]=\"'documentIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.documentIdVariable || 'epistolaDocumentId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"contentVariable\"\n [title]=\"'contentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'contentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.contentVariable || 'documentContent'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n" }]
2225
+ args: [{ selector: 'epistola-download-document-configuration', standalone: true, imports: [CommonModule, PluginTranslatePipeModule, FormModule, InputModule], template: "<v-form (valueChange)=\"formValueChange($event)\" *ngIf=\"prefillResolved$ | async\">\n <v-input\n name=\"documentVariable\"\n [title]=\"'documentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.documentVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"contentVariable\"\n [title]=\"'contentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'contentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.contentVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n" }]
2080
2226
  }], propDecorators: { save$: [{
2081
2227
  type: Input
2082
2228
  }], disabled$: [{
@@ -2091,35 +2237,89 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2091
2237
  type: Output
2092
2238
  }] } });
2093
2239
 
2094
- class EpistolaDownloadComponent {
2095
- http;
2240
+ /**
2241
+ * Unified Formio component for the after-generation Epistola PDF UX. Reads
2242
+ * the PDF id and tenant id from named process variables on the caller's
2243
+ * task via the {@code /documents/download} endpoint.
2244
+ *
2245
+ * <p>Three presentations, all backed by the same backend call:
2246
+ * <ul>
2247
+ * <li>{@code display="inline"} — render the PDF inline in a panel.</li>
2248
+ * <li>{@code display="button"} — show a click-to-save download button only.</li>
2249
+ * <li>{@code display="both"} (default) — inline panel with a download icon
2250
+ * in the header.</li>
2251
+ * </ul>
2252
+ *
2253
+ * <p>For the dry-run / what-would-be-generated UX use
2254
+ * {@code epistola-document-preview} instead.
2255
+ */
2256
+ class EpistolaDocumentComponent {
2257
+ epistolaPluginService;
2258
+ sanitizer;
2259
+ formIoStateService;
2260
+ taskContext;
2261
+ cdr;
2096
2262
  value;
2097
2263
  valueChange = new EventEmitter();
2098
2264
  disabled = false;
2265
+ label = 'Document';
2266
+ /** How to present the document. `both` (default) shows an inline panel with a download icon. */
2267
+ display = 'both';
2268
+ /**
2269
+ * Process-variable name holding the Epistola result. Default: `epistolaResult`.
2270
+ *
2271
+ * Type-tolerant on the backend: if the named variable holds a rich result object
2272
+ * (`Map<String, Object>` written by `generate-document` and updated by the result
2273
+ * collector), the backend digs out the `documentId` key. If it holds a plain
2274
+ * String (custom flow that wrote a bare id), the backend uses it as-is.
2275
+ */
2276
+ documentVariable = 'epistolaResult';
2277
+ /** Process-variable name holding the Epistola tenant id. Default: `epistolaTenantId`. */
2278
+ tenantIdVariable = 'epistolaTenantId';
2279
+ /** Filename used for the download disposition. */
2099
2280
  filename = 'document.pdf';
2100
- label = 'Download PDF';
2281
+ loading = false;
2101
2282
  downloading = false;
2102
2283
  error = null;
2103
- get buttonLabel() {
2104
- return this.label || 'Download PDF';
2284
+ previewUrl = null;
2285
+ currentBlobUrl = null;
2286
+ subscription;
2287
+ get designMode() {
2288
+ return !this.formIoStateService.documentId;
2105
2289
  }
2106
- constructor(http) {
2107
- this.http = http;
2290
+ constructor(epistolaPluginService, sanitizer, formIoStateService, taskContext, cdr) {
2291
+ this.epistolaPluginService = epistolaPluginService;
2292
+ this.sanitizer = sanitizer;
2293
+ this.formIoStateService = formIoStateService;
2294
+ this.taskContext = taskContext;
2295
+ this.cdr = cdr;
2296
+ }
2297
+ ngOnInit() {
2298
+ if (this.designMode) {
2299
+ return;
2300
+ }
2301
+ if (this.display !== 'button') {
2302
+ this.loadInline();
2303
+ }
2304
+ }
2305
+ ngOnDestroy() {
2306
+ this.subscription?.unsubscribe();
2307
+ this.revokeBlobUrl();
2108
2308
  }
2109
- hasRequiredData() {
2110
- return !!(this.value?.documentId && this.value?.tenantId);
2309
+ refresh() {
2310
+ this.loadInline();
2111
2311
  }
2112
2312
  download() {
2113
- if (!this.hasRequiredData() || this.downloading) {
2313
+ if (this.designMode || this.downloading) {
2114
2314
  return;
2115
2315
  }
2316
+ const request = this.buildRequest('attachment');
2317
+ if (!request)
2318
+ return;
2116
2319
  this.downloading = true;
2117
2320
  this.error = null;
2118
- const { documentId, tenantId } = this.value;
2119
- const url = `/api/v1/plugin/epistola/documents/${encodeURIComponent(documentId)}/download` +
2120
- `?tenantId=${encodeURIComponent(tenantId)}` +
2121
- `&filename=${encodeURIComponent(this.filename)}`;
2122
- this.http.get(url, { responseType: 'blob' }).subscribe({
2321
+ this.cdr.markForCheck();
2322
+ this.epistolaPluginService.downloadDocumentBlob(request).subscribe({
2123
2323
  next: (blob) => {
2124
2324
  const objectUrl = URL.createObjectURL(blob);
2125
2325
  const anchor = document.createElement('a');
@@ -2128,66 +2328,246 @@ class EpistolaDownloadComponent {
2128
2328
  anchor.click();
2129
2329
  URL.revokeObjectURL(objectUrl);
2130
2330
  this.downloading = false;
2331
+ this.cdr.markForCheck();
2131
2332
  },
2132
2333
  error: (err) => {
2133
- console.error('Download failed', err);
2134
- this.error = 'Download mislukt. Probeer opnieuw.';
2334
+ this.error = err.status === 404 ? 'Document is nog niet gegenereerd.' : 'Download mislukt.';
2135
2335
  this.downloading = false;
2336
+ this.cdr.markForCheck();
2136
2337
  },
2137
2338
  });
2138
2339
  }
2139
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDownloadComponent, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Component });
2140
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaDownloadComponent, isStandalone: true, selector: "epistola-download-component", inputs: { value: "value", disabled: "disabled", filename: "filename", label: "label" }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: `
2141
- <button
2142
- type="button"
2143
- class="btn btn-outline-primary"
2144
- [disabled]="disabled || downloading || !hasRequiredData()"
2145
- (click)="download()"
2146
- >
2147
- <i class="mdi mdi-download mr-1"></i>
2148
- {{ downloading ? 'Downloading...' : buttonLabel }}
2149
- </button>
2150
- <span *ngIf="error" class="text-danger ml-2">{{ error }}</span>
2151
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
2340
+ loadInline() {
2341
+ const request = this.buildRequest('inline');
2342
+ if (!request)
2343
+ return;
2344
+ this.loading = true;
2345
+ this.error = null;
2346
+ this.cdr.markForCheck();
2347
+ this.revokeBlobUrl();
2348
+ this.subscription?.unsubscribe();
2349
+ this.subscription = this.epistolaPluginService.downloadDocumentBlob(request).subscribe({
2350
+ next: (blob) => {
2351
+ this.currentBlobUrl = URL.createObjectURL(blob);
2352
+ this.previewUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.currentBlobUrl);
2353
+ this.loading = false;
2354
+ this.cdr.markForCheck();
2355
+ },
2356
+ error: (err) => {
2357
+ this.previewUrl = null;
2358
+ this.error =
2359
+ err.status === 404
2360
+ ? 'Document is nog niet gegenereerd.'
2361
+ : 'Document kon niet geladen worden.';
2362
+ this.loading = false;
2363
+ this.cdr.markForCheck();
2364
+ },
2365
+ });
2366
+ }
2367
+ buildRequest(disposition) {
2368
+ const taskId = this.taskContext.taskInstanceId;
2369
+ const caseDocumentId = this.formIoStateService.documentId;
2370
+ if (!taskId || !caseDocumentId) {
2371
+ this.error = 'Document is alleen beschikbaar binnen een taak.';
2372
+ this.cdr.markForCheck();
2373
+ return null;
2374
+ }
2375
+ return {
2376
+ taskId,
2377
+ caseDocumentId,
2378
+ documentVariable: this.documentVariable,
2379
+ tenantIdVariable: this.tenantIdVariable,
2380
+ filename: this.filename,
2381
+ disposition,
2382
+ };
2383
+ }
2384
+ revokeBlobUrl() {
2385
+ if (this.currentBlobUrl) {
2386
+ URL.revokeObjectURL(this.currentBlobUrl);
2387
+ this.currentBlobUrl = null;
2388
+ this.previewUrl = null;
2389
+ }
2390
+ }
2391
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDocumentComponent, deps: [{ token: EpistolaPluginService }, { token: i2$4.DomSanitizer }, { token: i3.FormIoStateService }, { token: EpistolaTaskContextService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
2392
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaDocumentComponent, isStandalone: true, selector: "epistola-document-component", inputs: { value: "value", disabled: "disabled", label: "label", display: "display", documentVariable: "documentVariable", tenantIdVariable: "tenantIdVariable", filename: "filename" }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: `
2393
+ <!-- Design-time placeholder -->
2394
+ <div *ngIf="designMode" class="epistola-doc-panel">
2395
+ <div class="doc-header">
2396
+ <span>{{ label || 'Document' }}</span>
2397
+ <span class="design-tag">design mode</span>
2398
+ </div>
2399
+ <div class="doc-body design-info">
2400
+ <div class="design-section">
2401
+ <div class="design-label">Display</div>
2402
+ <div class="design-value">{{ display }}</div>
2403
+ <div class="design-label">Document variable</div>
2404
+ <div class="design-value">{{ documentVariable }}</div>
2405
+ <div class="design-label">Tenant ID variable</div>
2406
+ <div class="design-value">{{ tenantIdVariable }}</div>
2407
+ </div>
2408
+ </div>
2409
+ </div>
2410
+
2411
+ <!-- Button-only display -->
2412
+ <div *ngIf="!designMode && display === 'button'">
2413
+ <button
2414
+ type="button"
2415
+ class="btn btn-outline-primary"
2416
+ [disabled]="disabled || downloading"
2417
+ (click)="download()"
2418
+ >
2419
+ <i class="mdi mdi-download mr-1"></i>
2420
+ {{ downloading ? 'Downloading...' : label || 'Download PDF' }}
2421
+ </button>
2422
+ <span *ngIf="error" class="text-danger ml-2">{{ error }}</span>
2423
+ </div>
2424
+
2425
+ <!-- Inline / both: panel with optional download icon -->
2426
+ <div *ngIf="!designMode && display !== 'button'" class="epistola-doc-panel">
2427
+ <div class="doc-header">
2428
+ <span>{{ label || 'Document' }}</span>
2429
+ <div class="doc-controls">
2430
+ <button
2431
+ *ngIf="display === 'both'"
2432
+ type="button"
2433
+ class="doc-icon-btn"
2434
+ [disabled]="disabled || downloading"
2435
+ (click)="download()"
2436
+ [title]="downloading ? 'Downloading...' : 'Download'"
2437
+ >
2438
+ <i class="mdi mdi-download"></i>
2439
+ </button>
2440
+ <button
2441
+ type="button"
2442
+ class="doc-icon-btn"
2443
+ [disabled]="loading"
2444
+ (click)="refresh()"
2445
+ title="Refresh"
2446
+ >
2447
+ <i class="mdi mdi-refresh"></i>
2448
+ </button>
2449
+ </div>
2450
+ </div>
2451
+ <div class="doc-body">
2452
+ <div *ngIf="loading" class="doc-loading">Loading document...</div>
2453
+ <div *ngIf="error && !loading" class="doc-unavailable">
2454
+ <i class="mdi mdi-information-outline"></i>
2455
+ {{ error }}
2456
+ </div>
2457
+ <object
2458
+ *ngIf="previewUrl && !loading"
2459
+ [data]="previewUrl"
2460
+ type="application/pdf"
2461
+ class="doc-pdf"
2462
+ >
2463
+ PDF preview is not supported in this browser.
2464
+ </object>
2465
+ </div>
2466
+ </div>
2467
+ `, isInline: true, styles: [".epistola-doc-panel{border:1px solid #dee2e6;border-radius:4px;background:#f8f9fa;display:flex;flex-direction:column}.doc-header{display:flex;justify-content:space-between;align-items:center;padding:.5rem 1rem;border-bottom:1px solid #dee2e6;font-weight:700;color:#495057}.doc-controls{display:flex;gap:.25rem}.doc-icon-btn{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.25rem .5rem;font-size:.9rem;cursor:pointer}.doc-icon-btn:hover:not(:disabled){background:#e9ecef}.doc-icon-btn:disabled{opacity:.5;cursor:not-allowed}.doc-body{display:flex;flex-direction:column;min-height:500px}.doc-loading,.doc-unavailable{padding:2rem;text-align:center;color:#6c757d;font-style:italic}.doc-unavailable i{margin-right:.25rem}.doc-pdf{width:100%;flex:1;min-height:500px}.design-info{padding:1rem;min-height:auto}.design-section{margin-bottom:.5rem}.design-label{font-size:.7rem;text-transform:uppercase;color:#868e96;font-weight:600;letter-spacing:.05em}.design-value{font-family:monospace;font-size:.85rem;color:#212529;margin-bottom:.25rem}.design-tag{font-size:.7rem;font-weight:400;color:#6c757d;font-style:italic}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2152
2468
  }
2153
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDownloadComponent, decorators: [{
2469
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDocumentComponent, decorators: [{
2154
2470
  type: Component,
2155
- args: [{
2156
- standalone: true,
2157
- imports: [CommonModule],
2158
- selector: 'epistola-download-component',
2159
- template: `
2160
- <button
2161
- type="button"
2162
- class="btn btn-outline-primary"
2163
- [disabled]="disabled || downloading || !hasRequiredData()"
2164
- (click)="download()"
2165
- >
2166
- <i class="mdi mdi-download mr-1"></i>
2167
- {{ downloading ? 'Downloading...' : buttonLabel }}
2168
- </button>
2169
- <span *ngIf="error" class="text-danger ml-2">{{ error }}</span>
2170
- `,
2171
- }]
2172
- }], ctorParameters: () => [{ type: i1.HttpClient }], propDecorators: { value: [{
2471
+ args: [{ standalone: true, imports: [CommonModule], selector: 'epistola-document-component', changeDetection: ChangeDetectionStrategy.OnPush, template: `
2472
+ <!-- Design-time placeholder -->
2473
+ <div *ngIf="designMode" class="epistola-doc-panel">
2474
+ <div class="doc-header">
2475
+ <span>{{ label || 'Document' }}</span>
2476
+ <span class="design-tag">design mode</span>
2477
+ </div>
2478
+ <div class="doc-body design-info">
2479
+ <div class="design-section">
2480
+ <div class="design-label">Display</div>
2481
+ <div class="design-value">{{ display }}</div>
2482
+ <div class="design-label">Document variable</div>
2483
+ <div class="design-value">{{ documentVariable }}</div>
2484
+ <div class="design-label">Tenant ID variable</div>
2485
+ <div class="design-value">{{ tenantIdVariable }}</div>
2486
+ </div>
2487
+ </div>
2488
+ </div>
2489
+
2490
+ <!-- Button-only display -->
2491
+ <div *ngIf="!designMode && display === 'button'">
2492
+ <button
2493
+ type="button"
2494
+ class="btn btn-outline-primary"
2495
+ [disabled]="disabled || downloading"
2496
+ (click)="download()"
2497
+ >
2498
+ <i class="mdi mdi-download mr-1"></i>
2499
+ {{ downloading ? 'Downloading...' : label || 'Download PDF' }}
2500
+ </button>
2501
+ <span *ngIf="error" class="text-danger ml-2">{{ error }}</span>
2502
+ </div>
2503
+
2504
+ <!-- Inline / both: panel with optional download icon -->
2505
+ <div *ngIf="!designMode && display !== 'button'" class="epistola-doc-panel">
2506
+ <div class="doc-header">
2507
+ <span>{{ label || 'Document' }}</span>
2508
+ <div class="doc-controls">
2509
+ <button
2510
+ *ngIf="display === 'both'"
2511
+ type="button"
2512
+ class="doc-icon-btn"
2513
+ [disabled]="disabled || downloading"
2514
+ (click)="download()"
2515
+ [title]="downloading ? 'Downloading...' : 'Download'"
2516
+ >
2517
+ <i class="mdi mdi-download"></i>
2518
+ </button>
2519
+ <button
2520
+ type="button"
2521
+ class="doc-icon-btn"
2522
+ [disabled]="loading"
2523
+ (click)="refresh()"
2524
+ title="Refresh"
2525
+ >
2526
+ <i class="mdi mdi-refresh"></i>
2527
+ </button>
2528
+ </div>
2529
+ </div>
2530
+ <div class="doc-body">
2531
+ <div *ngIf="loading" class="doc-loading">Loading document...</div>
2532
+ <div *ngIf="error && !loading" class="doc-unavailable">
2533
+ <i class="mdi mdi-information-outline"></i>
2534
+ {{ error }}
2535
+ </div>
2536
+ <object
2537
+ *ngIf="previewUrl && !loading"
2538
+ [data]="previewUrl"
2539
+ type="application/pdf"
2540
+ class="doc-pdf"
2541
+ >
2542
+ PDF preview is not supported in this browser.
2543
+ </object>
2544
+ </div>
2545
+ </div>
2546
+ `, styles: [".epistola-doc-panel{border:1px solid #dee2e6;border-radius:4px;background:#f8f9fa;display:flex;flex-direction:column}.doc-header{display:flex;justify-content:space-between;align-items:center;padding:.5rem 1rem;border-bottom:1px solid #dee2e6;font-weight:700;color:#495057}.doc-controls{display:flex;gap:.25rem}.doc-icon-btn{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.25rem .5rem;font-size:.9rem;cursor:pointer}.doc-icon-btn:hover:not(:disabled){background:#e9ecef}.doc-icon-btn:disabled{opacity:.5;cursor:not-allowed}.doc-body{display:flex;flex-direction:column;min-height:500px}.doc-loading,.doc-unavailable{padding:2rem;text-align:center;color:#6c757d;font-style:italic}.doc-unavailable i{margin-right:.25rem}.doc-pdf{width:100%;flex:1;min-height:500px}.design-info{padding:1rem;min-height:auto}.design-section{margin-bottom:.5rem}.design-label{font-size:.7rem;text-transform:uppercase;color:#868e96;font-weight:600;letter-spacing:.05em}.design-value{font-family:monospace;font-size:.85rem;color:#212529;margin-bottom:.25rem}.design-tag{font-size:.7rem;font-weight:400;color:#6c757d;font-style:italic}\n"] }]
2547
+ }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$4.DomSanitizer }, { type: i3.FormIoStateService }, { type: EpistolaTaskContextService }, { type: i0.ChangeDetectorRef }], propDecorators: { value: [{
2173
2548
  type: Input
2174
2549
  }], valueChange: [{
2175
2550
  type: Output
2176
2551
  }], disabled: [{
2177
2552
  type: Input
2178
- }], filename: [{
2179
- type: Input
2180
2553
  }], label: [{
2181
2554
  type: Input
2555
+ }], display: [{
2556
+ type: Input
2557
+ }], documentVariable: [{
2558
+ type: Input
2559
+ }], tenantIdVariable: [{
2560
+ type: Input
2561
+ }], filename: [{
2562
+ type: Input
2182
2563
  }] } });
2183
2564
 
2184
2565
  class EpistolaRetryFormComponent {
2185
2566
  epistolaPluginService;
2186
2567
  formIoStateService;
2187
2568
  cdr;
2188
- http;
2189
2569
  sanitizer;
2190
- configService;
2570
+ taskContext;
2191
2571
  value;
2192
2572
  valueChange = new EventEmitter();
2193
2573
  disabled = false;
@@ -2208,19 +2588,16 @@ class EpistolaRetryFormComponent {
2208
2588
  currentBlobUrl = null;
2209
2589
  resolvedSourceActivityId;
2210
2590
  processDefinitionKey;
2211
- apiEndpoint;
2212
2591
  formOptions = {
2213
2592
  noAlerts: true,
2214
2593
  buttonSettings: { showCancel: false, showSubmit: false, showPrevious: false, showNext: false },
2215
2594
  };
2216
- constructor(epistolaPluginService, formIoStateService, cdr, http, sanitizer, configService) {
2595
+ constructor(epistolaPluginService, formIoStateService, cdr, sanitizer, taskContext) {
2217
2596
  this.epistolaPluginService = epistolaPluginService;
2218
2597
  this.formIoStateService = formIoStateService;
2219
2598
  this.cdr = cdr;
2220
- this.http = http;
2221
2599
  this.sanitizer = sanitizer;
2222
- this.configService = configService;
2223
- this.apiEndpoint = `${this.configService.config.valtimoApi.endpointUri}v1/plugin/epistola`;
2600
+ this.taskContext = taskContext;
2224
2601
  // Debounce preview calls
2225
2602
  this.previewSubscription = this.previewSubject.pipe(debounceTime$1(1500)).subscribe((data) => {
2226
2603
  this.loadPreview(data);
@@ -2257,6 +2634,12 @@ class EpistolaRetryFormComponent {
2257
2634
  const processInstanceId = this.formIoStateService.processInstanceId;
2258
2635
  if (!documentId || !processInstanceId)
2259
2636
  return;
2637
+ const taskId = this.taskContext.taskInstanceId;
2638
+ if (!taskId) {
2639
+ this.previewError = 'Preview is only available from within a user task.';
2640
+ this.cdr.markForCheck();
2641
+ return;
2642
+ }
2260
2643
  this.previewLoading = true;
2261
2644
  this.previewError = null;
2262
2645
  this.cdr.markForCheck();
@@ -2265,13 +2648,14 @@ class EpistolaRetryFormComponent {
2265
2648
  URL.revokeObjectURL(this.currentBlobUrl);
2266
2649
  this.currentBlobUrl = null;
2267
2650
  }
2268
- this.http
2269
- .post(`${this.apiEndpoint}/preview`, {
2651
+ this.epistolaPluginService
2652
+ .previewToBlob({
2653
+ taskId,
2270
2654
  documentId,
2271
2655
  processInstanceId,
2272
2656
  sourceActivityId: this.sourceActivityId || null,
2273
2657
  overrides: formData,
2274
- }, { responseType: 'blob', headers: new HttpHeaders().set('X-Skip-Interceptor', '422') })
2658
+ })
2275
2659
  .subscribe({
2276
2660
  next: (blob) => {
2277
2661
  this.currentBlobUrl = URL.createObjectURL(blob);
@@ -2313,8 +2697,15 @@ class EpistolaRetryFormComponent {
2313
2697
  this.cdr.markForCheck();
2314
2698
  return;
2315
2699
  }
2700
+ const taskId = this.taskContext.taskInstanceId;
2701
+ if (!taskId) {
2702
+ this.error = 'Retry form is only available from within a user task.';
2703
+ this.loading = false;
2704
+ this.cdr.markForCheck();
2705
+ return;
2706
+ }
2316
2707
  this.loadSubscription = this.epistolaPluginService
2317
- .getRetryForm(processInstanceId, documentId ?? undefined, this.sourceActivityId)
2708
+ .getRetryForm(taskId, processInstanceId, documentId ?? undefined, this.sourceActivityId)
2318
2709
  .subscribe({
2319
2710
  next: (form) => {
2320
2711
  this.formDefinition = form;
@@ -2337,7 +2728,7 @@ class EpistolaRetryFormComponent {
2337
2728
  },
2338
2729
  });
2339
2730
  }
2340
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaRetryFormComponent, deps: [{ token: EpistolaPluginService }, { token: i3.FormIoStateService }, { token: i0.ChangeDetectorRef }, { token: i1.HttpClient }, { token: i4.DomSanitizer }, { token: i2.ConfigService }], target: i0.ɵɵFactoryTarget.Component });
2731
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaRetryFormComponent, deps: [{ token: EpistolaPluginService }, { token: i3.FormIoStateService }, { token: i0.ChangeDetectorRef }, { token: i2$4.DomSanitizer }, { token: EpistolaTaskContextService }], target: i0.ɵɵFactoryTarget.Component });
2341
2732
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaRetryFormComponent, isStandalone: true, selector: "epistola-retry-form-component", inputs: { value: "value", disabled: "disabled", label: "label", sourceActivityId: "sourceActivityId" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
2342
2733
  <div *ngIf="loading" class="epistola-retry-loading">Loading form...</div>
2343
2734
  <div *ngIf="error" class="epistola-retry-error">{{ error }}</div>
@@ -2376,7 +2767,7 @@ class EpistolaRetryFormComponent {
2376
2767
  </div>
2377
2768
  </div>
2378
2769
  </div>
2379
- `, isInline: true, styles: [".epistola-retry-loading{padding:1rem;color:#6c757d}.epistola-retry-error{padding:.5rem;color:#dc3545}.epistola-retry-container{display:flex;gap:1rem}.epistola-retry-form{flex:2;min-width:0}.epistola-retry-preview{flex:1;min-width:0;border:1px solid #dee2e6;border-radius:4px;padding:1rem;background:#f8f9fa;display:flex;flex-direction:column}.preview-expanded .epistola-retry-preview{flex:1}.preview-header{display:flex;justify-content:space-between;align-items:center;font-weight:700;margin-bottom:.5rem;color:#495057}.preview-toggle{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.2rem .5rem;font-size:.75rem;cursor:pointer}.preview-toggle:hover{background:#e9ecef}.preview-loading{color:#6c757d;font-style:italic}.preview-pdf{width:100%;flex:1;min-height:500px}.preview-expanded .preview-pdf{min-height:80vh}.preview-error{color:#dc3545}.preview-empty{color:#6c757d;font-style:italic}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormioModule }, { kind: "component", type: i7.FormioComponent, selector: "formio" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2770
+ `, isInline: true, styles: [".epistola-retry-loading{padding:1rem;color:#6c757d}.epistola-retry-error{padding:.5rem;color:#dc3545}.epistola-retry-container{display:flex;gap:1rem}.epistola-retry-form{flex:2;min-width:0}.epistola-retry-preview{flex:1;min-width:0;border:1px solid #dee2e6;border-radius:4px;padding:1rem;background:#f8f9fa;display:flex;flex-direction:column}.preview-expanded .epistola-retry-preview{flex:1}.preview-header{display:flex;justify-content:space-between;align-items:center;font-weight:700;margin-bottom:.5rem;color:#495057}.preview-toggle{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.2rem .5rem;font-size:.75rem;cursor:pointer}.preview-toggle:hover{background:#e9ecef}.preview-loading{color:#6c757d;font-style:italic}.preview-pdf{width:100%;flex:1;min-height:500px}.preview-expanded .preview-pdf{min-height:80vh}.preview-error{color:#dc3545}.preview-empty{color:#6c757d;font-style:italic}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormioModule }, { kind: "component", type: i5.FormioComponent, selector: "formio" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2380
2771
  }
2381
2772
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaRetryFormComponent, decorators: [{
2382
2773
  type: Component,
@@ -2419,7 +2810,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2419
2810
  </div>
2420
2811
  </div>
2421
2812
  `, styles: [".epistola-retry-loading{padding:1rem;color:#6c757d}.epistola-retry-error{padding:.5rem;color:#dc3545}.epistola-retry-container{display:flex;gap:1rem}.epistola-retry-form{flex:2;min-width:0}.epistola-retry-preview{flex:1;min-width:0;border:1px solid #dee2e6;border-radius:4px;padding:1rem;background:#f8f9fa;display:flex;flex-direction:column}.preview-expanded .epistola-retry-preview{flex:1}.preview-header{display:flex;justify-content:space-between;align-items:center;font-weight:700;margin-bottom:.5rem;color:#495057}.preview-toggle{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.2rem .5rem;font-size:.75rem;cursor:pointer}.preview-toggle:hover{background:#e9ecef}.preview-loading{color:#6c757d;font-style:italic}.preview-pdf{width:100%;flex:1;min-height:500px}.preview-expanded .preview-pdf{min-height:80vh}.preview-error{color:#dc3545}.preview-empty{color:#6c757d;font-style:italic}\n"] }]
2422
- }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }, { type: i1.HttpClient }, { type: i4.DomSanitizer }, { type: i2.ConfigService }], propDecorators: { value: [{
2813
+ }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }, { type: i2$4.DomSanitizer }, { type: EpistolaTaskContextService }], propDecorators: { value: [{
2423
2814
  type: Input
2424
2815
  }], valueChange: [{
2425
2816
  type: Output
@@ -2431,160 +2822,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2431
2822
  type: Input
2432
2823
  }] } });
2433
2824
 
2434
- class EpistolaPreviewButtonComponent {
2435
- http;
2436
- sanitizer;
2437
- configService;
2438
- value;
2439
- valueChange = new EventEmitter();
2440
- disabled = false;
2441
- label = 'Preview PDF';
2442
- modalOpen = false;
2443
- loading = false;
2444
- previewLoading = false;
2445
- previewError = null;
2446
- previewUrl = null;
2447
- currentBlobUrl = null;
2448
- apiEndpoint;
2449
- get buttonLabel() {
2450
- return this.label || 'Preview PDF';
2451
- }
2452
- constructor(http, sanitizer, configService) {
2453
- this.http = http;
2454
- this.sanitizer = sanitizer;
2455
- this.configService = configService;
2456
- this.apiEndpoint = `${this.configService.config.valtimoApi.endpointUri}v1/plugin/epistola`;
2457
- }
2458
- ngOnDestroy() {
2459
- this.revokeBlobUrl();
2460
- }
2461
- hasRequiredData() {
2462
- return !!(this.value?.documentId && this.value?.tenantId);
2463
- }
2464
- openPreview() {
2465
- if (!this.hasRequiredData() || this.loading)
2466
- return;
2467
- this.modalOpen = true;
2468
- this.previewLoading = true;
2469
- this.previewError = null;
2470
- this.revokeBlobUrl();
2471
- const { documentId, tenantId } = this.value;
2472
- const url = `${this.apiEndpoint}/documents/${encodeURIComponent(documentId)}/download` +
2473
- `?tenantId=${encodeURIComponent(tenantId)}` +
2474
- `&filename=preview.pdf`;
2475
- this.http.get(url, { responseType: 'blob' }).subscribe({
2476
- next: (blob) => {
2477
- this.currentBlobUrl = URL.createObjectURL(blob);
2478
- this.previewUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.currentBlobUrl);
2479
- this.previewLoading = false;
2480
- },
2481
- error: () => {
2482
- this.previewError = 'Could not load the document.';
2483
- this.previewLoading = false;
2484
- },
2485
- });
2486
- }
2487
- closePreview() {
2488
- this.modalOpen = false;
2489
- this.revokeBlobUrl();
2490
- this.previewUrl = null;
2491
- this.previewError = null;
2492
- }
2493
- revokeBlobUrl() {
2494
- if (this.currentBlobUrl) {
2495
- URL.revokeObjectURL(this.currentBlobUrl);
2496
- this.currentBlobUrl = null;
2497
- }
2498
- }
2499
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaPreviewButtonComponent, deps: [{ token: i1.HttpClient }, { token: i4.DomSanitizer }, { token: i2.ConfigService }], target: i0.ɵɵFactoryTarget.Component });
2500
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaPreviewButtonComponent, isStandalone: true, selector: "epistola-preview-button-component", inputs: { value: "value", disabled: "disabled", label: "label" }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: `
2501
- <button
2502
- type="button"
2503
- class="btn btn-outline-secondary"
2504
- [disabled]="disabled || loading || !hasRequiredData()"
2505
- (click)="openPreview()"
2506
- >
2507
- <i class="mdi mdi-eye mr-1"></i>
2508
- {{ loading ? 'Loading...' : buttonLabel }}
2509
- </button>
2510
-
2511
- <div *ngIf="modalOpen" class="preview-modal-overlay" (click)="closePreview()">
2512
- <div class="preview-modal-content" (click)="$event.stopPropagation()">
2513
- <div class="preview-modal-header">
2514
- <span>Document Preview</span>
2515
- <button type="button" class="preview-modal-close" (click)="closePreview()">
2516
- &times;
2517
- </button>
2518
- </div>
2519
- <div class="preview-modal-body">
2520
- <div *ngIf="previewLoading" class="preview-loading">Generating preview...</div>
2521
- <div *ngIf="previewError" class="preview-error">{{ previewError }}</div>
2522
- <object
2523
- *ngIf="previewUrl && !previewLoading"
2524
- [data]="previewUrl"
2525
- type="application/pdf"
2526
- class="preview-pdf"
2527
- >
2528
- PDF preview is not supported in this browser.
2529
- </object>
2530
- </div>
2531
- </div>
2532
- </div>
2533
- `, isInline: true, styles: [".preview-modal-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:10000}.preview-modal-content{background:#fff;border-radius:8px;width:90vw;height:90vh;max-width:1200px;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 8px 32px #0000004d}.preview-modal-header{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;border-bottom:1px solid #dee2e6;font-weight:700;font-size:1rem}.preview-modal-close{background:none;border:none;font-size:1.5rem;cursor:pointer;color:#6c757d;line-height:1;padding:0 .25rem}.preview-modal-close:hover{color:#333}.preview-modal-body{flex:1;overflow:hidden;display:flex;flex-direction:column}.preview-loading,.preview-error{padding:2rem;text-align:center}.preview-error{color:#dc3545}.preview-pdf{width:100%;flex:1}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
2534
- }
2535
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaPreviewButtonComponent, decorators: [{
2536
- type: Component,
2537
- args: [{ standalone: true, imports: [CommonModule], selector: 'epistola-preview-button-component', template: `
2538
- <button
2539
- type="button"
2540
- class="btn btn-outline-secondary"
2541
- [disabled]="disabled || loading || !hasRequiredData()"
2542
- (click)="openPreview()"
2543
- >
2544
- <i class="mdi mdi-eye mr-1"></i>
2545
- {{ loading ? 'Loading...' : buttonLabel }}
2546
- </button>
2547
-
2548
- <div *ngIf="modalOpen" class="preview-modal-overlay" (click)="closePreview()">
2549
- <div class="preview-modal-content" (click)="$event.stopPropagation()">
2550
- <div class="preview-modal-header">
2551
- <span>Document Preview</span>
2552
- <button type="button" class="preview-modal-close" (click)="closePreview()">
2553
- &times;
2554
- </button>
2555
- </div>
2556
- <div class="preview-modal-body">
2557
- <div *ngIf="previewLoading" class="preview-loading">Generating preview...</div>
2558
- <div *ngIf="previewError" class="preview-error">{{ previewError }}</div>
2559
- <object
2560
- *ngIf="previewUrl && !previewLoading"
2561
- [data]="previewUrl"
2562
- type="application/pdf"
2563
- class="preview-pdf"
2564
- >
2565
- PDF preview is not supported in this browser.
2566
- </object>
2567
- </div>
2568
- </div>
2569
- </div>
2570
- `, styles: [".preview-modal-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:10000}.preview-modal-content{background:#fff;border-radius:8px;width:90vw;height:90vh;max-width:1200px;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 8px 32px #0000004d}.preview-modal-header{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;border-bottom:1px solid #dee2e6;font-weight:700;font-size:1rem}.preview-modal-close{background:none;border:none;font-size:1.5rem;cursor:pointer;color:#6c757d;line-height:1;padding:0 .25rem}.preview-modal-close:hover{color:#333}.preview-modal-body{flex:1;overflow:hidden;display:flex;flex-direction:column}.preview-loading,.preview-error{padding:2rem;text-align:center}.preview-error{color:#dc3545}.preview-pdf{width:100%;flex:1}\n"] }]
2571
- }], ctorParameters: () => [{ type: i1.HttpClient }, { type: i4.DomSanitizer }, { type: i2.ConfigService }], propDecorators: { value: [{
2572
- type: Input
2573
- }], valueChange: [{
2574
- type: Output
2575
- }], disabled: [{
2576
- type: Input
2577
- }], label: [{
2578
- type: Input
2579
- }] } });
2580
-
2581
2825
  class EpistolaDocumentPreviewComponent {
2582
2826
  epistolaPluginService;
2583
- http;
2584
2827
  sanitizer;
2585
- configService;
2586
2828
  formIoStateService;
2587
2829
  cdr;
2830
+ taskContext;
2588
2831
  value;
2589
2832
  valueChange = new EventEmitter();
2590
2833
  disabled = false;
@@ -2592,30 +2835,27 @@ class EpistolaDocumentPreviewComponent {
2592
2835
  processDefinitionKey;
2593
2836
  sourceActivityId;
2594
2837
  overrideMapping;
2595
- sources = [];
2596
- selectedIndex = 0;
2597
- discovering = false;
2598
2838
  loading = false;
2599
2839
  error = null;
2600
2840
  previewUrl = null;
2601
2841
  designMode = false;
2602
2842
  initialized = false;
2603
2843
  currentBlobUrl = null;
2604
- discoverSubscription;
2605
2844
  previewSubscription;
2606
- apiEndpoint;
2607
- /** Whether the component is in configured mode (explicit process link) vs auto-discover mode */
2608
- get configuredMode() {
2609
- return !!this.sourceActivityId;
2610
- }
2611
- constructor(epistolaPluginService, http, sanitizer, configService, formIoStateService, cdr) {
2845
+ constructor(epistolaPluginService, sanitizer, formIoStateService, cdr, taskContext) {
2612
2846
  this.epistolaPluginService = epistolaPluginService;
2613
- this.http = http;
2614
2847
  this.sanitizer = sanitizer;
2615
- this.configService = configService;
2616
2848
  this.formIoStateService = formIoStateService;
2617
2849
  this.cdr = cdr;
2618
- this.apiEndpoint = `${this.configService.config.valtimoApi.endpointUri}v1/plugin/epistola`;
2850
+ this.taskContext = taskContext;
2851
+ }
2852
+ /**
2853
+ * Resolve the active task id from {@link EpistolaTaskContextService}, populated
2854
+ * by {@code EpistolaTaskContextInterceptor} on the canonical Valtimo task-open
2855
+ * call. Returns null when used outside a task context (e.g. Formio builder).
2856
+ */
2857
+ get currentTaskId() {
2858
+ return this.taskContext.taskInstanceId;
2619
2859
  }
2620
2860
  get overrideMappingScopes() {
2621
2861
  return this.overrideMapping ? Object.keys(this.overrideMapping) : [];
@@ -2636,122 +2876,69 @@ class EpistolaDocumentPreviewComponent {
2636
2876
  this.cdr.markForCheck();
2637
2877
  return;
2638
2878
  }
2639
- if (this.configuredMode) {
2640
- this.loadConfiguredPreview();
2641
- }
2642
- else {
2643
- this.discoverSources();
2879
+ if (!this.sourceActivityId) {
2880
+ this.error = 'Preview is not configured: set the source activity on the form component.';
2881
+ this.cdr.markForCheck();
2882
+ return;
2644
2883
  }
2884
+ this.loadPreview();
2645
2885
  return;
2646
2886
  }
2647
- // In configured mode, react to value changes (input overrides from Formio wrapper)
2648
- if (this.configuredMode && changes['value']) {
2649
- this.loadConfiguredPreview();
2887
+ // React to value changes (input overrides from the Formio wrapper).
2888
+ if (changes['value']) {
2889
+ this.loadPreview();
2650
2890
  }
2651
2891
  }
2652
2892
  ngOnDestroy() {
2653
- this.discoverSubscription?.unsubscribe();
2654
2893
  this.previewSubscription?.unsubscribe();
2655
2894
  this.revokeBlobUrl();
2656
2895
  }
2657
- onSourceChange(event) {
2658
- this.selectedIndex = +event.target.value;
2659
- this.loadDiscoveredPreview();
2660
- }
2661
2896
  refresh() {
2662
- if (this.configuredMode) {
2663
- this.loadConfiguredPreview();
2664
- }
2665
- else {
2666
- this.loadDiscoveredPreview();
2667
- }
2897
+ this.loadPreview();
2668
2898
  }
2669
2899
  /**
2670
- * Configured mode: preview using the explicitly configured process link + input overrides.
2900
+ * Preview using the explicitly configured process link + input overrides.
2901
+ * Requires a runtime task context — the backend authorizes the request against
2902
+ * the task's process instance and case document, so all three ids must match.
2671
2903
  */
2672
- loadConfiguredPreview() {
2904
+ loadPreview() {
2673
2905
  const documentId = this.formIoStateService.documentId;
2674
2906
  if (!documentId) {
2675
2907
  this.error = 'Could not determine document ID from context.';
2676
2908
  this.cdr.markForCheck();
2677
2909
  return;
2678
2910
  }
2679
- this.loading = true;
2680
- this.error = null;
2681
- this.cdr.markForCheck();
2682
- this.revokeBlobUrl();
2683
- this.previewSubscription?.unsubscribe();
2684
- this.previewSubscription = this.http
2685
- .post(`${this.apiEndpoint}/preview`, {
2686
- documentId,
2687
- processDefinitionKey: this.processDefinitionKey || null,
2688
- processInstanceId: this.formIoStateService.processInstanceId || null,
2689
- sourceActivityId: this.sourceActivityId,
2690
- inputOverrides: this.value || null,
2691
- overrides: null,
2692
- }, {
2693
- responseType: 'blob',
2694
- headers: new HttpHeaders().set('X-Skip-Interceptor', '422'),
2695
- })
2696
- .subscribe({
2697
- next: (blob) => this.handlePreviewSuccess(blob),
2698
- error: (err) => this.handlePreviewError(err),
2699
- });
2700
- }
2701
- /**
2702
- * Auto-discover mode: discover sources from running process instances.
2703
- */
2704
- discoverSources() {
2705
- const documentId = this.formIoStateService.documentId;
2706
- if (!documentId) {
2707
- this.error = 'Could not determine document ID from context.';
2911
+ if (!this.sourceActivityId) {
2912
+ this.error = 'Preview is not configured: set the source activity on the form component.';
2708
2913
  this.cdr.markForCheck();
2709
2914
  return;
2710
2915
  }
2711
- this.discovering = true;
2712
- this.error = null;
2713
- this.cdr.markForCheck();
2714
- this.discoverSubscription = this.epistolaPluginService.getPreviewSources(documentId).subscribe({
2715
- next: (sources) => {
2716
- this.sources = sources;
2717
- this.discovering = false;
2718
- this.cdr.markForCheck();
2719
- if (sources.length > 0) {
2720
- this.selectedIndex = 0;
2721
- this.loadDiscoveredPreview();
2722
- }
2723
- },
2724
- error: (err) => {
2725
- this.error = err.error?.error || 'Failed to discover preview sources';
2726
- this.discovering = false;
2727
- this.cdr.markForCheck();
2728
- },
2729
- });
2730
- }
2731
- /**
2732
- * Auto-discover mode: load preview for the selected discovered source.
2733
- */
2734
- loadDiscoveredPreview() {
2735
- const source = this.sources[this.selectedIndex];
2736
- if (!source)
2916
+ const processInstanceId = this.formIoStateService.processInstanceId;
2917
+ if (!processInstanceId) {
2918
+ this.error = 'Preview is only available from within a running process.';
2919
+ this.cdr.markForCheck();
2737
2920
  return;
2738
- const documentId = this.formIoStateService.documentId;
2739
- if (!documentId)
2921
+ }
2922
+ const taskId = this.currentTaskId;
2923
+ if (!taskId) {
2924
+ this.error = 'Preview is only available from within a user task.';
2925
+ this.cdr.markForCheck();
2740
2926
  return;
2927
+ }
2741
2928
  this.loading = true;
2742
2929
  this.error = null;
2743
2930
  this.cdr.markForCheck();
2744
2931
  this.revokeBlobUrl();
2745
2932
  this.previewSubscription?.unsubscribe();
2746
- this.previewSubscription = this.http
2747
- .post(`${this.apiEndpoint}/preview`, {
2933
+ this.previewSubscription = this.epistolaPluginService
2934
+ .previewToBlob({
2935
+ taskId,
2748
2936
  documentId,
2749
- processInstanceId: source.processInstanceId,
2750
- sourceActivityId: source.activityId,
2937
+ processDefinitionKey: this.processDefinitionKey || null,
2938
+ processInstanceId,
2939
+ sourceActivityId: this.sourceActivityId,
2940
+ inputOverrides: this.value || null,
2751
2941
  overrides: null,
2752
- }, {
2753
- responseType: 'blob',
2754
- headers: new HttpHeaders().set('X-Skip-Interceptor', '422'),
2755
2942
  })
2756
2943
  .subscribe({
2757
2944
  next: (blob) => this.handlePreviewSuccess(blob),
@@ -2793,7 +2980,7 @@ class EpistolaDocumentPreviewComponent {
2793
2980
  this.previewUrl = null;
2794
2981
  }
2795
2982
  }
2796
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDocumentPreviewComponent, deps: [{ token: EpistolaPluginService }, { token: i1.HttpClient }, { token: i4.DomSanitizer }, { token: i2.ConfigService }, { token: i3.FormIoStateService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
2983
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDocumentPreviewComponent, deps: [{ token: EpistolaPluginService }, { token: i2$4.DomSanitizer }, { token: i3.FormIoStateService }, { token: i0.ChangeDetectorRef }, { token: EpistolaTaskContextService }], target: i0.ɵɵFactoryTarget.Component });
2797
2984
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaDocumentPreviewComponent, isStandalone: true, selector: "epistola-document-preview-component", inputs: { value: "value", disabled: "disabled", label: "label", processDefinitionKey: "processDefinitionKey", sourceActivityId: "sourceActivityId", overrideMapping: "overrideMapping" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
2798
2985
  <!-- Design-time view: show configuration summary when no runtime context -->
2799
2986
  <div *ngIf="designMode" class="epistola-preview-panel">
@@ -2829,55 +3016,26 @@ class EpistolaDocumentPreviewComponent {
2829
3016
  <div class="preview-header">
2830
3017
  <span>{{ label || 'Document Preview' }}</span>
2831
3018
  <div class="preview-controls">
2832
- <select
2833
- *ngIf="!sourceActivityId && sources.length > 1"
2834
- class="preview-select"
2835
- [value]="selectedIndex"
2836
- (change)="onSourceChange($event)"
2837
- >
2838
- <option *ngFor="let source of sources; let i = index" [value]="i">
2839
- {{ source.templateName }} ({{ source.activityId }})
2840
- </option>
2841
- </select>
2842
- <button
2843
- type="button"
2844
- class="preview-refresh"
2845
- [disabled]="loading || discovering"
2846
- (click)="refresh()"
2847
- >
3019
+ <button type="button" class="preview-refresh" [disabled]="loading" (click)="refresh()">
2848
3020
  <i class="mdi mdi-refresh mr-1"></i>
2849
3021
  {{ loading ? 'Generating...' : 'Refresh' }}
2850
3022
  </button>
2851
3023
  </div>
2852
3024
  </div>
2853
3025
  <div class="preview-body">
2854
- <div *ngIf="discovering" class="preview-loading">Discovering documents...</div>
2855
- <div *ngIf="loading && !discovering" class="preview-loading">Generating preview...</div>
2856
- <div *ngIf="error && !loading && !discovering" class="preview-unavailable">
3026
+ <div *ngIf="loading" class="preview-loading">Generating preview...</div>
3027
+ <div *ngIf="error && !loading" class="preview-unavailable">
2857
3028
  <i class="mdi mdi-information-outline"></i>
2858
- Preview is niet beschikbaar — niet alle gegevens zijn al ingevuld.
3029
+ {{ error }}
2859
3030
  </div>
2860
3031
  <object
2861
- *ngIf="previewUrl && !loading && !discovering"
3032
+ *ngIf="previewUrl && !loading"
2862
3033
  [data]="previewUrl"
2863
3034
  type="application/pdf"
2864
3035
  class="preview-pdf"
2865
3036
  >
2866
3037
  PDF preview is not supported in this browser.
2867
3038
  </object>
2868
- <div
2869
- *ngIf="
2870
- !previewUrl &&
2871
- !loading &&
2872
- !discovering &&
2873
- !error &&
2874
- !sourceActivityId &&
2875
- sources.length === 0
2876
- "
2877
- class="preview-empty"
2878
- >
2879
- No previewable documents found
2880
- </div>
2881
3039
  </div>
2882
3040
  </div>
2883
3041
  `, isInline: true, styles: [".epistola-preview-panel{border:1px solid #dee2e6;border-radius:4px;background:#f8f9fa;display:flex;flex-direction:column}.preview-header{display:flex;justify-content:space-between;align-items:center;padding:.5rem 1rem;border-bottom:1px solid #dee2e6;font-weight:700;color:#495057;flex-wrap:wrap;gap:.5rem}.preview-controls{display:flex;align-items:center;gap:.5rem}.preview-select{border:1px solid #ced4da;border-radius:4px;padding:.25rem .5rem;font-size:.8rem;background:#fff;max-width:300px}.preview-refresh{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.25rem .75rem;font-size:.8rem;cursor:pointer;display:flex;align-items:center;white-space:nowrap}.preview-refresh:hover:not(:disabled){background:#e9ecef}.preview-refresh:disabled{opacity:.5;cursor:not-allowed}.preview-body{display:flex;flex-direction:column;min-height:500px}.preview-loading{padding:2rem;text-align:center;color:#6c757d;font-style:italic}.preview-unavailable{padding:1.5rem;text-align:center;color:#6c757d;font-style:italic}.preview-unavailable i{margin-right:.25rem}.preview-pdf{width:100%;flex:1;min-height:500px}.preview-empty{padding:2rem;text-align:center;color:#6c757d;font-style:italic}.design-info{padding:1rem;min-height:auto}.design-section{margin-bottom:.75rem}.design-label{font-size:.7rem;text-transform:uppercase;color:#868e96;font-weight:600;letter-spacing:.05em}.design-value{font-family:monospace;font-size:.85rem;color:#212529;margin-bottom:.25rem}.design-mapping{margin-top:.25rem}.design-entry{font-family:monospace;font-size:.8rem;color:#495057;padding:.15rem 0}.design-scope{color:#0d6efd}.design-field{color:#198754}.design-entry i{font-size:.7rem;margin:0 .25rem;color:#adb5bd}.design-unconfigured{color:#6c757d;font-style:italic;font-size:.85rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
@@ -2919,59 +3077,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2919
3077
  <div class="preview-header">
2920
3078
  <span>{{ label || 'Document Preview' }}</span>
2921
3079
  <div class="preview-controls">
2922
- <select
2923
- *ngIf="!sourceActivityId && sources.length > 1"
2924
- class="preview-select"
2925
- [value]="selectedIndex"
2926
- (change)="onSourceChange($event)"
2927
- >
2928
- <option *ngFor="let source of sources; let i = index" [value]="i">
2929
- {{ source.templateName }} ({{ source.activityId }})
2930
- </option>
2931
- </select>
2932
- <button
2933
- type="button"
2934
- class="preview-refresh"
2935
- [disabled]="loading || discovering"
2936
- (click)="refresh()"
2937
- >
3080
+ <button type="button" class="preview-refresh" [disabled]="loading" (click)="refresh()">
2938
3081
  <i class="mdi mdi-refresh mr-1"></i>
2939
3082
  {{ loading ? 'Generating...' : 'Refresh' }}
2940
3083
  </button>
2941
3084
  </div>
2942
3085
  </div>
2943
3086
  <div class="preview-body">
2944
- <div *ngIf="discovering" class="preview-loading">Discovering documents...</div>
2945
- <div *ngIf="loading && !discovering" class="preview-loading">Generating preview...</div>
2946
- <div *ngIf="error && !loading && !discovering" class="preview-unavailable">
3087
+ <div *ngIf="loading" class="preview-loading">Generating preview...</div>
3088
+ <div *ngIf="error && !loading" class="preview-unavailable">
2947
3089
  <i class="mdi mdi-information-outline"></i>
2948
- Preview is niet beschikbaar — niet alle gegevens zijn al ingevuld.
3090
+ {{ error }}
2949
3091
  </div>
2950
3092
  <object
2951
- *ngIf="previewUrl && !loading && !discovering"
3093
+ *ngIf="previewUrl && !loading"
2952
3094
  [data]="previewUrl"
2953
3095
  type="application/pdf"
2954
3096
  class="preview-pdf"
2955
3097
  >
2956
3098
  PDF preview is not supported in this browser.
2957
3099
  </object>
2958
- <div
2959
- *ngIf="
2960
- !previewUrl &&
2961
- !loading &&
2962
- !discovering &&
2963
- !error &&
2964
- !sourceActivityId &&
2965
- sources.length === 0
2966
- "
2967
- class="preview-empty"
2968
- >
2969
- No previewable documents found
2970
- </div>
2971
3100
  </div>
2972
3101
  </div>
2973
3102
  `, styles: [".epistola-preview-panel{border:1px solid #dee2e6;border-radius:4px;background:#f8f9fa;display:flex;flex-direction:column}.preview-header{display:flex;justify-content:space-between;align-items:center;padding:.5rem 1rem;border-bottom:1px solid #dee2e6;font-weight:700;color:#495057;flex-wrap:wrap;gap:.5rem}.preview-controls{display:flex;align-items:center;gap:.5rem}.preview-select{border:1px solid #ced4da;border-radius:4px;padding:.25rem .5rem;font-size:.8rem;background:#fff;max-width:300px}.preview-refresh{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.25rem .75rem;font-size:.8rem;cursor:pointer;display:flex;align-items:center;white-space:nowrap}.preview-refresh:hover:not(:disabled){background:#e9ecef}.preview-refresh:disabled{opacity:.5;cursor:not-allowed}.preview-body{display:flex;flex-direction:column;min-height:500px}.preview-loading{padding:2rem;text-align:center;color:#6c757d;font-style:italic}.preview-unavailable{padding:1.5rem;text-align:center;color:#6c757d;font-style:italic}.preview-unavailable i{margin-right:.25rem}.preview-pdf{width:100%;flex:1;min-height:500px}.preview-empty{padding:2rem;text-align:center;color:#6c757d;font-style:italic}.design-info{padding:1rem;min-height:auto}.design-section{margin-bottom:.75rem}.design-label{font-size:.7rem;text-transform:uppercase;color:#868e96;font-weight:600;letter-spacing:.05em}.design-value{font-family:monospace;font-size:.85rem;color:#212529;margin-bottom:.25rem}.design-mapping{margin-top:.25rem}.design-entry{font-family:monospace;font-size:.8rem;color:#495057;padding:.15rem 0}.design-scope{color:#0d6efd}.design-field{color:#198754}.design-entry i{font-size:.7rem;margin:0 .25rem;color:#adb5bd}.design-unconfigured{color:#6c757d;font-style:italic;font-size:.85rem}\n"] }]
2974
- }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i1.HttpClient }, { type: i4.DomSanitizer }, { type: i2.ConfigService }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }], propDecorators: { value: [{
3103
+ }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$4.DomSanitizer }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }, { type: EpistolaTaskContextService }], propDecorators: { value: [{
2975
3104
  type: Input
2976
3105
  }], valueChange: [{
2977
3106
  type: Output
@@ -2994,8 +3123,12 @@ class EpistolaAdminPageComponent {
2994
3123
  cards = [];
2995
3124
  selectedCard = null;
2996
3125
  activeTab = 'actions';
3126
+ overviewTab = 'configurations';
2997
3127
  loading = false;
2998
3128
  pluginVersion = null;
3129
+ validationViolations = [];
3130
+ reconcilingExecutionIds = new Set();
3131
+ reconcileFeedback = null;
2999
3132
  connectionStatuses = [];
3000
3133
  usageEntries = [];
3001
3134
  pendingJobs = [];
@@ -3031,6 +3164,9 @@ class EpistolaAdminPageComponent {
3031
3164
  this.activeTab = tab;
3032
3165
  this.updateUrl(this.selectedCard?.configurationId ?? null, tab);
3033
3166
  }
3167
+ setOverviewTab(tab) {
3168
+ this.overviewTab = tab;
3169
+ }
3034
3170
  refresh() {
3035
3171
  this.selectedCard = null;
3036
3172
  this.loadData();
@@ -3047,6 +3183,53 @@ class EpistolaAdminPageComponent {
3047
3183
  },
3048
3184
  });
3049
3185
  }
3186
+ /**
3187
+ * Manually reconcile a single stuck catch event. Pulls the current Epistola job
3188
+ * status and re-runs message correlation if the job is in a terminal state.
3189
+ * Refreshes the Pending list on success so the row drops out of the table.
3190
+ */
3191
+ reconcilePending(job) {
3192
+ if (this.reconcilingExecutionIds.has(job.executionId)) {
3193
+ return;
3194
+ }
3195
+ this.reconcilingExecutionIds.add(job.executionId);
3196
+ this.reconcileFeedback = null;
3197
+ this.adminService.reconcilePending(job.executionId).subscribe({
3198
+ next: (result) => {
3199
+ this.reconcilingExecutionIds.delete(job.executionId);
3200
+ this.reconcileFeedback = {
3201
+ executionId: job.executionId,
3202
+ type: 'success',
3203
+ message: `OK (${result.epistolaStatus}, ${result.correlatedCount ?? 0} correlated)`,
3204
+ };
3205
+ this.loadData();
3206
+ },
3207
+ error: (err) => {
3208
+ this.reconcilingExecutionIds.delete(job.executionId);
3209
+ // 409 from the backend = job is still PENDING/IN_PROGRESS, surface as
3210
+ // informational rather than an error.
3211
+ if (err?.status === 409) {
3212
+ const status = err.error?.epistolaStatus ?? 'still pending';
3213
+ this.reconcileFeedback = {
3214
+ executionId: job.executionId,
3215
+ type: 'pending',
3216
+ message: `Epistola: ${status}. Try again in a moment.`,
3217
+ };
3218
+ }
3219
+ else {
3220
+ const message = err?.error?.detail ?? err?.error?.message ?? err?.message ?? 'unknown error';
3221
+ this.reconcileFeedback = {
3222
+ executionId: job.executionId,
3223
+ type: 'error',
3224
+ message,
3225
+ };
3226
+ }
3227
+ },
3228
+ });
3229
+ }
3230
+ isReconciling(job) {
3231
+ return this.reconcilingExecutionIds.has(job.executionId);
3232
+ }
3050
3233
  updateUrl(configurationId, tab) {
3051
3234
  this.router.navigate([], {
3052
3235
  relativeTo: this.route,
@@ -3098,6 +3281,16 @@ class EpistolaAdminPageComponent {
3098
3281
  this.tryBuildCards();
3099
3282
  },
3100
3283
  });
3284
+ // Validation violations are independent of cards — load alongside but don't
3285
+ // gate the loading flag on them.
3286
+ this.adminService.getValidationViolations().subscribe({
3287
+ next: (violations) => {
3288
+ this.validationViolations = violations;
3289
+ },
3290
+ error: () => {
3291
+ this.validationViolations = [];
3292
+ },
3293
+ });
3101
3294
  }
3102
3295
  tryBuildCards() {
3103
3296
  if (!this.connectionLoaded || !this.usageLoaded || !this.pendingLoaded) {
@@ -3138,13 +3331,13 @@ class EpistolaAdminPageComponent {
3138
3331
  },
3139
3332
  });
3140
3333
  }
3141
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminPageComponent, deps: [{ token: EpistolaAdminService }, { token: i2$4.ActivatedRoute }, { token: i2$4.Router }], target: i0.ɵɵFactoryTarget.Component });
3142
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaAdminPageComponent, isStandalone: true, selector: "epistola-admin-page", ngImport: i0, template: "<div class=\"epistola-admin\">\n <!-- Overview: card grid (no configuration selected) -->\n <ng-container *ngIf=\"!selectedCard\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"d-flex align-items-center\">\n <h5 class=\"mb-0\">{{ 'epistolaAdminOverview' | pluginTranslate: 'epistola' | async }}</h5>\n <span *ngIf=\"pluginVersion\" class=\"version-badge ms-2\">v{{ pluginVersion }}</span>\n </div>\n <button class=\"btn btn-outline-primary btn-sm\" (click)=\"refresh()\" [disabled]=\"loading\">\n {{ 'epistolaAdminRefresh' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div *ngIf=\"loading\" class=\"text-muted\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length === 0\" class=\"text-muted\">\n {{ 'epistolaAdminNoConfigurations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length > 0\" class=\"card-grid\">\n <div\n *ngFor=\"let card of cards\"\n class=\"config-card\"\n [class.config-card--ok]=\"card.reachable && card.problemCount === 0\"\n [class.config-card--warning]=\"card.reachable && card.problemCount > 0\"\n [class.config-card--error]=\"!card.reachable\"\n (click)=\"selectConfiguration(card)\"\n >\n <div class=\"config-card__header\">\n <span\n class=\"status-dot\"\n [class.status-dot--ok]=\"card.reachable\"\n [class.status-dot--error]=\"!card.reachable\"\n >\n </span>\n <h5 class=\"config-card__title\">{{ card.configurationTitle }}</h5>\n </div>\n\n <div class=\"config-card__body\">\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async\n }}</span>\n <code class=\"config-card__value\">{{ card.tenantId }}</code>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" [type]=\"card.reachable ? 'green' : 'red'\">\n {{\n card.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n </div>\n <div *ngIf=\"card.serverVersion\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.serverVersion }}</span>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.usageCount }}</span>\n </div>\n <div *ngIf=\"card.pendingJobs.length > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">{{ card.pendingJobs.length }}</cds-tag>\n </div>\n <div *ngIf=\"card.problemCount > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"red\">{{ card.problemCount }}</cds-tag>\n </div>\n </div>\n\n <div class=\"config-card__footer\">\n <span class=\"config-card__latency\">{{ card.latencyMs }} ms</span>\n </div>\n </div>\n </div>\n </ng-container>\n\n <!-- Detail view: selected configuration -->\n <ng-container *ngIf=\"selectedCard\">\n <div class=\"detail-header mb-3\">\n <button class=\"btn btn-link btn-sm p-0\" (click)=\"backToOverview()\">\n &larr; {{ 'epistolaAdminBackToOverview' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div class=\"detail-summary mb-4\">\n <h4>\n <span\n class=\"status-dot me-2\"\n [class.status-dot--ok]=\"selectedCard.reachable\"\n [class.status-dot--error]=\"!selectedCard.reachable\"\n >\n </span>\n {{ selectedCard.configurationTitle }}\n </h4>\n\n <table class=\"table table-sm detail-info-table\">\n <tbody>\n <tr>\n <th>{{ 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <code>{{ selectedCard.tenantId }}</code>\n </td>\n </tr>\n <tr>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <cds-tag size=\"sm\" [type]=\"selectedCard.reachable ? 'green' : 'red'\">\n {{\n selectedCard.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n <span class=\"text-muted ms-2\">{{ selectedCard.latencyMs }} ms</span>\n </td>\n </tr>\n <tr *ngIf=\"selectedCard.serverVersion\">\n <th>{{ 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async }}</th>\n <td>{{ selectedCard.serverVersion }}</td>\n </tr>\n <tr *ngIf=\"selectedCard.errorMessage\">\n <th>{{ 'epistolaAdminError' | pluginTranslate: 'epistola' | async }}</th>\n <td class=\"text-danger\">{{ selectedCard.errorMessage }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Tabs -->\n <ng-template #actionsHeading>\n {{ 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ selectedCard.usageEntries.length }}</cds-tag>\n </ng-template>\n\n <ng-template #pendingHeading>\n {{ 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n size=\"sm\"\n [type]=\"selectedCard.pendingJobs.length > 0 ? 'blue' : 'gray'\"\n class=\"ms-1\"\n >\n {{ selectedCard.pendingJobs.length }}\n </cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"actionsHeading\"\n [active]=\"activeTab === 'actions'\"\n (selected)=\"setActiveTab('actions')\"\n >\n <div *ngIf=\"selectedCard.usageEntries.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoUsageForConfig' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.usageEntries.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCase' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminAction' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let entry of selectedCard.usageEntries\"\n [class.table-warning]=\"entry.problems.length > 0\"\n >\n <td>{{ entry.caseDefinitionKey || '-' }}</td>\n <td>\n <a\n *ngIf=\"entry.caseDefinitionKey && entry.caseDefinitionVersionTag\"\n [routerLink]=\"[\n '/case-management',\n 'case',\n entry.caseDefinitionKey,\n 'version',\n entry.caseDefinitionVersionTag,\n 'processes',\n entry.processDefinitionKey,\n ]\"\n class=\"usage-link\"\n >\n {{ entry.processDefinitionName }}\n </a>\n <span *ngIf=\"!entry.caseDefinitionKey || !entry.caseDefinitionVersionTag\">\n {{ entry.processDefinitionName }}\n </span>\n </td>\n <td>{{ entry.activityName }}</td>\n <td>\n <code>{{ entry.actionKey }}</code>\n </td>\n <td>\n <cds-tag *ngIf=\"entry.problems.length === 0\" size=\"sm\" type=\"green\">OK</cds-tag>\n <cds-tag\n *ngFor=\"let problem of entry.problems\"\n size=\"sm\"\n type=\"red\"\n class=\"d-block mb-1\"\n >\n {{ problem }}\n </cds-tag>\n </td>\n <td>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"exportProcessLink(entry)\"\n [title]=\"'epistolaAdminExport' | pluginTranslate: 'epistola' | async\"\n >\n &#x2913;\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"pendingHeading\"\n [active]=\"activeTab === 'pending'\"\n (selected)=\"setActiveTab('pending')\"\n >\n <div *ngIf=\"selectedCard.pendingJobs.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoPendingJobs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.pendingJobs.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminRequestId' | pluginTranslate: 'epistola' | async }}</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let job of selectedCard.pendingJobs\">\n <td>{{ job.processDefinitionName }}</td>\n <td>{{ job.activityName }}</td>\n <td>\n <code>{{ job.requestId }}</code>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n</div>\n", styles: [".epistola-admin{padding:1.5rem}.epistola-admin .version-badge{font-size:.75rem;font-weight:500;padding:.2em .6em;border-radius:4px;background-color:#e8e8e8;color:#525252}.epistola-admin .badge{font-size:.85em;padding:.35em .65em}.epistola-admin code{font-size:.9em;color:#525252}.epistola-admin table th{font-weight:600;white-space:nowrap}.epistola-admin .status-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:.5rem;flex-shrink:0;background-color:#adb5bd}.epistola-admin .status-dot--ok{background-color:#198754}.epistola-admin .status-dot--error{background-color:#dc3545}.epistola-admin .card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.epistola-admin .config-card{border:1px solid #dee2e6;border-radius:8px;padding:1.25rem;cursor:pointer;transition:box-shadow .15s ease,border-color .15s ease;background:#fff}.epistola-admin .config-card:hover{box-shadow:0 2px 8px #0000001a}.epistola-admin .config-card--ok{border-left:4px solid #198754}.epistola-admin .config-card--warning{border-left:4px solid #ffc107}.epistola-admin .config-card--error{border-left:4px solid #dc3545}.epistola-admin .config-card__header{display:flex;align-items:center;margin-bottom:1rem}.epistola-admin .config-card__title{margin:0;font-size:1.05rem;font-weight:600;color:#161616}.epistola-admin .config-card__body{display:flex;flex-direction:column;gap:.5rem}.epistola-admin .config-card__field{display:flex;justify-content:space-between;align-items:center}.epistola-admin .config-card__label{font-size:.875rem;color:#6c757d}.epistola-admin .config-card__value{font-size:.875rem;color:#161616}.epistola-admin .config-card__footer{margin-top:1rem;padding-top:.75rem;border-top:1px solid #f0f0f0;text-align:right}.epistola-admin .config-card__latency{font-size:.8rem;color:#adb5bd}.epistola-admin .usage-link{color:#0f62fe;text-decoration:none}.epistola-admin .usage-link:hover{text-decoration:underline}.epistola-admin .detail-info-table{max-width:500px}.epistola-admin .detail-info-table th{width:140px}.epistola-admin .detail-summary{padding:1rem 0;border-bottom:1px solid #dee2e6}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2$4.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: TabsModule }, { kind: "component", type: i5.Tabs, selector: "cds-tabs, ibm-tabs", inputs: ["position", "cacheActive", "followFocus", "isNavigation", "ariaLabel", "ariaLabelledby", "type", "theme", "skeleton"] }, { kind: "component", type: i5.Tab, selector: "cds-tab, ibm-tab", inputs: ["heading", "title", "context", "active", "disabled", "tabIndex", "id", "cacheActive", "tabContent", "templateContext"], outputs: ["selected"] }, { kind: "ngmodule", type: TagModule }, { kind: "component", type: i6.Tag, selector: "cds-tag, ibm-tag", inputs: ["type", "size", "class", "skeleton"] }] });
3334
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminPageComponent, deps: [{ token: EpistolaAdminService }, { token: i2$5.ActivatedRoute }, { token: i2$5.Router }], target: i0.ɵɵFactoryTarget.Component });
3335
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaAdminPageComponent, isStandalone: true, selector: "epistola-admin-page", ngImport: i0, template: "<div class=\"epistola-admin\">\n <!-- Overview: tabs (no configuration selected) -->\n <ng-container *ngIf=\"!selectedCard\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"d-flex align-items-center\">\n <h5 class=\"mb-0\">{{ 'epistolaAdminOverview' | pluginTranslate: 'epistola' | async }}</h5>\n <span *ngIf=\"pluginVersion\" class=\"version-badge ms-2\">v{{ pluginVersion }}</span>\n </div>\n <button class=\"btn btn-outline-primary btn-sm\" (click)=\"refresh()\" [disabled]=\"loading\">\n {{ 'epistolaAdminRefresh' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <ng-template #configurationsHeading>\n {{ 'epistolaAdminConfigurations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ cards.length }}</cds-tag>\n </ng-template>\n\n <ng-template #validationsHeading>\n {{ 'epistolaAdminValidations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" [type]=\"validationViolations.length > 0 ? 'red' : 'gray'\" class=\"ms-1\">\n {{ validationViolations.length }}\n </cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"configurationsHeading\"\n [active]=\"overviewTab === 'configurations'\"\n (selected)=\"setOverviewTab('configurations')\"\n >\n <div *ngIf=\"loading\" class=\"text-muted mt-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length === 0\" class=\"text-muted mt-3\">\n {{ 'epistolaAdminNoConfigurations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length > 0\" class=\"card-grid\">\n <div\n *ngFor=\"let card of cards\"\n class=\"config-card\"\n [class.config-card--ok]=\"card.reachable && card.problemCount === 0\"\n [class.config-card--warning]=\"card.reachable && card.problemCount > 0\"\n [class.config-card--error]=\"!card.reachable\"\n (click)=\"selectConfiguration(card)\"\n >\n <div class=\"config-card__header\">\n <span\n class=\"status-dot\"\n [class.status-dot--ok]=\"card.reachable\"\n [class.status-dot--error]=\"!card.reachable\"\n >\n </span>\n <h5 class=\"config-card__title\">{{ card.configurationTitle }}</h5>\n </div>\n\n <div class=\"config-card__body\">\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async\n }}</span>\n <code class=\"config-card__value\">{{ card.tenantId }}</code>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" [type]=\"card.reachable ? 'green' : 'red'\">\n {{\n card.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n </div>\n <div *ngIf=\"card.serverVersion\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.serverVersion }}</span>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.usageCount }}</span>\n </div>\n <div *ngIf=\"card.pendingJobs.length > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">{{ card.pendingJobs.length }}</cds-tag>\n </div>\n <div *ngIf=\"card.problemCount > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"red\">{{ card.problemCount }}</cds-tag>\n </div>\n </div>\n\n <div class=\"config-card__footer\">\n <span class=\"config-card__latency\">{{ card.latencyMs }} ms</span>\n </div>\n </div>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"validationsHeading\"\n [active]=\"overviewTab === 'validations'\"\n (selected)=\"setOverviewTab('validations')\"\n >\n <div *ngIf=\"validationViolations.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoValidations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"validationViolations.length > 0\" class=\"mt-3\">\n <p class=\"text-muted mb-3\">\n {{ 'epistolaAdminValidationWarningBody' | pluginTranslate: 'epistola' | async }}\n </p>\n <table class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminValidationCode' | pluginTranslate: 'epistola' | async }}</th>\n <th>\n {{ 'epistolaAdminValidationMessage' | pluginTranslate: 'epistola' | async }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let v of validationViolations\">\n <td>\n {{ v.processDefinitionName || v.processDefinitionKey }}\n <div *ngIf=\"v.processDefinitionName\" class=\"text-muted small\">\n <code>{{ v.processDefinitionKey }}</code>\n </div>\n </td>\n <td>\n <code>{{ v.activityId }}</code>\n </td>\n <td>\n <cds-tag size=\"sm\" type=\"red\">{{ v.code }}</cds-tag>\n </td>\n <td>{{ v.message }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n\n <!-- Detail view: selected configuration -->\n <ng-container *ngIf=\"selectedCard\">\n <div class=\"detail-header mb-3\">\n <button class=\"btn btn-link btn-sm p-0\" (click)=\"backToOverview()\">\n &larr; {{ 'epistolaAdminBackToOverview' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div class=\"detail-summary mb-4\">\n <h4>\n <span\n class=\"status-dot me-2\"\n [class.status-dot--ok]=\"selectedCard.reachable\"\n [class.status-dot--error]=\"!selectedCard.reachable\"\n >\n </span>\n {{ selectedCard.configurationTitle }}\n </h4>\n\n <table class=\"table table-sm detail-info-table\">\n <tbody>\n <tr>\n <th>{{ 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <code>{{ selectedCard.tenantId }}</code>\n </td>\n </tr>\n <tr>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <cds-tag size=\"sm\" [type]=\"selectedCard.reachable ? 'green' : 'red'\">\n {{\n selectedCard.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n <span class=\"text-muted ms-2\">{{ selectedCard.latencyMs }} ms</span>\n </td>\n </tr>\n <tr *ngIf=\"selectedCard.serverVersion\">\n <th>{{ 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async }}</th>\n <td>{{ selectedCard.serverVersion }}</td>\n </tr>\n <tr *ngIf=\"selectedCard.errorMessage\">\n <th>{{ 'epistolaAdminError' | pluginTranslate: 'epistola' | async }}</th>\n <td class=\"text-danger\">{{ selectedCard.errorMessage }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Tabs -->\n <ng-template #actionsHeading>\n {{ 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ selectedCard.usageEntries.length }}</cds-tag>\n </ng-template>\n\n <ng-template #pendingHeading>\n {{ 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n size=\"sm\"\n [type]=\"selectedCard.pendingJobs.length > 0 ? 'blue' : 'gray'\"\n class=\"ms-1\"\n >\n {{ selectedCard.pendingJobs.length }}\n </cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"actionsHeading\"\n [active]=\"activeTab === 'actions'\"\n (selected)=\"setActiveTab('actions')\"\n >\n <div *ngIf=\"selectedCard.usageEntries.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoUsageForConfig' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.usageEntries.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCase' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminAction' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let entry of selectedCard.usageEntries\"\n [class.table-warning]=\"entry.problems.length > 0\"\n >\n <td>{{ entry.caseDefinitionKey || '-' }}</td>\n <td>\n <a\n *ngIf=\"entry.caseDefinitionKey && entry.caseDefinitionVersionTag\"\n [routerLink]=\"[\n '/case-management',\n 'case',\n entry.caseDefinitionKey,\n 'version',\n entry.caseDefinitionVersionTag,\n 'processes',\n entry.processDefinitionKey,\n ]\"\n class=\"usage-link\"\n >\n {{ entry.processDefinitionName }}\n </a>\n <span *ngIf=\"!entry.caseDefinitionKey || !entry.caseDefinitionVersionTag\">\n {{ entry.processDefinitionName }}\n </span>\n </td>\n <td>{{ entry.activityName }}</td>\n <td>\n <code>{{ entry.actionKey }}</code>\n </td>\n <td>\n <cds-tag *ngIf=\"entry.problems.length === 0\" size=\"sm\" type=\"green\">OK</cds-tag>\n <cds-tag\n *ngFor=\"let problem of entry.problems\"\n size=\"sm\"\n type=\"red\"\n class=\"d-block mb-1\"\n >\n {{ problem }}\n </cds-tag>\n </td>\n <td>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"exportProcessLink(entry)\"\n [title]=\"'epistolaAdminExport' | pluginTranslate: 'epistola' | async\"\n >\n &#x2913;\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"pendingHeading\"\n [active]=\"activeTab === 'pending'\"\n (selected)=\"setActiveTab('pending')\"\n >\n <div *ngIf=\"selectedCard.pendingJobs.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoPendingJobs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.pendingJobs.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminRequestId' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let job of selectedCard.pendingJobs\">\n <td>{{ job.processDefinitionName }}</td>\n <td>{{ job.activityName }}</td>\n <td>\n <code>{{ job.requestId }}</code>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"reconcilePending(job)\"\n [disabled]=\"isReconciling(job)\"\n [title]=\"'epistolaAdminReconcileTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isReconciling(job)\">\n {{ 'epistolaAdminReconcile' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isReconciling(job)\">\n {{ 'epistolaAdminReconciling' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"reconcileFeedback && reconcileFeedback.executionId === job.executionId\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"reconcileFeedback.type === 'success'\"\n [class.text-warning]=\"reconcileFeedback.type === 'pending'\"\n [class.text-danger]=\"reconcileFeedback.type === 'error'\"\n >\n {{ reconcileFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n</div>\n", styles: [".epistola-admin{padding:1.5rem}.epistola-admin .version-badge{font-size:.75rem;font-weight:500;padding:.2em .6em;border-radius:4px;background-color:#e8e8e8;color:#525252}.epistola-admin .badge{font-size:.85em;padding:.35em .65em}.epistola-admin code{font-size:.9em;color:#525252}.epistola-admin table th{font-weight:600;white-space:nowrap}.epistola-admin .status-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:.5rem;flex-shrink:0;background-color:#adb5bd}.epistola-admin .status-dot--ok{background-color:#198754}.epistola-admin .status-dot--error{background-color:#dc3545}.epistola-admin .card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.epistola-admin .config-card{border:1px solid #dee2e6;border-radius:8px;padding:1.25rem;cursor:pointer;transition:box-shadow .15s ease,border-color .15s ease;background:#fff}.epistola-admin .config-card:hover{box-shadow:0 2px 8px #0000001a}.epistola-admin .config-card--ok{border-left:4px solid #198754}.epistola-admin .config-card--warning{border-left:4px solid #ffc107}.epistola-admin .config-card--error{border-left:4px solid #dc3545}.epistola-admin .config-card__header{display:flex;align-items:center;margin-bottom:1rem}.epistola-admin .config-card__title{margin:0;font-size:1.05rem;font-weight:600;color:#161616}.epistola-admin .config-card__body{display:flex;flex-direction:column;gap:.5rem}.epistola-admin .config-card__field{display:flex;justify-content:space-between;align-items:center}.epistola-admin .config-card__label{font-size:.875rem;color:#6c757d}.epistola-admin .config-card__value{font-size:.875rem;color:#161616}.epistola-admin .config-card__footer{margin-top:1rem;padding-top:.75rem;border-top:1px solid #f0f0f0;text-align:right}.epistola-admin .config-card__latency{font-size:.8rem;color:#adb5bd}.epistola-admin .usage-link{color:#0f62fe;text-decoration:none}.epistola-admin .usage-link:hover{text-decoration:underline}.epistola-admin .detail-info-table{max-width:500px}.epistola-admin .detail-info-table th{width:140px}.epistola-admin .detail-summary{padding:1rem 0;border-bottom:1px solid #dee2e6}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2$5.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: TabsModule }, { kind: "component", type: i5$1.Tabs, selector: "cds-tabs, ibm-tabs", inputs: ["position", "cacheActive", "followFocus", "isNavigation", "ariaLabel", "ariaLabelledby", "type", "theme", "skeleton"] }, { kind: "component", type: i5$1.Tab, selector: "cds-tab, ibm-tab", inputs: ["heading", "title", "context", "active", "disabled", "tabIndex", "id", "cacheActive", "tabContent", "templateContext"], outputs: ["selected"] }, { kind: "ngmodule", type: TagModule }, { kind: "component", type: i6.Tag, selector: "cds-tag, ibm-tag", inputs: ["type", "size", "class", "skeleton"] }] });
3143
3336
  }
3144
3337
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminPageComponent, decorators: [{
3145
3338
  type: Component,
3146
- args: [{ selector: 'epistola-admin-page', standalone: true, imports: [CommonModule, RouterModule, PluginTranslatePipeModule, TabsModule, TagModule], template: "<div class=\"epistola-admin\">\n <!-- Overview: card grid (no configuration selected) -->\n <ng-container *ngIf=\"!selectedCard\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"d-flex align-items-center\">\n <h5 class=\"mb-0\">{{ 'epistolaAdminOverview' | pluginTranslate: 'epistola' | async }}</h5>\n <span *ngIf=\"pluginVersion\" class=\"version-badge ms-2\">v{{ pluginVersion }}</span>\n </div>\n <button class=\"btn btn-outline-primary btn-sm\" (click)=\"refresh()\" [disabled]=\"loading\">\n {{ 'epistolaAdminRefresh' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div *ngIf=\"loading\" class=\"text-muted\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length === 0\" class=\"text-muted\">\n {{ 'epistolaAdminNoConfigurations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length > 0\" class=\"card-grid\">\n <div\n *ngFor=\"let card of cards\"\n class=\"config-card\"\n [class.config-card--ok]=\"card.reachable && card.problemCount === 0\"\n [class.config-card--warning]=\"card.reachable && card.problemCount > 0\"\n [class.config-card--error]=\"!card.reachable\"\n (click)=\"selectConfiguration(card)\"\n >\n <div class=\"config-card__header\">\n <span\n class=\"status-dot\"\n [class.status-dot--ok]=\"card.reachable\"\n [class.status-dot--error]=\"!card.reachable\"\n >\n </span>\n <h5 class=\"config-card__title\">{{ card.configurationTitle }}</h5>\n </div>\n\n <div class=\"config-card__body\">\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async\n }}</span>\n <code class=\"config-card__value\">{{ card.tenantId }}</code>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" [type]=\"card.reachable ? 'green' : 'red'\">\n {{\n card.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n </div>\n <div *ngIf=\"card.serverVersion\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.serverVersion }}</span>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.usageCount }}</span>\n </div>\n <div *ngIf=\"card.pendingJobs.length > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">{{ card.pendingJobs.length }}</cds-tag>\n </div>\n <div *ngIf=\"card.problemCount > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"red\">{{ card.problemCount }}</cds-tag>\n </div>\n </div>\n\n <div class=\"config-card__footer\">\n <span class=\"config-card__latency\">{{ card.latencyMs }} ms</span>\n </div>\n </div>\n </div>\n </ng-container>\n\n <!-- Detail view: selected configuration -->\n <ng-container *ngIf=\"selectedCard\">\n <div class=\"detail-header mb-3\">\n <button class=\"btn btn-link btn-sm p-0\" (click)=\"backToOverview()\">\n &larr; {{ 'epistolaAdminBackToOverview' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div class=\"detail-summary mb-4\">\n <h4>\n <span\n class=\"status-dot me-2\"\n [class.status-dot--ok]=\"selectedCard.reachable\"\n [class.status-dot--error]=\"!selectedCard.reachable\"\n >\n </span>\n {{ selectedCard.configurationTitle }}\n </h4>\n\n <table class=\"table table-sm detail-info-table\">\n <tbody>\n <tr>\n <th>{{ 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <code>{{ selectedCard.tenantId }}</code>\n </td>\n </tr>\n <tr>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <cds-tag size=\"sm\" [type]=\"selectedCard.reachable ? 'green' : 'red'\">\n {{\n selectedCard.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n <span class=\"text-muted ms-2\">{{ selectedCard.latencyMs }} ms</span>\n </td>\n </tr>\n <tr *ngIf=\"selectedCard.serverVersion\">\n <th>{{ 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async }}</th>\n <td>{{ selectedCard.serverVersion }}</td>\n </tr>\n <tr *ngIf=\"selectedCard.errorMessage\">\n <th>{{ 'epistolaAdminError' | pluginTranslate: 'epistola' | async }}</th>\n <td class=\"text-danger\">{{ selectedCard.errorMessage }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Tabs -->\n <ng-template #actionsHeading>\n {{ 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ selectedCard.usageEntries.length }}</cds-tag>\n </ng-template>\n\n <ng-template #pendingHeading>\n {{ 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n size=\"sm\"\n [type]=\"selectedCard.pendingJobs.length > 0 ? 'blue' : 'gray'\"\n class=\"ms-1\"\n >\n {{ selectedCard.pendingJobs.length }}\n </cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"actionsHeading\"\n [active]=\"activeTab === 'actions'\"\n (selected)=\"setActiveTab('actions')\"\n >\n <div *ngIf=\"selectedCard.usageEntries.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoUsageForConfig' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.usageEntries.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCase' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminAction' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let entry of selectedCard.usageEntries\"\n [class.table-warning]=\"entry.problems.length > 0\"\n >\n <td>{{ entry.caseDefinitionKey || '-' }}</td>\n <td>\n <a\n *ngIf=\"entry.caseDefinitionKey && entry.caseDefinitionVersionTag\"\n [routerLink]=\"[\n '/case-management',\n 'case',\n entry.caseDefinitionKey,\n 'version',\n entry.caseDefinitionVersionTag,\n 'processes',\n entry.processDefinitionKey,\n ]\"\n class=\"usage-link\"\n >\n {{ entry.processDefinitionName }}\n </a>\n <span *ngIf=\"!entry.caseDefinitionKey || !entry.caseDefinitionVersionTag\">\n {{ entry.processDefinitionName }}\n </span>\n </td>\n <td>{{ entry.activityName }}</td>\n <td>\n <code>{{ entry.actionKey }}</code>\n </td>\n <td>\n <cds-tag *ngIf=\"entry.problems.length === 0\" size=\"sm\" type=\"green\">OK</cds-tag>\n <cds-tag\n *ngFor=\"let problem of entry.problems\"\n size=\"sm\"\n type=\"red\"\n class=\"d-block mb-1\"\n >\n {{ problem }}\n </cds-tag>\n </td>\n <td>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"exportProcessLink(entry)\"\n [title]=\"'epistolaAdminExport' | pluginTranslate: 'epistola' | async\"\n >\n &#x2913;\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"pendingHeading\"\n [active]=\"activeTab === 'pending'\"\n (selected)=\"setActiveTab('pending')\"\n >\n <div *ngIf=\"selectedCard.pendingJobs.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoPendingJobs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.pendingJobs.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminRequestId' | pluginTranslate: 'epistola' | async }}</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let job of selectedCard.pendingJobs\">\n <td>{{ job.processDefinitionName }}</td>\n <td>{{ job.activityName }}</td>\n <td>\n <code>{{ job.requestId }}</code>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n</div>\n", styles: [".epistola-admin{padding:1.5rem}.epistola-admin .version-badge{font-size:.75rem;font-weight:500;padding:.2em .6em;border-radius:4px;background-color:#e8e8e8;color:#525252}.epistola-admin .badge{font-size:.85em;padding:.35em .65em}.epistola-admin code{font-size:.9em;color:#525252}.epistola-admin table th{font-weight:600;white-space:nowrap}.epistola-admin .status-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:.5rem;flex-shrink:0;background-color:#adb5bd}.epistola-admin .status-dot--ok{background-color:#198754}.epistola-admin .status-dot--error{background-color:#dc3545}.epistola-admin .card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.epistola-admin .config-card{border:1px solid #dee2e6;border-radius:8px;padding:1.25rem;cursor:pointer;transition:box-shadow .15s ease,border-color .15s ease;background:#fff}.epistola-admin .config-card:hover{box-shadow:0 2px 8px #0000001a}.epistola-admin .config-card--ok{border-left:4px solid #198754}.epistola-admin .config-card--warning{border-left:4px solid #ffc107}.epistola-admin .config-card--error{border-left:4px solid #dc3545}.epistola-admin .config-card__header{display:flex;align-items:center;margin-bottom:1rem}.epistola-admin .config-card__title{margin:0;font-size:1.05rem;font-weight:600;color:#161616}.epistola-admin .config-card__body{display:flex;flex-direction:column;gap:.5rem}.epistola-admin .config-card__field{display:flex;justify-content:space-between;align-items:center}.epistola-admin .config-card__label{font-size:.875rem;color:#6c757d}.epistola-admin .config-card__value{font-size:.875rem;color:#161616}.epistola-admin .config-card__footer{margin-top:1rem;padding-top:.75rem;border-top:1px solid #f0f0f0;text-align:right}.epistola-admin .config-card__latency{font-size:.8rem;color:#adb5bd}.epistola-admin .usage-link{color:#0f62fe;text-decoration:none}.epistola-admin .usage-link:hover{text-decoration:underline}.epistola-admin .detail-info-table{max-width:500px}.epistola-admin .detail-info-table th{width:140px}.epistola-admin .detail-summary{padding:1rem 0;border-bottom:1px solid #dee2e6}\n"] }]
3147
- }], ctorParameters: () => [{ type: EpistolaAdminService }, { type: i2$4.ActivatedRoute }, { type: i2$4.Router }] });
3339
+ args: [{ selector: 'epistola-admin-page', standalone: true, imports: [CommonModule, RouterModule, PluginTranslatePipeModule, TabsModule, TagModule], template: "<div class=\"epistola-admin\">\n <!-- Overview: tabs (no configuration selected) -->\n <ng-container *ngIf=\"!selectedCard\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"d-flex align-items-center\">\n <h5 class=\"mb-0\">{{ 'epistolaAdminOverview' | pluginTranslate: 'epistola' | async }}</h5>\n <span *ngIf=\"pluginVersion\" class=\"version-badge ms-2\">v{{ pluginVersion }}</span>\n </div>\n <button class=\"btn btn-outline-primary btn-sm\" (click)=\"refresh()\" [disabled]=\"loading\">\n {{ 'epistolaAdminRefresh' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <ng-template #configurationsHeading>\n {{ 'epistolaAdminConfigurations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ cards.length }}</cds-tag>\n </ng-template>\n\n <ng-template #validationsHeading>\n {{ 'epistolaAdminValidations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" [type]=\"validationViolations.length > 0 ? 'red' : 'gray'\" class=\"ms-1\">\n {{ validationViolations.length }}\n </cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"configurationsHeading\"\n [active]=\"overviewTab === 'configurations'\"\n (selected)=\"setOverviewTab('configurations')\"\n >\n <div *ngIf=\"loading\" class=\"text-muted mt-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length === 0\" class=\"text-muted mt-3\">\n {{ 'epistolaAdminNoConfigurations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length > 0\" class=\"card-grid\">\n <div\n *ngFor=\"let card of cards\"\n class=\"config-card\"\n [class.config-card--ok]=\"card.reachable && card.problemCount === 0\"\n [class.config-card--warning]=\"card.reachable && card.problemCount > 0\"\n [class.config-card--error]=\"!card.reachable\"\n (click)=\"selectConfiguration(card)\"\n >\n <div class=\"config-card__header\">\n <span\n class=\"status-dot\"\n [class.status-dot--ok]=\"card.reachable\"\n [class.status-dot--error]=\"!card.reachable\"\n >\n </span>\n <h5 class=\"config-card__title\">{{ card.configurationTitle }}</h5>\n </div>\n\n <div class=\"config-card__body\">\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async\n }}</span>\n <code class=\"config-card__value\">{{ card.tenantId }}</code>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" [type]=\"card.reachable ? 'green' : 'red'\">\n {{\n card.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n </div>\n <div *ngIf=\"card.serverVersion\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.serverVersion }}</span>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.usageCount }}</span>\n </div>\n <div *ngIf=\"card.pendingJobs.length > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">{{ card.pendingJobs.length }}</cds-tag>\n </div>\n <div *ngIf=\"card.problemCount > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"red\">{{ card.problemCount }}</cds-tag>\n </div>\n </div>\n\n <div class=\"config-card__footer\">\n <span class=\"config-card__latency\">{{ card.latencyMs }} ms</span>\n </div>\n </div>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"validationsHeading\"\n [active]=\"overviewTab === 'validations'\"\n (selected)=\"setOverviewTab('validations')\"\n >\n <div *ngIf=\"validationViolations.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoValidations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"validationViolations.length > 0\" class=\"mt-3\">\n <p class=\"text-muted mb-3\">\n {{ 'epistolaAdminValidationWarningBody' | pluginTranslate: 'epistola' | async }}\n </p>\n <table class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminValidationCode' | pluginTranslate: 'epistola' | async }}</th>\n <th>\n {{ 'epistolaAdminValidationMessage' | pluginTranslate: 'epistola' | async }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let v of validationViolations\">\n <td>\n {{ v.processDefinitionName || v.processDefinitionKey }}\n <div *ngIf=\"v.processDefinitionName\" class=\"text-muted small\">\n <code>{{ v.processDefinitionKey }}</code>\n </div>\n </td>\n <td>\n <code>{{ v.activityId }}</code>\n </td>\n <td>\n <cds-tag size=\"sm\" type=\"red\">{{ v.code }}</cds-tag>\n </td>\n <td>{{ v.message }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n\n <!-- Detail view: selected configuration -->\n <ng-container *ngIf=\"selectedCard\">\n <div class=\"detail-header mb-3\">\n <button class=\"btn btn-link btn-sm p-0\" (click)=\"backToOverview()\">\n &larr; {{ 'epistolaAdminBackToOverview' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div class=\"detail-summary mb-4\">\n <h4>\n <span\n class=\"status-dot me-2\"\n [class.status-dot--ok]=\"selectedCard.reachable\"\n [class.status-dot--error]=\"!selectedCard.reachable\"\n >\n </span>\n {{ selectedCard.configurationTitle }}\n </h4>\n\n <table class=\"table table-sm detail-info-table\">\n <tbody>\n <tr>\n <th>{{ 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <code>{{ selectedCard.tenantId }}</code>\n </td>\n </tr>\n <tr>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <cds-tag size=\"sm\" [type]=\"selectedCard.reachable ? 'green' : 'red'\">\n {{\n selectedCard.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n <span class=\"text-muted ms-2\">{{ selectedCard.latencyMs }} ms</span>\n </td>\n </tr>\n <tr *ngIf=\"selectedCard.serverVersion\">\n <th>{{ 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async }}</th>\n <td>{{ selectedCard.serverVersion }}</td>\n </tr>\n <tr *ngIf=\"selectedCard.errorMessage\">\n <th>{{ 'epistolaAdminError' | pluginTranslate: 'epistola' | async }}</th>\n <td class=\"text-danger\">{{ selectedCard.errorMessage }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Tabs -->\n <ng-template #actionsHeading>\n {{ 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ selectedCard.usageEntries.length }}</cds-tag>\n </ng-template>\n\n <ng-template #pendingHeading>\n {{ 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n size=\"sm\"\n [type]=\"selectedCard.pendingJobs.length > 0 ? 'blue' : 'gray'\"\n class=\"ms-1\"\n >\n {{ selectedCard.pendingJobs.length }}\n </cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"actionsHeading\"\n [active]=\"activeTab === 'actions'\"\n (selected)=\"setActiveTab('actions')\"\n >\n <div *ngIf=\"selectedCard.usageEntries.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoUsageForConfig' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.usageEntries.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCase' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminAction' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let entry of selectedCard.usageEntries\"\n [class.table-warning]=\"entry.problems.length > 0\"\n >\n <td>{{ entry.caseDefinitionKey || '-' }}</td>\n <td>\n <a\n *ngIf=\"entry.caseDefinitionKey && entry.caseDefinitionVersionTag\"\n [routerLink]=\"[\n '/case-management',\n 'case',\n entry.caseDefinitionKey,\n 'version',\n entry.caseDefinitionVersionTag,\n 'processes',\n entry.processDefinitionKey,\n ]\"\n class=\"usage-link\"\n >\n {{ entry.processDefinitionName }}\n </a>\n <span *ngIf=\"!entry.caseDefinitionKey || !entry.caseDefinitionVersionTag\">\n {{ entry.processDefinitionName }}\n </span>\n </td>\n <td>{{ entry.activityName }}</td>\n <td>\n <code>{{ entry.actionKey }}</code>\n </td>\n <td>\n <cds-tag *ngIf=\"entry.problems.length === 0\" size=\"sm\" type=\"green\">OK</cds-tag>\n <cds-tag\n *ngFor=\"let problem of entry.problems\"\n size=\"sm\"\n type=\"red\"\n class=\"d-block mb-1\"\n >\n {{ problem }}\n </cds-tag>\n </td>\n <td>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"exportProcessLink(entry)\"\n [title]=\"'epistolaAdminExport' | pluginTranslate: 'epistola' | async\"\n >\n &#x2913;\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"pendingHeading\"\n [active]=\"activeTab === 'pending'\"\n (selected)=\"setActiveTab('pending')\"\n >\n <div *ngIf=\"selectedCard.pendingJobs.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoPendingJobs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.pendingJobs.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminRequestId' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let job of selectedCard.pendingJobs\">\n <td>{{ job.processDefinitionName }}</td>\n <td>{{ job.activityName }}</td>\n <td>\n <code>{{ job.requestId }}</code>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"reconcilePending(job)\"\n [disabled]=\"isReconciling(job)\"\n [title]=\"'epistolaAdminReconcileTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isReconciling(job)\">\n {{ 'epistolaAdminReconcile' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isReconciling(job)\">\n {{ 'epistolaAdminReconciling' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"reconcileFeedback && reconcileFeedback.executionId === job.executionId\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"reconcileFeedback.type === 'success'\"\n [class.text-warning]=\"reconcileFeedback.type === 'pending'\"\n [class.text-danger]=\"reconcileFeedback.type === 'error'\"\n >\n {{ reconcileFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n</div>\n", styles: [".epistola-admin{padding:1.5rem}.epistola-admin .version-badge{font-size:.75rem;font-weight:500;padding:.2em .6em;border-radius:4px;background-color:#e8e8e8;color:#525252}.epistola-admin .badge{font-size:.85em;padding:.35em .65em}.epistola-admin code{font-size:.9em;color:#525252}.epistola-admin table th{font-weight:600;white-space:nowrap}.epistola-admin .status-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:.5rem;flex-shrink:0;background-color:#adb5bd}.epistola-admin .status-dot--ok{background-color:#198754}.epistola-admin .status-dot--error{background-color:#dc3545}.epistola-admin .card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.epistola-admin .config-card{border:1px solid #dee2e6;border-radius:8px;padding:1.25rem;cursor:pointer;transition:box-shadow .15s ease,border-color .15s ease;background:#fff}.epistola-admin .config-card:hover{box-shadow:0 2px 8px #0000001a}.epistola-admin .config-card--ok{border-left:4px solid #198754}.epistola-admin .config-card--warning{border-left:4px solid #ffc107}.epistola-admin .config-card--error{border-left:4px solid #dc3545}.epistola-admin .config-card__header{display:flex;align-items:center;margin-bottom:1rem}.epistola-admin .config-card__title{margin:0;font-size:1.05rem;font-weight:600;color:#161616}.epistola-admin .config-card__body{display:flex;flex-direction:column;gap:.5rem}.epistola-admin .config-card__field{display:flex;justify-content:space-between;align-items:center}.epistola-admin .config-card__label{font-size:.875rem;color:#6c757d}.epistola-admin .config-card__value{font-size:.875rem;color:#161616}.epistola-admin .config-card__footer{margin-top:1rem;padding-top:.75rem;border-top:1px solid #f0f0f0;text-align:right}.epistola-admin .config-card__latency{font-size:.8rem;color:#adb5bd}.epistola-admin .usage-link{color:#0f62fe;text-decoration:none}.epistola-admin .usage-link:hover{text-decoration:underline}.epistola-admin .detail-info-table{max-width:500px}.epistola-admin .detail-info-table th{width:140px}.epistola-admin .detail-summary{padding:1rem 0;border-bottom:1px solid #dee2e6}\n"] }]
3340
+ }], ctorParameters: () => [{ type: EpistolaAdminService }, { type: i2$5.ActivatedRoute }, { type: i2$5.Router }] });
3148
3341
 
3149
3342
  function isRuntimeWindow(value) {
3150
3343
  return typeof value === 'object' && value !== null;
@@ -3191,7 +3384,7 @@ const routes = [
3191
3384
  ];
3192
3385
  class EpistolaAdminRoutingModule {
3193
3386
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
3194
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, imports: [i2$4.RouterModule], exports: [RouterModule] });
3387
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, imports: [i2$5.RouterModule], exports: [RouterModule] });
3195
3388
  static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, imports: [RouterModule.forChild(routes), RouterModule] });
3196
3389
  }
3197
3390
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, decorators: [{
@@ -3202,19 +3395,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3202
3395
  }]
3203
3396
  }] });
3204
3397
 
3205
- const EPISTOLA_DOWNLOAD_OPTIONS = {
3206
- type: 'epistola-download',
3207
- selector: 'epistola-download-button',
3208
- title: 'Epistola Download',
3398
+ const EPISTOLA_DOCUMENT_OPTIONS = {
3399
+ type: 'epistola-document',
3400
+ selector: 'epistola-document-element',
3401
+ title: 'Epistola Document',
3209
3402
  group: 'basic',
3210
- icon: 'download',
3403
+ icon: 'file-pdf-o',
3211
3404
  emptyValue: null,
3212
- fieldOptions: ['filename', 'label'],
3405
+ fieldOptions: ['label', 'display', 'documentVariable', 'tenantIdVariable', 'filename'],
3213
3406
  };
3214
- function registerEpistolaDownloadComponent(injector) {
3215
- if (!customElements.get(EPISTOLA_DOWNLOAD_OPTIONS.selector)) {
3216
- registerCustomFormioComponent(EPISTOLA_DOWNLOAD_OPTIONS, EpistolaDownloadComponent, injector);
3407
+ function registerEpistolaDocumentComponent(injector) {
3408
+ if (customElements.get(EPISTOLA_DOCUMENT_OPTIONS.selector)) {
3409
+ return;
3217
3410
  }
3411
+ registerCustomFormioComponent(EPISTOLA_DOCUMENT_OPTIONS, EpistolaDocumentComponent, injector);
3218
3412
  }
3219
3413
 
3220
3414
  const EPISTOLA_RETRY_FORM_OPTIONS = {
@@ -3232,21 +3426,6 @@ function registerEpistolaRetryFormComponent(injector) {
3232
3426
  }
3233
3427
  }
3234
3428
 
3235
- const EPISTOLA_PREVIEW_BUTTON_OPTIONS = {
3236
- type: 'epistola-preview-button',
3237
- selector: 'epistola-preview-button-element',
3238
- title: 'Epistola Preview',
3239
- group: 'basic',
3240
- icon: 'eye',
3241
- emptyValue: null,
3242
- fieldOptions: ['label'],
3243
- };
3244
- function registerEpistolaPreviewButtonComponent(injector) {
3245
- if (!customElements.get(EPISTOLA_PREVIEW_BUTTON_OPTIONS.selector)) {
3246
- registerCustomFormioComponent(EPISTOLA_PREVIEW_BUTTON_OPTIONS, EpistolaPreviewButtonComponent, injector);
3247
- }
3248
- }
3249
-
3250
3429
  const EPISTOLA_DOCUMENT_PREVIEW_OPTIONS = {
3251
3430
  type: 'epistola-document-preview',
3252
3431
  selector: 'epistola-document-preview-element',
@@ -3798,6 +3977,11 @@ class EpistolaPluginModule {
3798
3977
  ngModule: EpistolaPluginModule,
3799
3978
  providers: [
3800
3979
  EpistolaMenuService,
3980
+ {
3981
+ provide: HTTP_INTERCEPTORS,
3982
+ useClass: EpistolaTaskContextInterceptor,
3983
+ multi: true,
3984
+ },
3801
3985
  {
3802
3986
  provide: ENVIRONMENT_INITIALIZER,
3803
3987
  multi: true,
@@ -3805,9 +3989,8 @@ class EpistolaPluginModule {
3805
3989
  if (!isEpistolaEnabled())
3806
3990
  return;
3807
3991
  const injector = inject(Injector);
3808
- registerEpistolaDownloadComponent(injector);
3992
+ registerEpistolaDocumentComponent(injector);
3809
3993
  registerEpistolaRetryFormComponent(injector);
3810
- registerEpistolaPreviewButtonComponent(injector);
3811
3994
  registerEpistolaOverrideBuilderComponent(injector);
3812
3995
  registerEpistolaProcessLinkSelectorComponent(injector);
3813
3996
  registerEpistolaDocumentPreviewComponent(injector);
@@ -3830,17 +4013,15 @@ class EpistolaPluginModule {
3830
4013
  GenerateDocumentConfigurationComponent,
3831
4014
  CheckJobStatusConfigurationComponent,
3832
4015
  DownloadDocumentConfigurationComponent,
3833
- EpistolaDownloadComponent,
4016
+ EpistolaDocumentComponent,
3834
4017
  EpistolaRetryFormComponent,
3835
- EpistolaPreviewButtonComponent,
3836
4018
  EpistolaDocumentPreviewComponent,
3837
4019
  EpistolaAdminPageComponent], exports: [EpistolaConfigurationComponent,
3838
4020
  GenerateDocumentConfigurationComponent,
3839
4021
  CheckJobStatusConfigurationComponent,
3840
4022
  DownloadDocumentConfigurationComponent,
3841
- EpistolaDownloadComponent,
4023
+ EpistolaDocumentComponent,
3842
4024
  EpistolaRetryFormComponent,
3843
- EpistolaPreviewButtonComponent,
3844
4025
  EpistolaDocumentPreviewComponent,
3845
4026
  EpistolaAdminPageComponent] });
3846
4027
  static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaPluginModule, providers: [EpistolaPluginService, EpistolaAdminService], imports: [CommonModule,
@@ -3854,9 +4035,8 @@ class EpistolaPluginModule {
3854
4035
  GenerateDocumentConfigurationComponent,
3855
4036
  CheckJobStatusConfigurationComponent,
3856
4037
  DownloadDocumentConfigurationComponent,
3857
- EpistolaDownloadComponent,
4038
+ EpistolaDocumentComponent,
3858
4039
  EpistolaRetryFormComponent,
3859
- EpistolaPreviewButtonComponent,
3860
4040
  EpistolaDocumentPreviewComponent,
3861
4041
  EpistolaAdminPageComponent] });
3862
4042
  }
@@ -3875,9 +4055,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3875
4055
  GenerateDocumentConfigurationComponent,
3876
4056
  CheckJobStatusConfigurationComponent,
3877
4057
  DownloadDocumentConfigurationComponent,
3878
- EpistolaDownloadComponent,
4058
+ EpistolaDocumentComponent,
3879
4059
  EpistolaRetryFormComponent,
3880
- EpistolaPreviewButtonComponent,
3881
4060
  EpistolaDocumentPreviewComponent,
3882
4061
  EpistolaAdminPageComponent,
3883
4062
  ],
@@ -3886,9 +4065,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3886
4065
  GenerateDocumentConfigurationComponent,
3887
4066
  CheckJobStatusConfigurationComponent,
3888
4067
  DownloadDocumentConfigurationComponent,
3889
- EpistolaDownloadComponent,
4068
+ EpistolaDocumentComponent,
3890
4069
  EpistolaRetryFormComponent,
3891
- EpistolaPreviewButtonComponent,
3892
4070
  EpistolaDocumentPreviewComponent,
3893
4071
  EpistolaAdminPageComponent,
3894
4072
  ],
@@ -3910,9 +4088,9 @@ const epistolaPluginSpecification = {
3910
4088
  pluginLogoBase64: EPISTOLA_PLUGIN_LOGO_BASE64,
3911
4089
  // Map action keys to their configuration components
3912
4090
  functionConfigurationComponents: {
3913
- 'generate-document': GenerateDocumentConfigurationComponent,
3914
- 'check-job-status': CheckJobStatusConfigurationComponent,
3915
- 'download-document': DownloadDocumentConfigurationComponent,
4091
+ 'epistola-generate-document': GenerateDocumentConfigurationComponent,
4092
+ 'epistola-check-job-status': CheckJobStatusConfigurationComponent,
4093
+ 'epistola-download-document': DownloadDocumentConfigurationComponent,
3916
4094
  },
3917
4095
  // Translations
3918
4096
  pluginTranslations: {
@@ -3930,7 +4108,7 @@ const epistolaPluginSpecification = {
3930
4108
  defaultEnvironmentIdTooltip: 'De standaard omgeving voor documentgeneratie (3-30 tekens, alleen kleine letters, cijfers en koppeltekens, bijv. "productie")',
3931
4109
  templateSyncEnabled: 'Template synchronisatie',
3932
4110
  templateSyncEnabledTooltip: 'Synchroniseer template definities automatisch van het classpath naar Epistola bij het opstarten',
3933
- 'generate-document': 'Genereer Document',
4111
+ 'epistola-generate-document': 'Genereer Document',
3934
4112
  catalogId: 'Catalogus',
3935
4113
  catalogIdTooltip: 'Selecteer de catalogus waaruit een template gekozen wordt',
3936
4114
  templateId: 'Template',
@@ -4007,7 +4185,7 @@ const epistolaPluginSpecification = {
4007
4185
  sourceFieldPlaceholder: 'Bronveldnaam',
4008
4186
  noTemplateFields: 'Geen template velden beschikbaar',
4009
4187
  // Check job status action
4010
- 'check-job-status': 'Controleer Taakstatus',
4188
+ 'epistola-check-job-status': 'Controleer Taakstatus',
4011
4189
  requestIdVariable: 'Request ID Variabele',
4012
4190
  requestIdVariableTooltip: 'Naam van de procesvariabele met het Epistola request ID',
4013
4191
  statusVariable: 'Status Variabele',
@@ -4017,7 +4195,9 @@ const epistolaPluginSpecification = {
4017
4195
  errorMessageVariable: 'Foutmelding Variabele',
4018
4196
  errorMessageVariableTooltip: 'Naam van de procesvariabele waarin de foutmelding wordt opgeslagen (bij fout)',
4019
4197
  // Download document action
4020
- 'download-document': 'Download Document',
4198
+ 'epistola-download-document': 'Download Document',
4199
+ documentVariable: 'Document Variabele',
4200
+ documentVariableTooltip: 'Naam van de procesvariabele met het Epistola resultaat. Mag een String document ID zijn (legacy) of een rich-result object met een documentId-veld; de actie haalt het document ID eruit.',
4021
4201
  contentVariable: 'Inhoud Variabele',
4022
4202
  contentVariableTooltip: 'Naam van de procesvariabele waarin de documentinhoud (Base64) wordt opgeslagen',
4023
4203
  // Admin page
@@ -4044,6 +4224,16 @@ const epistolaPluginSpecification = {
4044
4224
  epistolaAdminNoPendingJobs: 'Geen wachtende taken voor deze verbinding.',
4045
4225
  epistolaAdminConfiguration: 'Configuratie',
4046
4226
  epistolaAdminRequestId: 'Request ID',
4227
+ epistolaAdminReconcile: 'Hersynchroniseer',
4228
+ epistolaAdminReconciling: 'Bezig...',
4229
+ epistolaAdminReconcileTooltip: 'Vraag de huidige status op bij Epistola en hervat het wachtende proces als het klaar is.',
4230
+ epistolaAdminConfigurations: 'Configuraties',
4231
+ epistolaAdminValidations: 'BPMN-validatie',
4232
+ epistolaAdminNoValidations: 'Geen race-onveilige procesdefinities gevonden. Alles ziet er goed uit.',
4233
+ epistolaAdminValidationWarningTitle: 'BPMN configuratie waarschuwing',
4234
+ epistolaAdminValidationWarningBody: 'In deze procesdefinities is de grens tussen de generate-document service task en de EpistolaDocumentGenerated catch event niet synchroon. Resultaten kunnen verloren gaan; gebruik in dat geval de Hersynchroniseer-knop in de Wachtende taken-tab.',
4235
+ epistolaAdminValidationCode: 'Code',
4236
+ epistolaAdminValidationMessage: 'Bericht',
4047
4237
  },
4048
4238
  en: {
4049
4239
  title: 'Epistola Document Suite',
@@ -4059,7 +4249,7 @@ const epistolaPluginSpecification = {
4059
4249
  defaultEnvironmentIdTooltip: 'The default environment for document generation (3-30 chars, lowercase letters, digits and hyphens only, e.g. "production")',
4060
4250
  templateSyncEnabled: 'Template sync',
4061
4251
  templateSyncEnabledTooltip: 'Automatically synchronize template definitions from classpath to Epistola on startup',
4062
- 'generate-document': 'Generate Document',
4252
+ 'epistola-generate-document': 'Generate Document',
4063
4253
  catalogId: 'Catalog',
4064
4254
  catalogIdTooltip: 'Select the catalog to choose a template from',
4065
4255
  templateId: 'Template',
@@ -4136,7 +4326,7 @@ const epistolaPluginSpecification = {
4136
4326
  sourceFieldPlaceholder: 'Source field name',
4137
4327
  noTemplateFields: 'No template fields available',
4138
4328
  // Check job status action
4139
- 'check-job-status': 'Check Job Status',
4329
+ 'epistola-check-job-status': 'Check Job Status',
4140
4330
  requestIdVariable: 'Request ID Variable',
4141
4331
  requestIdVariableTooltip: 'Name of the process variable containing the Epistola request ID',
4142
4332
  statusVariable: 'Status Variable',
@@ -4146,7 +4336,9 @@ const epistolaPluginSpecification = {
4146
4336
  errorMessageVariable: 'Error Message Variable',
4147
4337
  errorMessageVariableTooltip: 'Name of the process variable to store the error message in (when failed)',
4148
4338
  // Download document action
4149
- 'download-document': 'Download Document',
4339
+ 'epistola-download-document': 'Download Document',
4340
+ documentVariable: 'Document Variable',
4341
+ documentVariableTooltip: 'Name of the process variable holding the Epistola result. May be a String document id (legacy) or a rich result object with a `documentId` key — the action extracts the document id from it.',
4150
4342
  contentVariable: 'Content Variable',
4151
4343
  contentVariableTooltip: 'Name of the process variable to store the document content (Base64) in',
4152
4344
  // Admin page
@@ -4173,6 +4365,16 @@ const epistolaPluginSpecification = {
4173
4365
  epistolaAdminNoPendingJobs: 'No pending jobs for this connection.',
4174
4366
  epistolaAdminConfiguration: 'Configuration',
4175
4367
  epistolaAdminRequestId: 'Request ID',
4368
+ epistolaAdminReconcile: 'Reconcile',
4369
+ epistolaAdminReconciling: 'Reconciling...',
4370
+ epistolaAdminReconcileTooltip: "Ask Epistola for this job's current status and resume the waiting process if it has finished.",
4371
+ epistolaAdminConfigurations: 'Configurations',
4372
+ epistolaAdminValidations: 'BPMN validation',
4373
+ epistolaAdminNoValidations: 'No race-unsafe process definitions detected. Everything looks good.',
4374
+ epistolaAdminValidationWarningTitle: 'BPMN configuration warning',
4375
+ epistolaAdminValidationWarningBody: 'These process definitions have a non-synchronous boundary between the generate-document service task and the EpistolaDocumentGenerated catch event. Results can be missed; use the Reconcile button on the Pending Jobs tab to recover.',
4376
+ epistolaAdminValidationCode: 'Code',
4377
+ epistolaAdminValidationMessage: 'Message',
4176
4378
  },
4177
4379
  },
4178
4380
  };
@@ -4185,5 +4387,5 @@ const epistolaPluginSpecification = {
4185
4387
  * Generated bundle index. Do not edit.
4186
4388
  */
4187
4389
 
4188
- export { CheckJobStatusConfigurationComponent, DownloadDocumentConfigurationComponent, EPISTOLA_DOCUMENT_PREVIEW_OPTIONS, EPISTOLA_DOWNLOAD_OPTIONS, EPISTOLA_OVERRIDE_BUILDER_OPTIONS, EPISTOLA_PREVIEW_BUTTON_OPTIONS, EPISTOLA_PROCESS_LINK_SELECTOR_OPTIONS, EPISTOLA_RETRY_FORM_OPTIONS, EpistolaAdminPageComponent, EpistolaAdminRoutingModule, EpistolaAdminService, EpistolaConfigurationComponent, EpistolaDocumentPreviewComponent, EpistolaDownloadComponent, EpistolaMenuService, EpistolaOverrideBuilderComponent, EpistolaPluginModule, EpistolaPluginService, EpistolaPreviewButtonComponent, EpistolaProcessLinkSelectorComponent, EpistolaRetryFormComponent, GenerateDocumentConfigurationComponent, JsonataEditorComponent, MappingBuilderComponent, epistolaPluginSpecification, errorResource, initialResource, isEpistolaEnabled, loadingResource, registerEpistolaDocumentPreviewComponent, registerEpistolaDownloadComponent, registerEpistolaOverrideBuilderComponent, registerEpistolaPreviewButtonComponent, registerEpistolaProcessLinkSelectorComponent, registerEpistolaRetryFormComponent, successResource };
4390
+ export { CheckJobStatusConfigurationComponent, DownloadDocumentConfigurationComponent, EPISTOLA_DOCUMENT_OPTIONS, EPISTOLA_DOCUMENT_PREVIEW_OPTIONS, EPISTOLA_OVERRIDE_BUILDER_OPTIONS, EPISTOLA_PROCESS_LINK_SELECTOR_OPTIONS, EPISTOLA_RETRY_FORM_OPTIONS, EpistolaAdminPageComponent, EpistolaAdminRoutingModule, EpistolaAdminService, EpistolaConfigurationComponent, EpistolaDocumentComponent, EpistolaDocumentPreviewComponent, EpistolaMenuService, EpistolaOverrideBuilderComponent, EpistolaPluginModule, EpistolaPluginService, EpistolaProcessLinkSelectorComponent, EpistolaRetryFormComponent, EpistolaTaskContextInterceptor, EpistolaTaskContextService, GenerateDocumentConfigurationComponent, JsonataEditorComponent, MappingBuilderComponent, epistolaPluginSpecification, errorResource, initialResource, isEpistolaEnabled, loadingResource, registerEpistolaDocumentComponent, registerEpistolaDocumentPreviewComponent, registerEpistolaOverrideBuilderComponent, registerEpistolaProcessLinkSelectorComponent, registerEpistolaRetryFormComponent, successResource };
4189
4391
  //# sourceMappingURL=epistola.app-valtimo-plugin.mjs.map