@epistola.app/valtimo-plugin 0.7.0 → 0.9.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 +801 -459
  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 +44 -3
  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 +72 -0
  12. package/lib/models/config.d.ts +7 -11
  13. package/lib/services/epistola-admin.service.d.ts +32 -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,47 @@ 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
+ }
97
+ /**
98
+ * List the classpath catalogs available to manually redeploy for a plugin
99
+ * configuration, each annotated with the version last deployed in this
100
+ * backend process.
101
+ */
102
+ getClasspathCatalogs(configurationId) {
103
+ return this.http.get(`${this.apiEndpoint}/configurations/${encodeURIComponent(configurationId)}/catalogs`);
104
+ }
105
+ /**
106
+ * Force-redeploy a single classpath catalog to the configuration's Epistola
107
+ * installation. Explicit operator action — bypasses the templateSyncEnabled
108
+ * gate and the version-skip check. Returns 200 with per-resource counts on
109
+ * success, or 502 (error callback, body carries `errorMessage`) on failure.
110
+ */
111
+ redeployCatalog(configurationId, slug) {
112
+ return this.http.post(`${this.apiEndpoint}/configurations/${encodeURIComponent(configurationId)}/catalogs/${encodeURIComponent(slug)}/redeploy`, null);
113
+ }
114
+ /**
115
+ * Get the plugin CHANGELOG parsed (server-side) into structured releases for
116
+ * the admin Changelog tab — no markdown renderer needed on the client.
117
+ */
118
+ getChangelog() {
119
+ return this.http.get(`${this.apiEndpoint}/changelog`);
120
+ }
80
121
  /**
81
122
  * Export a single process link as a .process-link.json file.
82
123
  */
@@ -220,9 +261,11 @@ class EpistolaPluginService {
220
261
  }
221
262
  /**
222
263
  * Get a dynamically generated Formio form for retrying a failed document generation.
264
+ *
265
+ * @param taskId Operaton user task id (required — backend authorizes via OperatonTask:VIEW)
223
266
  */
224
- getRetryForm(processInstanceId, documentId, sourceActivityId) {
225
- const params = { processInstanceId };
267
+ getRetryForm(taskId, processInstanceId, documentId, sourceActivityId) {
268
+ const params = { taskId, processInstanceId };
226
269
  if (documentId) {
227
270
  params['documentId'] = documentId;
228
271
  }
@@ -245,24 +288,36 @@ class EpistolaPluginService {
245
288
  return this.http.post(`${this.apiEndpoint}/validate-jsonata`, request);
246
289
  }
247
290
  /**
248
- * Discover all previewable document sources for a given Valtimo document.
291
+ * Preview a document by dry-running the {@code generate-document} process
292
+ * link. Returns the rendered PDF as a {@link Blob}.
293
+ *
294
+ * <p>The {@code X-Skip-Interceptor: 422} header tells the global Valtimo
295
+ * error interceptor to skip its toast for validation failures so the
296
+ * caller can render an inline error message.
249
297
  */
250
- getPreviewSources(documentId) {
251
- return this.http.get(`${this.apiEndpoint}/preview-sources`, {
252
- params: { documentId },
298
+ previewToBlob(request) {
299
+ return this.http.post(`${this.apiEndpoint}/preview`, request, {
300
+ responseType: 'blob',
301
+ headers: new HttpHeaders().set('X-Skip-Interceptor', '422'),
253
302
  });
254
303
  }
255
304
  /**
256
- * Preview a document by dry-running the generate-document process link.
257
- * Returns the resolved data as a mock preview (Phase 1).
305
+ * Stream an already-generated Epistola PDF for the caller's task. The
306
+ * backend resolves the Epistola document id and tenant id from the named
307
+ * process variables on the task's process instance, so the wire never
308
+ * carries a raw PDF id.
258
309
  */
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,
310
+ downloadDocumentBlob(request) {
311
+ const params = new URLSearchParams({
312
+ taskId: request.taskId,
313
+ caseDocumentId: request.caseDocumentId,
314
+ documentVariable: request.documentVariable,
315
+ tenantIdVariable: request.tenantIdVariable,
316
+ filename: request.filename,
317
+ disposition: request.disposition,
318
+ });
319
+ return this.http.get(`${this.apiEndpoint}/documents/download?${params.toString()}`, {
320
+ responseType: 'blob',
266
321
  });
267
322
  }
268
323
  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 +327,101 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
272
327
  type: Injectable
273
328
  }], ctorParameters: () => [{ type: i1.HttpClient }, { type: i2.ConfigService }] });
274
329
 
330
+ /**
331
+ * Holds the currently-open Operaton user task instance id so that custom
332
+ * Formio components rendered inside a Valtimo task form (preview, download,
333
+ * retry-form) can include it in backend requests for PBAC.
334
+ *
335
+ * Populated by {@link EpistolaTaskContextInterceptor}, which sniffs Valtimo's
336
+ * canonical "load process link for task" GET (`/api/v2/process-link/task/{taskId}`)
337
+ * — the request always fires when a task opens, before the form renders.
338
+ *
339
+ * <p><b>Why this exists:</b> Valtimo 13.21 does not expose the active task
340
+ * instance id through any service that custom Formio components can inject
341
+ * (`FormIoStateService` carries documentId and processInstanceId only;
342
+ * `TaskDetailContentComponent.taskInstanceId$` is private to that component
343
+ * and Formio elements live in their own injector tree). This service is a
344
+ * workaround until upstream exposes `taskInstanceId` via `FormIoStateService`.
345
+ */
346
+ class EpistolaTaskContextService {
347
+ _taskInstanceId$ = new BehaviorSubject(null);
348
+ taskInstanceId$ = this._taskInstanceId$.asObservable();
349
+ get taskInstanceId() {
350
+ return this._taskInstanceId$.value;
351
+ }
352
+ setTaskInstanceId(id) {
353
+ this._taskInstanceId$.next(id);
354
+ }
355
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
356
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextService, providedIn: 'root' });
357
+ }
358
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextService, decorators: [{
359
+ type: Injectable,
360
+ args: [{ providedIn: 'root' }]
361
+ }] });
362
+
363
+ /**
364
+ * Pure helpers for {@link EpistolaTaskContextInterceptor}, extracted so they
365
+ * can be unit-tested without pulling in {@code @angular/core} (which ts-jest
366
+ * cannot transform without {@code jest-preset-angular}).
367
+ */
368
+ /**
369
+ * Pattern Valtimo uses for the canonical task-open call:
370
+ * {@code GET /api/v2/process-link/task/{taskInstanceId}}.
371
+ *
372
+ * Captures the {@code taskInstanceId} (UUID v4-style 36-character hyphenated
373
+ * hex string). Anchored at the end of the URL or at a query-string delimiter
374
+ * so we don't accidentally match a longer trailing segment.
375
+ */
376
+ const TASK_PROCESS_LINK_PATTERN = /\/api\/v2\/process-link\/task\/([0-9a-fA-F-]{36})(?:\?|$)/;
377
+ /**
378
+ * Returns the captured {@code taskInstanceId} from a Valtimo task-open
379
+ * request URL, or {@code null} if the request does not match.
380
+ */
381
+ function extractTaskInstanceIdFromUrl(method, url) {
382
+ if (method !== 'GET')
383
+ return null;
384
+ const match = TASK_PROCESS_LINK_PATTERN.exec(url);
385
+ return match ? match[1] : null;
386
+ }
387
+
388
+ /**
389
+ * Sniffs Valtimo's task-open signal and pushes the active taskInstanceId into
390
+ * {@link EpistolaTaskContextService}. The signal is the canonical
391
+ * {@code GET /api/v2/process-link/task/{taskId}} call that
392
+ * {@code TaskDetailContentComponent.loadTaskDetails(...)} fires unconditionally
393
+ * before any task form is rendered (see @valtimo/task internals).
394
+ *
395
+ * <p>This interceptor does <b>not</b> modify the outgoing request. It only
396
+ * captures the taskId from the URL.
397
+ *
398
+ * <p>Workaround for Valtimo 13.21 not exposing taskInstanceId through any
399
+ * injectable service. Remove once upstream adds e.g.
400
+ * {@code FormIoStateService.setTaskInstanceId(...)}.
401
+ *
402
+ * <p>The actual URL-matching logic lives in
403
+ * {@link extractTaskInstanceIdFromUrl} so it can be unit-tested without an
404
+ * Angular harness.
405
+ */
406
+ class EpistolaTaskContextInterceptor {
407
+ taskContext;
408
+ constructor(taskContext) {
409
+ this.taskContext = taskContext;
410
+ }
411
+ intercept(request, next) {
412
+ const taskId = extractTaskInstanceIdFromUrl(request.method, request.url);
413
+ if (taskId !== null && taskId !== this.taskContext.taskInstanceId) {
414
+ this.taskContext.setTaskInstanceId(taskId);
415
+ }
416
+ return next.handle(request);
417
+ }
418
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextInterceptor, deps: [{ token: EpistolaTaskContextService }], target: i0.ɵɵFactoryTarget.Injectable });
419
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextInterceptor });
420
+ }
421
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextInterceptor, decorators: [{
422
+ type: Injectable
423
+ }], ctorParameters: () => [{ type: EpistolaTaskContextService }] });
424
+
275
425
  class EpistolaConfigurationComponent {
276
426
  save$;
277
427
  disabled$;
@@ -1983,9 +2133,17 @@ class CheckJobStatusConfigurationComponent {
1983
2133
  saveSubscription;
1984
2134
  formValue$ = new BehaviorSubject(null);
1985
2135
  valid$ = new BehaviorSubject(false);
2136
+ /** Resolved synchronously before the v-form renders — see download-document-configuration for the why. */
2137
+ resolvedPrefill = {};
2138
+ prefillResolved$ = new BehaviorSubject(false);
1986
2139
  safeDisabled$;
1987
2140
  ngOnInit() {
1988
2141
  this.safeDisabled$ = this.disabled$.pipe(startWith(true), delay(0));
2142
+ const prefill$ = this.prefillConfiguration$ ?? of({});
2143
+ prefill$.pipe(take(1)).subscribe((prefill) => {
2144
+ this.resolvedPrefill = prefill ?? {};
2145
+ this.prefillResolved$.next(true);
2146
+ });
1989
2147
  this.openSaveSubscription();
1990
2148
  }
1991
2149
  ngOnDestroy() {
@@ -2013,11 +2171,11 @@ class CheckJobStatusConfigurationComponent {
2013
2171
  });
2014
2172
  }
2015
2173
  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"] }] });
2174
+ 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
2175
  }
2018
2176
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: CheckJobStatusConfigurationComponent, decorators: [{
2019
2177
  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" }]
2178
+ 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
2179
  }], propDecorators: { save$: [{
2022
2180
  type: Input
2023
2181
  }], disabled$: [{
@@ -2042,9 +2200,21 @@ class DownloadDocumentConfigurationComponent {
2042
2200
  saveSubscription;
2043
2201
  formValue$ = new BehaviorSubject(null);
2044
2202
  valid$ = new BehaviorSubject(false);
2203
+ /**
2204
+ * Resolved prefill — populated synchronously before the v-form renders. Avoids the
2205
+ * v-input `[defaultValue]` async-binding race that otherwise drops one of the
2206
+ * fields when prefill arrives after mount.
2207
+ */
2208
+ resolvedPrefill = {};
2209
+ prefillResolved$ = new BehaviorSubject(false);
2045
2210
  safeDisabled$;
2046
2211
  ngOnInit() {
2047
2212
  this.safeDisabled$ = this.disabled$.pipe(startWith(true), delay(0));
2213
+ const prefill$ = this.prefillConfiguration$ ?? of({});
2214
+ prefill$.pipe(take(1)).subscribe((prefill) => {
2215
+ this.resolvedPrefill = prefill ?? {};
2216
+ this.prefillResolved$.next(true);
2217
+ });
2048
2218
  this.openSaveSubscription();
2049
2219
  }
2050
2220
  ngOnDestroy() {
@@ -2056,7 +2226,7 @@ class DownloadDocumentConfigurationComponent {
2056
2226
  this.handleValid(formValue);
2057
2227
  }
2058
2228
  handleValid(formValue) {
2059
- const valid = !!(formValue?.documentIdVariable && formValue?.contentVariable);
2229
+ const valid = !!(formValue?.documentVariable && formValue?.contentVariable);
2060
2230
  this.valid$.next(valid);
2061
2231
  this.valid.emit(valid);
2062
2232
  }
@@ -2072,11 +2242,11 @@ class DownloadDocumentConfigurationComponent {
2072
2242
  });
2073
2243
  }
2074
2244
  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"] }] });
2245
+ 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
2246
  }
2077
2247
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DownloadDocumentConfigurationComponent, decorators: [{
2078
2248
  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" }]
2249
+ 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
2250
  }], propDecorators: { save$: [{
2081
2251
  type: Input
2082
2252
  }], disabled$: [{
@@ -2091,35 +2261,89 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2091
2261
  type: Output
2092
2262
  }] } });
2093
2263
 
2094
- class EpistolaDownloadComponent {
2095
- http;
2264
+ /**
2265
+ * Unified Formio component for the after-generation Epistola PDF UX. Reads
2266
+ * the PDF id and tenant id from named process variables on the caller's
2267
+ * task via the {@code /documents/download} endpoint.
2268
+ *
2269
+ * <p>Three presentations, all backed by the same backend call:
2270
+ * <ul>
2271
+ * <li>{@code display="inline"} — render the PDF inline in a panel.</li>
2272
+ * <li>{@code display="button"} — show a click-to-save download button only.</li>
2273
+ * <li>{@code display="both"} (default) — inline panel with a download icon
2274
+ * in the header.</li>
2275
+ * </ul>
2276
+ *
2277
+ * <p>For the dry-run / what-would-be-generated UX use
2278
+ * {@code epistola-document-preview} instead.
2279
+ */
2280
+ class EpistolaDocumentComponent {
2281
+ epistolaPluginService;
2282
+ sanitizer;
2283
+ formIoStateService;
2284
+ taskContext;
2285
+ cdr;
2096
2286
  value;
2097
2287
  valueChange = new EventEmitter();
2098
2288
  disabled = false;
2289
+ label = 'Document';
2290
+ /** How to present the document. `both` (default) shows an inline panel with a download icon. */
2291
+ display = 'both';
2292
+ /**
2293
+ * Process-variable name holding the Epistola result. Default: `epistolaResult`.
2294
+ *
2295
+ * Type-tolerant on the backend: if the named variable holds a rich result object
2296
+ * (`Map<String, Object>` written by `generate-document` and updated by the result
2297
+ * collector), the backend digs out the `documentId` key. If it holds a plain
2298
+ * String (custom flow that wrote a bare id), the backend uses it as-is.
2299
+ */
2300
+ documentVariable = 'epistolaResult';
2301
+ /** Process-variable name holding the Epistola tenant id. Default: `epistolaTenantId`. */
2302
+ tenantIdVariable = 'epistolaTenantId';
2303
+ /** Filename used for the download disposition. */
2099
2304
  filename = 'document.pdf';
2100
- label = 'Download PDF';
2305
+ loading = false;
2101
2306
  downloading = false;
2102
2307
  error = null;
2103
- get buttonLabel() {
2104
- return this.label || 'Download PDF';
2308
+ previewUrl = null;
2309
+ currentBlobUrl = null;
2310
+ subscription;
2311
+ get designMode() {
2312
+ return !this.formIoStateService.documentId;
2105
2313
  }
2106
- constructor(http) {
2107
- this.http = http;
2314
+ constructor(epistolaPluginService, sanitizer, formIoStateService, taskContext, cdr) {
2315
+ this.epistolaPluginService = epistolaPluginService;
2316
+ this.sanitizer = sanitizer;
2317
+ this.formIoStateService = formIoStateService;
2318
+ this.taskContext = taskContext;
2319
+ this.cdr = cdr;
2108
2320
  }
2109
- hasRequiredData() {
2110
- return !!(this.value?.documentId && this.value?.tenantId);
2321
+ ngOnInit() {
2322
+ if (this.designMode) {
2323
+ return;
2324
+ }
2325
+ if (this.display !== 'button') {
2326
+ this.loadInline();
2327
+ }
2328
+ }
2329
+ ngOnDestroy() {
2330
+ this.subscription?.unsubscribe();
2331
+ this.revokeBlobUrl();
2332
+ }
2333
+ refresh() {
2334
+ this.loadInline();
2111
2335
  }
2112
2336
  download() {
2113
- if (!this.hasRequiredData() || this.downloading) {
2337
+ if (this.designMode || this.downloading) {
2114
2338
  return;
2115
2339
  }
2340
+ const request = this.buildRequest('attachment');
2341
+ if (!request)
2342
+ return;
2116
2343
  this.downloading = true;
2117
2344
  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({
2345
+ this.cdr.markForCheck();
2346
+ this.epistolaPluginService.downloadDocumentBlob(request).subscribe({
2123
2347
  next: (blob) => {
2124
2348
  const objectUrl = URL.createObjectURL(blob);
2125
2349
  const anchor = document.createElement('a');
@@ -2128,66 +2352,246 @@ class EpistolaDownloadComponent {
2128
2352
  anchor.click();
2129
2353
  URL.revokeObjectURL(objectUrl);
2130
2354
  this.downloading = false;
2355
+ this.cdr.markForCheck();
2131
2356
  },
2132
2357
  error: (err) => {
2133
- console.error('Download failed', err);
2134
- this.error = 'Download mislukt. Probeer opnieuw.';
2358
+ this.error = err.status === 404 ? 'Document is nog niet gegenereerd.' : 'Download mislukt.';
2135
2359
  this.downloading = false;
2360
+ this.cdr.markForCheck();
2136
2361
  },
2137
2362
  });
2138
2363
  }
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"] }] });
2364
+ loadInline() {
2365
+ const request = this.buildRequest('inline');
2366
+ if (!request)
2367
+ return;
2368
+ this.loading = true;
2369
+ this.error = null;
2370
+ this.cdr.markForCheck();
2371
+ this.revokeBlobUrl();
2372
+ this.subscription?.unsubscribe();
2373
+ this.subscription = this.epistolaPluginService.downloadDocumentBlob(request).subscribe({
2374
+ next: (blob) => {
2375
+ this.currentBlobUrl = URL.createObjectURL(blob);
2376
+ this.previewUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.currentBlobUrl);
2377
+ this.loading = false;
2378
+ this.cdr.markForCheck();
2379
+ },
2380
+ error: (err) => {
2381
+ this.previewUrl = null;
2382
+ this.error =
2383
+ err.status === 404
2384
+ ? 'Document is nog niet gegenereerd.'
2385
+ : 'Document kon niet geladen worden.';
2386
+ this.loading = false;
2387
+ this.cdr.markForCheck();
2388
+ },
2389
+ });
2390
+ }
2391
+ buildRequest(disposition) {
2392
+ const taskId = this.taskContext.taskInstanceId;
2393
+ const caseDocumentId = this.formIoStateService.documentId;
2394
+ if (!taskId || !caseDocumentId) {
2395
+ this.error = 'Document is alleen beschikbaar binnen een taak.';
2396
+ this.cdr.markForCheck();
2397
+ return null;
2398
+ }
2399
+ return {
2400
+ taskId,
2401
+ caseDocumentId,
2402
+ documentVariable: this.documentVariable,
2403
+ tenantIdVariable: this.tenantIdVariable,
2404
+ filename: this.filename,
2405
+ disposition,
2406
+ };
2407
+ }
2408
+ revokeBlobUrl() {
2409
+ if (this.currentBlobUrl) {
2410
+ URL.revokeObjectURL(this.currentBlobUrl);
2411
+ this.currentBlobUrl = null;
2412
+ this.previewUrl = null;
2413
+ }
2414
+ }
2415
+ 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 });
2416
+ 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: `
2417
+ <!-- Design-time placeholder -->
2418
+ <div *ngIf="designMode" class="epistola-doc-panel">
2419
+ <div class="doc-header">
2420
+ <span>{{ label || 'Document' }}</span>
2421
+ <span class="design-tag">design mode</span>
2422
+ </div>
2423
+ <div class="doc-body design-info">
2424
+ <div class="design-section">
2425
+ <div class="design-label">Display</div>
2426
+ <div class="design-value">{{ display }}</div>
2427
+ <div class="design-label">Document variable</div>
2428
+ <div class="design-value">{{ documentVariable }}</div>
2429
+ <div class="design-label">Tenant ID variable</div>
2430
+ <div class="design-value">{{ tenantIdVariable }}</div>
2431
+ </div>
2432
+ </div>
2433
+ </div>
2434
+
2435
+ <!-- Button-only display -->
2436
+ <div *ngIf="!designMode && display === 'button'">
2437
+ <button
2438
+ type="button"
2439
+ class="btn btn-outline-primary"
2440
+ [disabled]="disabled || downloading"
2441
+ (click)="download()"
2442
+ >
2443
+ <i class="mdi mdi-download mr-1"></i>
2444
+ {{ downloading ? 'Downloading...' : label || 'Download PDF' }}
2445
+ </button>
2446
+ <span *ngIf="error" class="text-danger ml-2">{{ error }}</span>
2447
+ </div>
2448
+
2449
+ <!-- Inline / both: panel with optional download icon -->
2450
+ <div *ngIf="!designMode && display !== 'button'" class="epistola-doc-panel">
2451
+ <div class="doc-header">
2452
+ <span>{{ label || 'Document' }}</span>
2453
+ <div class="doc-controls">
2454
+ <button
2455
+ *ngIf="display === 'both'"
2456
+ type="button"
2457
+ class="doc-icon-btn"
2458
+ [disabled]="disabled || downloading"
2459
+ (click)="download()"
2460
+ [title]="downloading ? 'Downloading...' : 'Download'"
2461
+ >
2462
+ <i class="mdi mdi-download"></i>
2463
+ </button>
2464
+ <button
2465
+ type="button"
2466
+ class="doc-icon-btn"
2467
+ [disabled]="loading"
2468
+ (click)="refresh()"
2469
+ title="Refresh"
2470
+ >
2471
+ <i class="mdi mdi-refresh"></i>
2472
+ </button>
2473
+ </div>
2474
+ </div>
2475
+ <div class="doc-body">
2476
+ <div *ngIf="loading" class="doc-loading">Loading document...</div>
2477
+ <div *ngIf="error && !loading" class="doc-unavailable">
2478
+ <i class="mdi mdi-information-outline"></i>
2479
+ {{ error }}
2480
+ </div>
2481
+ <object
2482
+ *ngIf="previewUrl && !loading"
2483
+ [data]="previewUrl"
2484
+ type="application/pdf"
2485
+ class="doc-pdf"
2486
+ >
2487
+ PDF preview is not supported in this browser.
2488
+ </object>
2489
+ </div>
2490
+ </div>
2491
+ `, 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
2492
  }
2153
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDownloadComponent, decorators: [{
2493
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDocumentComponent, decorators: [{
2154
2494
  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: [{
2495
+ args: [{ standalone: true, imports: [CommonModule], selector: 'epistola-document-component', changeDetection: ChangeDetectionStrategy.OnPush, template: `
2496
+ <!-- Design-time placeholder -->
2497
+ <div *ngIf="designMode" class="epistola-doc-panel">
2498
+ <div class="doc-header">
2499
+ <span>{{ label || 'Document' }}</span>
2500
+ <span class="design-tag">design mode</span>
2501
+ </div>
2502
+ <div class="doc-body design-info">
2503
+ <div class="design-section">
2504
+ <div class="design-label">Display</div>
2505
+ <div class="design-value">{{ display }}</div>
2506
+ <div class="design-label">Document variable</div>
2507
+ <div class="design-value">{{ documentVariable }}</div>
2508
+ <div class="design-label">Tenant ID variable</div>
2509
+ <div class="design-value">{{ tenantIdVariable }}</div>
2510
+ </div>
2511
+ </div>
2512
+ </div>
2513
+
2514
+ <!-- Button-only display -->
2515
+ <div *ngIf="!designMode && display === 'button'">
2516
+ <button
2517
+ type="button"
2518
+ class="btn btn-outline-primary"
2519
+ [disabled]="disabled || downloading"
2520
+ (click)="download()"
2521
+ >
2522
+ <i class="mdi mdi-download mr-1"></i>
2523
+ {{ downloading ? 'Downloading...' : label || 'Download PDF' }}
2524
+ </button>
2525
+ <span *ngIf="error" class="text-danger ml-2">{{ error }}</span>
2526
+ </div>
2527
+
2528
+ <!-- Inline / both: panel with optional download icon -->
2529
+ <div *ngIf="!designMode && display !== 'button'" class="epistola-doc-panel">
2530
+ <div class="doc-header">
2531
+ <span>{{ label || 'Document' }}</span>
2532
+ <div class="doc-controls">
2533
+ <button
2534
+ *ngIf="display === 'both'"
2535
+ type="button"
2536
+ class="doc-icon-btn"
2537
+ [disabled]="disabled || downloading"
2538
+ (click)="download()"
2539
+ [title]="downloading ? 'Downloading...' : 'Download'"
2540
+ >
2541
+ <i class="mdi mdi-download"></i>
2542
+ </button>
2543
+ <button
2544
+ type="button"
2545
+ class="doc-icon-btn"
2546
+ [disabled]="loading"
2547
+ (click)="refresh()"
2548
+ title="Refresh"
2549
+ >
2550
+ <i class="mdi mdi-refresh"></i>
2551
+ </button>
2552
+ </div>
2553
+ </div>
2554
+ <div class="doc-body">
2555
+ <div *ngIf="loading" class="doc-loading">Loading document...</div>
2556
+ <div *ngIf="error && !loading" class="doc-unavailable">
2557
+ <i class="mdi mdi-information-outline"></i>
2558
+ {{ error }}
2559
+ </div>
2560
+ <object
2561
+ *ngIf="previewUrl && !loading"
2562
+ [data]="previewUrl"
2563
+ type="application/pdf"
2564
+ class="doc-pdf"
2565
+ >
2566
+ PDF preview is not supported in this browser.
2567
+ </object>
2568
+ </div>
2569
+ </div>
2570
+ `, 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"] }]
2571
+ }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$4.DomSanitizer }, { type: i3.FormIoStateService }, { type: EpistolaTaskContextService }, { type: i0.ChangeDetectorRef }], propDecorators: { value: [{
2173
2572
  type: Input
2174
2573
  }], valueChange: [{
2175
2574
  type: Output
2176
2575
  }], disabled: [{
2177
2576
  type: Input
2178
- }], filename: [{
2179
- type: Input
2180
2577
  }], label: [{
2181
2578
  type: Input
2579
+ }], display: [{
2580
+ type: Input
2581
+ }], documentVariable: [{
2582
+ type: Input
2583
+ }], tenantIdVariable: [{
2584
+ type: Input
2585
+ }], filename: [{
2586
+ type: Input
2182
2587
  }] } });
2183
2588
 
2184
2589
  class EpistolaRetryFormComponent {
2185
2590
  epistolaPluginService;
2186
2591
  formIoStateService;
2187
2592
  cdr;
2188
- http;
2189
2593
  sanitizer;
2190
- configService;
2594
+ taskContext;
2191
2595
  value;
2192
2596
  valueChange = new EventEmitter();
2193
2597
  disabled = false;
@@ -2208,19 +2612,16 @@ class EpistolaRetryFormComponent {
2208
2612
  currentBlobUrl = null;
2209
2613
  resolvedSourceActivityId;
2210
2614
  processDefinitionKey;
2211
- apiEndpoint;
2212
2615
  formOptions = {
2213
2616
  noAlerts: true,
2214
2617
  buttonSettings: { showCancel: false, showSubmit: false, showPrevious: false, showNext: false },
2215
2618
  };
2216
- constructor(epistolaPluginService, formIoStateService, cdr, http, sanitizer, configService) {
2619
+ constructor(epistolaPluginService, formIoStateService, cdr, sanitizer, taskContext) {
2217
2620
  this.epistolaPluginService = epistolaPluginService;
2218
2621
  this.formIoStateService = formIoStateService;
2219
2622
  this.cdr = cdr;
2220
- this.http = http;
2221
2623
  this.sanitizer = sanitizer;
2222
- this.configService = configService;
2223
- this.apiEndpoint = `${this.configService.config.valtimoApi.endpointUri}v1/plugin/epistola`;
2624
+ this.taskContext = taskContext;
2224
2625
  // Debounce preview calls
2225
2626
  this.previewSubscription = this.previewSubject.pipe(debounceTime$1(1500)).subscribe((data) => {
2226
2627
  this.loadPreview(data);
@@ -2257,6 +2658,12 @@ class EpistolaRetryFormComponent {
2257
2658
  const processInstanceId = this.formIoStateService.processInstanceId;
2258
2659
  if (!documentId || !processInstanceId)
2259
2660
  return;
2661
+ const taskId = this.taskContext.taskInstanceId;
2662
+ if (!taskId) {
2663
+ this.previewError = 'Preview is only available from within a user task.';
2664
+ this.cdr.markForCheck();
2665
+ return;
2666
+ }
2260
2667
  this.previewLoading = true;
2261
2668
  this.previewError = null;
2262
2669
  this.cdr.markForCheck();
@@ -2265,13 +2672,14 @@ class EpistolaRetryFormComponent {
2265
2672
  URL.revokeObjectURL(this.currentBlobUrl);
2266
2673
  this.currentBlobUrl = null;
2267
2674
  }
2268
- this.http
2269
- .post(`${this.apiEndpoint}/preview`, {
2675
+ this.epistolaPluginService
2676
+ .previewToBlob({
2677
+ taskId,
2270
2678
  documentId,
2271
2679
  processInstanceId,
2272
2680
  sourceActivityId: this.sourceActivityId || null,
2273
2681
  overrides: formData,
2274
- }, { responseType: 'blob', headers: new HttpHeaders().set('X-Skip-Interceptor', '422') })
2682
+ })
2275
2683
  .subscribe({
2276
2684
  next: (blob) => {
2277
2685
  this.currentBlobUrl = URL.createObjectURL(blob);
@@ -2313,8 +2721,15 @@ class EpistolaRetryFormComponent {
2313
2721
  this.cdr.markForCheck();
2314
2722
  return;
2315
2723
  }
2724
+ const taskId = this.taskContext.taskInstanceId;
2725
+ if (!taskId) {
2726
+ this.error = 'Retry form is only available from within a user task.';
2727
+ this.loading = false;
2728
+ this.cdr.markForCheck();
2729
+ return;
2730
+ }
2316
2731
  this.loadSubscription = this.epistolaPluginService
2317
- .getRetryForm(processInstanceId, documentId ?? undefined, this.sourceActivityId)
2732
+ .getRetryForm(taskId, processInstanceId, documentId ?? undefined, this.sourceActivityId)
2318
2733
  .subscribe({
2319
2734
  next: (form) => {
2320
2735
  this.formDefinition = form;
@@ -2337,7 +2752,7 @@ class EpistolaRetryFormComponent {
2337
2752
  },
2338
2753
  });
2339
2754
  }
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 });
2755
+ 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
2756
  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
2757
  <div *ngIf="loading" class="epistola-retry-loading">Loading form...</div>
2343
2758
  <div *ngIf="error" class="epistola-retry-error">{{ error }}</div>
@@ -2376,7 +2791,7 @@ class EpistolaRetryFormComponent {
2376
2791
  </div>
2377
2792
  </div>
2378
2793
  </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 });
2794
+ `, 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
2795
  }
2381
2796
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaRetryFormComponent, decorators: [{
2382
2797
  type: Component,
@@ -2419,7 +2834,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2419
2834
  </div>
2420
2835
  </div>
2421
2836
  `, 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: [{
2837
+ }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }, { type: i2$4.DomSanitizer }, { type: EpistolaTaskContextService }], propDecorators: { value: [{
2423
2838
  type: Input
2424
2839
  }], valueChange: [{
2425
2840
  type: Output
@@ -2431,160 +2846,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2431
2846
  type: Input
2432
2847
  }] } });
2433
2848
 
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
2849
  class EpistolaDocumentPreviewComponent {
2582
2850
  epistolaPluginService;
2583
- http;
2584
2851
  sanitizer;
2585
- configService;
2586
2852
  formIoStateService;
2587
2853
  cdr;
2854
+ taskContext;
2588
2855
  value;
2589
2856
  valueChange = new EventEmitter();
2590
2857
  disabled = false;
@@ -2592,30 +2859,27 @@ class EpistolaDocumentPreviewComponent {
2592
2859
  processDefinitionKey;
2593
2860
  sourceActivityId;
2594
2861
  overrideMapping;
2595
- sources = [];
2596
- selectedIndex = 0;
2597
- discovering = false;
2598
2862
  loading = false;
2599
2863
  error = null;
2600
2864
  previewUrl = null;
2601
2865
  designMode = false;
2602
2866
  initialized = false;
2603
2867
  currentBlobUrl = null;
2604
- discoverSubscription;
2605
2868
  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) {
2869
+ constructor(epistolaPluginService, sanitizer, formIoStateService, cdr, taskContext) {
2612
2870
  this.epistolaPluginService = epistolaPluginService;
2613
- this.http = http;
2614
2871
  this.sanitizer = sanitizer;
2615
- this.configService = configService;
2616
2872
  this.formIoStateService = formIoStateService;
2617
2873
  this.cdr = cdr;
2618
- this.apiEndpoint = `${this.configService.config.valtimoApi.endpointUri}v1/plugin/epistola`;
2874
+ this.taskContext = taskContext;
2875
+ }
2876
+ /**
2877
+ * Resolve the active task id from {@link EpistolaTaskContextService}, populated
2878
+ * by {@code EpistolaTaskContextInterceptor} on the canonical Valtimo task-open
2879
+ * call. Returns null when used outside a task context (e.g. Formio builder).
2880
+ */
2881
+ get currentTaskId() {
2882
+ return this.taskContext.taskInstanceId;
2619
2883
  }
2620
2884
  get overrideMappingScopes() {
2621
2885
  return this.overrideMapping ? Object.keys(this.overrideMapping) : [];
@@ -2636,122 +2900,69 @@ class EpistolaDocumentPreviewComponent {
2636
2900
  this.cdr.markForCheck();
2637
2901
  return;
2638
2902
  }
2639
- if (this.configuredMode) {
2640
- this.loadConfiguredPreview();
2641
- }
2642
- else {
2643
- this.discoverSources();
2903
+ if (!this.sourceActivityId) {
2904
+ this.error = 'Preview is not configured: set the source activity on the form component.';
2905
+ this.cdr.markForCheck();
2906
+ return;
2644
2907
  }
2908
+ this.loadPreview();
2645
2909
  return;
2646
2910
  }
2647
- // In configured mode, react to value changes (input overrides from Formio wrapper)
2648
- if (this.configuredMode && changes['value']) {
2649
- this.loadConfiguredPreview();
2911
+ // React to value changes (input overrides from the Formio wrapper).
2912
+ if (changes['value']) {
2913
+ this.loadPreview();
2650
2914
  }
2651
2915
  }
2652
2916
  ngOnDestroy() {
2653
- this.discoverSubscription?.unsubscribe();
2654
2917
  this.previewSubscription?.unsubscribe();
2655
2918
  this.revokeBlobUrl();
2656
2919
  }
2657
- onSourceChange(event) {
2658
- this.selectedIndex = +event.target.value;
2659
- this.loadDiscoveredPreview();
2660
- }
2661
2920
  refresh() {
2662
- if (this.configuredMode) {
2663
- this.loadConfiguredPreview();
2664
- }
2665
- else {
2666
- this.loadDiscoveredPreview();
2667
- }
2921
+ this.loadPreview();
2668
2922
  }
2669
2923
  /**
2670
- * Configured mode: preview using the explicitly configured process link + input overrides.
2924
+ * Preview using the explicitly configured process link + input overrides.
2925
+ * Requires a runtime task context — the backend authorizes the request against
2926
+ * the task's process instance and case document, so all three ids must match.
2671
2927
  */
2672
- loadConfiguredPreview() {
2928
+ loadPreview() {
2673
2929
  const documentId = this.formIoStateService.documentId;
2674
2930
  if (!documentId) {
2675
2931
  this.error = 'Could not determine document ID from context.';
2676
2932
  this.cdr.markForCheck();
2677
2933
  return;
2678
2934
  }
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.';
2935
+ if (!this.sourceActivityId) {
2936
+ this.error = 'Preview is not configured: set the source activity on the form component.';
2708
2937
  this.cdr.markForCheck();
2709
2938
  return;
2710
2939
  }
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)
2940
+ const processInstanceId = this.formIoStateService.processInstanceId;
2941
+ if (!processInstanceId) {
2942
+ this.error = 'Preview is only available from within a running process.';
2943
+ this.cdr.markForCheck();
2737
2944
  return;
2738
- const documentId = this.formIoStateService.documentId;
2739
- if (!documentId)
2945
+ }
2946
+ const taskId = this.currentTaskId;
2947
+ if (!taskId) {
2948
+ this.error = 'Preview is only available from within a user task.';
2949
+ this.cdr.markForCheck();
2740
2950
  return;
2951
+ }
2741
2952
  this.loading = true;
2742
2953
  this.error = null;
2743
2954
  this.cdr.markForCheck();
2744
2955
  this.revokeBlobUrl();
2745
2956
  this.previewSubscription?.unsubscribe();
2746
- this.previewSubscription = this.http
2747
- .post(`${this.apiEndpoint}/preview`, {
2957
+ this.previewSubscription = this.epistolaPluginService
2958
+ .previewToBlob({
2959
+ taskId,
2748
2960
  documentId,
2749
- processInstanceId: source.processInstanceId,
2750
- sourceActivityId: source.activityId,
2961
+ processDefinitionKey: this.processDefinitionKey || null,
2962
+ processInstanceId,
2963
+ sourceActivityId: this.sourceActivityId,
2964
+ inputOverrides: this.value || null,
2751
2965
  overrides: null,
2752
- }, {
2753
- responseType: 'blob',
2754
- headers: new HttpHeaders().set('X-Skip-Interceptor', '422'),
2755
2966
  })
2756
2967
  .subscribe({
2757
2968
  next: (blob) => this.handlePreviewSuccess(blob),
@@ -2793,7 +3004,7 @@ class EpistolaDocumentPreviewComponent {
2793
3004
  this.previewUrl = null;
2794
3005
  }
2795
3006
  }
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 });
3007
+ 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
3008
  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
3009
  <!-- Design-time view: show configuration summary when no runtime context -->
2799
3010
  <div *ngIf="designMode" class="epistola-preview-panel">
@@ -2829,55 +3040,26 @@ class EpistolaDocumentPreviewComponent {
2829
3040
  <div class="preview-header">
2830
3041
  <span>{{ label || 'Document Preview' }}</span>
2831
3042
  <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
- >
3043
+ <button type="button" class="preview-refresh" [disabled]="loading" (click)="refresh()">
2848
3044
  <i class="mdi mdi-refresh mr-1"></i>
2849
3045
  {{ loading ? 'Generating...' : 'Refresh' }}
2850
3046
  </button>
2851
3047
  </div>
2852
3048
  </div>
2853
3049
  <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">
3050
+ <div *ngIf="loading" class="preview-loading">Generating preview...</div>
3051
+ <div *ngIf="error && !loading" class="preview-unavailable">
2857
3052
  <i class="mdi mdi-information-outline"></i>
2858
- Preview is niet beschikbaar — niet alle gegevens zijn al ingevuld.
3053
+ {{ error }}
2859
3054
  </div>
2860
3055
  <object
2861
- *ngIf="previewUrl && !loading && !discovering"
3056
+ *ngIf="previewUrl && !loading"
2862
3057
  [data]="previewUrl"
2863
3058
  type="application/pdf"
2864
3059
  class="preview-pdf"
2865
3060
  >
2866
3061
  PDF preview is not supported in this browser.
2867
3062
  </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
3063
  </div>
2882
3064
  </div>
2883
3065
  `, 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 +3101,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2919
3101
  <div class="preview-header">
2920
3102
  <span>{{ label || 'Document Preview' }}</span>
2921
3103
  <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
- >
3104
+ <button type="button" class="preview-refresh" [disabled]="loading" (click)="refresh()">
2938
3105
  <i class="mdi mdi-refresh mr-1"></i>
2939
3106
  {{ loading ? 'Generating...' : 'Refresh' }}
2940
3107
  </button>
2941
3108
  </div>
2942
3109
  </div>
2943
3110
  <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">
3111
+ <div *ngIf="loading" class="preview-loading">Generating preview...</div>
3112
+ <div *ngIf="error && !loading" class="preview-unavailable">
2947
3113
  <i class="mdi mdi-information-outline"></i>
2948
- Preview is niet beschikbaar — niet alle gegevens zijn al ingevuld.
3114
+ {{ error }}
2949
3115
  </div>
2950
3116
  <object
2951
- *ngIf="previewUrl && !loading && !discovering"
3117
+ *ngIf="previewUrl && !loading"
2952
3118
  [data]="previewUrl"
2953
3119
  type="application/pdf"
2954
3120
  class="preview-pdf"
2955
3121
  >
2956
3122
  PDF preview is not supported in this browser.
2957
3123
  </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
3124
  </div>
2972
3125
  </div>
2973
3126
  `, 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: [{
3127
+ }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$4.DomSanitizer }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }, { type: EpistolaTaskContextService }], propDecorators: { value: [{
2975
3128
  type: Input
2976
3129
  }], valueChange: [{
2977
3130
  type: Output
@@ -2994,8 +3147,18 @@ class EpistolaAdminPageComponent {
2994
3147
  cards = [];
2995
3148
  selectedCard = null;
2996
3149
  activeTab = 'actions';
3150
+ overviewTab = 'configurations';
2997
3151
  loading = false;
2998
3152
  pluginVersion = null;
3153
+ changelog = null;
3154
+ changelogLoading = false;
3155
+ validationViolations = [];
3156
+ reconcilingExecutionIds = new Set();
3157
+ reconcileFeedback = null;
3158
+ catalogs = [];
3159
+ catalogsLoading = false;
3160
+ redeployingSlugs = new Set();
3161
+ catalogFeedback = null;
2999
3162
  connectionStatuses = [];
3000
3163
  usageEntries = [];
3001
3164
  pendingJobs = [];
@@ -3011,7 +3174,7 @@ class EpistolaAdminPageComponent {
3011
3174
  ngOnInit() {
3012
3175
  this.deepLinkConfigId = this.route.snapshot.queryParamMap.get('configurationId');
3013
3176
  const tab = this.route.snapshot.queryParamMap.get('tab');
3014
- if (tab === 'pending' || tab === 'actions') {
3177
+ if (tab === 'pending' || tab === 'actions' || tab === 'catalogs') {
3015
3178
  this.activeTab = tab;
3016
3179
  }
3017
3180
  this.loadData();
@@ -3021,16 +3184,38 @@ class EpistolaAdminPageComponent {
3021
3184
  this.selectedCard = card;
3022
3185
  this.activeTab = 'actions';
3023
3186
  this.updateUrl(card.configurationId, this.activeTab);
3187
+ this.loadCatalogs(card.configurationId);
3024
3188
  }
3025
3189
  backToOverview() {
3026
3190
  this.selectedCard = null;
3027
3191
  this.activeTab = 'actions';
3192
+ this.catalogs = [];
3193
+ this.catalogFeedback = null;
3028
3194
  this.updateUrl(null, null);
3029
3195
  }
3030
3196
  setActiveTab(tab) {
3031
3197
  this.activeTab = tab;
3032
3198
  this.updateUrl(this.selectedCard?.configurationId ?? null, tab);
3033
3199
  }
3200
+ setOverviewTab(tab) {
3201
+ this.overviewTab = tab;
3202
+ if (tab === 'changelog' && this.changelog === null && !this.changelogLoading) {
3203
+ this.loadChangelog();
3204
+ }
3205
+ }
3206
+ loadChangelog() {
3207
+ this.changelogLoading = true;
3208
+ this.adminService.getChangelog().subscribe({
3209
+ next: (releases) => {
3210
+ this.changelog = releases;
3211
+ this.changelogLoading = false;
3212
+ },
3213
+ error: () => {
3214
+ this.changelog = [];
3215
+ this.changelogLoading = false;
3216
+ },
3217
+ });
3218
+ }
3034
3219
  refresh() {
3035
3220
  this.selectedCard = null;
3036
3221
  this.loadData();
@@ -3047,6 +3232,113 @@ class EpistolaAdminPageComponent {
3047
3232
  },
3048
3233
  });
3049
3234
  }
3235
+ /**
3236
+ * Manually reconcile a single stuck catch event. Pulls the current Epistola job
3237
+ * status and re-runs message correlation if the job is in a terminal state.
3238
+ * Refreshes the Pending list on success so the row drops out of the table.
3239
+ */
3240
+ reconcilePending(job) {
3241
+ if (this.reconcilingExecutionIds.has(job.executionId)) {
3242
+ return;
3243
+ }
3244
+ this.reconcilingExecutionIds.add(job.executionId);
3245
+ this.reconcileFeedback = null;
3246
+ this.adminService.reconcilePending(job.executionId).subscribe({
3247
+ next: (result) => {
3248
+ this.reconcilingExecutionIds.delete(job.executionId);
3249
+ this.reconcileFeedback = {
3250
+ executionId: job.executionId,
3251
+ type: 'success',
3252
+ message: `OK (${result.epistolaStatus}, ${result.correlatedCount ?? 0} correlated)`,
3253
+ };
3254
+ this.loadData();
3255
+ },
3256
+ error: (err) => {
3257
+ this.reconcilingExecutionIds.delete(job.executionId);
3258
+ // 409 from the backend = job is still PENDING/IN_PROGRESS, surface as
3259
+ // informational rather than an error.
3260
+ if (err?.status === 409) {
3261
+ const status = err.error?.epistolaStatus ?? 'still pending';
3262
+ this.reconcileFeedback = {
3263
+ executionId: job.executionId,
3264
+ type: 'pending',
3265
+ message: `Epistola: ${status}. Try again in a moment.`,
3266
+ };
3267
+ }
3268
+ else {
3269
+ const message = err?.error?.detail ?? err?.error?.message ?? err?.message ?? 'unknown error';
3270
+ this.reconcileFeedback = {
3271
+ executionId: job.executionId,
3272
+ type: 'error',
3273
+ message,
3274
+ };
3275
+ }
3276
+ },
3277
+ });
3278
+ }
3279
+ isReconciling(job) {
3280
+ return this.reconcilingExecutionIds.has(job.executionId);
3281
+ }
3282
+ /**
3283
+ * Load the classpath catalogs available to (re)deploy for the given
3284
+ * configuration. Lazy — only called when a configuration is opened, not for
3285
+ * every card in the overview.
3286
+ */
3287
+ loadCatalogs(configurationId) {
3288
+ this.catalogsLoading = true;
3289
+ this.catalogFeedback = null;
3290
+ this.adminService.getClasspathCatalogs(configurationId).subscribe({
3291
+ next: (catalogs) => {
3292
+ this.catalogs = catalogs;
3293
+ this.catalogsLoading = false;
3294
+ },
3295
+ error: () => {
3296
+ this.catalogs = [];
3297
+ this.catalogsLoading = false;
3298
+ },
3299
+ });
3300
+ }
3301
+ /**
3302
+ * Force-redeploy a single classpath catalog to the selected configuration's
3303
+ * Epistola installation. Explicit operator action — the backend bypasses the
3304
+ * templateSyncEnabled gate and the version-skip check. Reloads the catalog
3305
+ * list on success so the deployed version / up-to-date hint refreshes.
3306
+ */
3307
+ redeployCatalog(catalog) {
3308
+ if (!this.selectedCard || this.redeployingSlugs.has(catalog.slug)) {
3309
+ return;
3310
+ }
3311
+ const configurationId = this.selectedCard.configurationId;
3312
+ this.redeployingSlugs.add(catalog.slug);
3313
+ this.catalogFeedback = null;
3314
+ this.adminService.redeployCatalog(configurationId, catalog.slug).subscribe({
3315
+ next: (result) => {
3316
+ this.redeployingSlugs.delete(catalog.slug);
3317
+ this.catalogFeedback = {
3318
+ slug: catalog.slug,
3319
+ type: 'success',
3320
+ message: `OK — installed ${result.installed}, updated ${result.updated}, failed ${result.failed} (of ${result.total})`,
3321
+ };
3322
+ this.loadCatalogs(configurationId);
3323
+ },
3324
+ error: (err) => {
3325
+ this.redeployingSlugs.delete(catalog.slug);
3326
+ const message = err?.error?.errorMessage ??
3327
+ err?.error?.detail ??
3328
+ err?.error?.message ??
3329
+ err?.message ??
3330
+ 'unknown error';
3331
+ this.catalogFeedback = {
3332
+ slug: catalog.slug,
3333
+ type: 'error',
3334
+ message,
3335
+ };
3336
+ },
3337
+ });
3338
+ }
3339
+ isRedeploying(catalog) {
3340
+ return this.redeployingSlugs.has(catalog.slug);
3341
+ }
3050
3342
  updateUrl(configurationId, tab) {
3051
3343
  this.router.navigate([], {
3052
3344
  relativeTo: this.route,
@@ -3098,6 +3390,16 @@ class EpistolaAdminPageComponent {
3098
3390
  this.tryBuildCards();
3099
3391
  },
3100
3392
  });
3393
+ // Validation violations are independent of cards — load alongside but don't
3394
+ // gate the loading flag on them.
3395
+ this.adminService.getValidationViolations().subscribe({
3396
+ next: (violations) => {
3397
+ this.validationViolations = violations;
3398
+ },
3399
+ error: () => {
3400
+ this.validationViolations = [];
3401
+ },
3402
+ });
3101
3403
  }
3102
3404
  tryBuildCards() {
3103
3405
  if (!this.connectionLoaded || !this.usageLoaded || !this.pendingLoaded) {
@@ -3126,6 +3428,7 @@ class EpistolaAdminPageComponent {
3126
3428
  const match = this.cards.find((c) => c.configurationId === this.deepLinkConfigId);
3127
3429
  if (match) {
3128
3430
  this.selectedCard = match;
3431
+ this.loadCatalogs(match.configurationId);
3129
3432
  }
3130
3433
  this.deepLinkConfigId = null;
3131
3434
  }
@@ -3138,13 +3441,13 @@ class EpistolaAdminPageComponent {
3138
3441
  },
3139
3442
  });
3140
3443
  }
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"] }] });
3444
+ 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 });
3445
+ 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 <ng-template #changelogHeading>\n {{ 'epistolaAdminChangelog' | pluginTranslate: 'epistola' | async }}\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\n <cds-tab\n [heading]=\"changelogHeading\"\n [active]=\"overviewTab === 'changelog'\"\n (selected)=\"setOverviewTab('changelog')\"\n >\n <div class=\"d-flex align-items-center mt-3 mb-3\">\n <span class=\"text-muted me-2\">{{\n 'epistolaAdminRunningVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">v{{ pluginVersion || '\u2014' }}</cds-tag>\n </div>\n\n <div *ngIf=\"changelogLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoChangelog' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length > 0\"\n class=\"epistola-changelog\"\n >\n <section *ngFor=\"let release of changelog\" class=\"changelog-release\">\n <div class=\"changelog-release__header\">\n <cds-tag size=\"sm\" type=\"purple\">{{ release.version }}</cds-tag>\n <span *ngIf=\"release.date\" class=\"text-muted ms-2\">{{ release.date }}</span>\n </div>\n <div *ngFor=\"let section of release.sections\" class=\"changelog-section\">\n <h6 class=\"changelog-section__title\">{{ section.title }}</h6>\n <ul class=\"changelog-section__items\">\n <li *ngFor=\"let item of section.items\">{{ item }}</li>\n </ul>\n </div>\n </section>\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 <ng-template #catalogsHeading>\n {{ 'epistolaAdminCatalogs' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ catalogs.length }}</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\n <cds-tab\n [heading]=\"catalogsHeading\"\n [active]=\"activeTab === 'catalogs'\"\n (selected)=\"setActiveTab('catalogs')\"\n >\n <p class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminCatalogsIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"catalogsLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!catalogsLoading && catalogs.length === 0\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminNoCatalogs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"!catalogsLoading && catalogs.length > 0\" class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCatalogSlug' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogVersion' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogStatus' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let catalog of catalogs\">\n <td>\n <code>{{ catalog.slug }}</code>\n </td>\n <td>{{ catalog.version }}</td>\n <td>\n <cds-tag *ngIf=\"catalog.status === 'IN_EPISTOLA'\" size=\"sm\" type=\"green\">\n {{ 'epistolaAdminCatalogInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'NOT_IN_EPISTOLA'\" size=\"sm\" type=\"red\">\n {{ 'epistolaAdminCatalogNotInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'UNKNOWN'\" size=\"sm\" type=\"gray\">\n {{ 'epistolaAdminCatalogStatusUnknown' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"redeployCatalog(catalog)\"\n [disabled]=\"isRedeploying(catalog)\"\n [title]=\"'epistolaAdminRedeployTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploy' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploying' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"catalogFeedback && catalogFeedback.slug === catalog.slug\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"catalogFeedback.type === 'success'\"\n [class.text-danger]=\"catalogFeedback.type === 'error'\"\n >\n {{ catalogFeedback.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}.epistola-admin .epistola-changelog{max-height:60vh;overflow:auto;padding:.5rem 1rem;border:1px solid #dee2e6;border-radius:4px}.epistola-admin .epistola-changelog .changelog-release{padding:.75rem 0}.epistola-admin .epistola-changelog .changelog-release+.changelog-release{border-top:1px solid #eee}.epistola-admin .epistola-changelog .changelog-release__header{display:flex;align-items:center;margin-bottom:.5rem}.epistola-admin .epistola-changelog .changelog-section{margin:.25rem 0 .75rem}.epistola-admin .epistola-changelog .changelog-section__title{font-size:.8125rem;font-weight:600;text-transform:uppercase;letter-spacing:.02em;color:#6f6f6f;margin-bottom:.25rem}.epistola-admin .epistola-changelog .changelog-section__items{margin:0;padding-left:1.25rem;font-size:.8125rem;line-height:1.5}.epistola-admin .epistola-changelog .changelog-section__items li{margin-bottom:.25rem;word-break:break-word}\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
3446
  }
3144
3447
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminPageComponent, decorators: [{
3145
3448
  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 }] });
3449
+ 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 <ng-template #changelogHeading>\n {{ 'epistolaAdminChangelog' | pluginTranslate: 'epistola' | async }}\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\n <cds-tab\n [heading]=\"changelogHeading\"\n [active]=\"overviewTab === 'changelog'\"\n (selected)=\"setOverviewTab('changelog')\"\n >\n <div class=\"d-flex align-items-center mt-3 mb-3\">\n <span class=\"text-muted me-2\">{{\n 'epistolaAdminRunningVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">v{{ pluginVersion || '\u2014' }}</cds-tag>\n </div>\n\n <div *ngIf=\"changelogLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoChangelog' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length > 0\"\n class=\"epistola-changelog\"\n >\n <section *ngFor=\"let release of changelog\" class=\"changelog-release\">\n <div class=\"changelog-release__header\">\n <cds-tag size=\"sm\" type=\"purple\">{{ release.version }}</cds-tag>\n <span *ngIf=\"release.date\" class=\"text-muted ms-2\">{{ release.date }}</span>\n </div>\n <div *ngFor=\"let section of release.sections\" class=\"changelog-section\">\n <h6 class=\"changelog-section__title\">{{ section.title }}</h6>\n <ul class=\"changelog-section__items\">\n <li *ngFor=\"let item of section.items\">{{ item }}</li>\n </ul>\n </div>\n </section>\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 <ng-template #catalogsHeading>\n {{ 'epistolaAdminCatalogs' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ catalogs.length }}</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\n <cds-tab\n [heading]=\"catalogsHeading\"\n [active]=\"activeTab === 'catalogs'\"\n (selected)=\"setActiveTab('catalogs')\"\n >\n <p class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminCatalogsIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"catalogsLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!catalogsLoading && catalogs.length === 0\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminNoCatalogs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"!catalogsLoading && catalogs.length > 0\" class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCatalogSlug' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogVersion' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogStatus' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let catalog of catalogs\">\n <td>\n <code>{{ catalog.slug }}</code>\n </td>\n <td>{{ catalog.version }}</td>\n <td>\n <cds-tag *ngIf=\"catalog.status === 'IN_EPISTOLA'\" size=\"sm\" type=\"green\">\n {{ 'epistolaAdminCatalogInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'NOT_IN_EPISTOLA'\" size=\"sm\" type=\"red\">\n {{ 'epistolaAdminCatalogNotInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'UNKNOWN'\" size=\"sm\" type=\"gray\">\n {{ 'epistolaAdminCatalogStatusUnknown' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"redeployCatalog(catalog)\"\n [disabled]=\"isRedeploying(catalog)\"\n [title]=\"'epistolaAdminRedeployTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploy' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploying' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"catalogFeedback && catalogFeedback.slug === catalog.slug\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"catalogFeedback.type === 'success'\"\n [class.text-danger]=\"catalogFeedback.type === 'error'\"\n >\n {{ catalogFeedback.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}.epistola-admin .epistola-changelog{max-height:60vh;overflow:auto;padding:.5rem 1rem;border:1px solid #dee2e6;border-radius:4px}.epistola-admin .epistola-changelog .changelog-release{padding:.75rem 0}.epistola-admin .epistola-changelog .changelog-release+.changelog-release{border-top:1px solid #eee}.epistola-admin .epistola-changelog .changelog-release__header{display:flex;align-items:center;margin-bottom:.5rem}.epistola-admin .epistola-changelog .changelog-section{margin:.25rem 0 .75rem}.epistola-admin .epistola-changelog .changelog-section__title{font-size:.8125rem;font-weight:600;text-transform:uppercase;letter-spacing:.02em;color:#6f6f6f;margin-bottom:.25rem}.epistola-admin .epistola-changelog .changelog-section__items{margin:0;padding-left:1.25rem;font-size:.8125rem;line-height:1.5}.epistola-admin .epistola-changelog .changelog-section__items li{margin-bottom:.25rem;word-break:break-word}\n"] }]
3450
+ }], ctorParameters: () => [{ type: EpistolaAdminService }, { type: i2$5.ActivatedRoute }, { type: i2$5.Router }] });
3148
3451
 
3149
3452
  function isRuntimeWindow(value) {
3150
3453
  return typeof value === 'object' && value !== null;
@@ -3191,7 +3494,7 @@ const routes = [
3191
3494
  ];
3192
3495
  class EpistolaAdminRoutingModule {
3193
3496
  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] });
3497
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, imports: [i2$5.RouterModule], exports: [RouterModule] });
3195
3498
  static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, imports: [RouterModule.forChild(routes), RouterModule] });
3196
3499
  }
3197
3500
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, decorators: [{
@@ -3202,19 +3505,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3202
3505
  }]
3203
3506
  }] });
3204
3507
 
3205
- const EPISTOLA_DOWNLOAD_OPTIONS = {
3206
- type: 'epistola-download',
3207
- selector: 'epistola-download-button',
3208
- title: 'Epistola Download',
3508
+ const EPISTOLA_DOCUMENT_OPTIONS = {
3509
+ type: 'epistola-document',
3510
+ selector: 'epistola-document-element',
3511
+ title: 'Epistola Document',
3209
3512
  group: 'basic',
3210
- icon: 'download',
3513
+ icon: 'file-pdf-o',
3211
3514
  emptyValue: null,
3212
- fieldOptions: ['filename', 'label'],
3515
+ fieldOptions: ['label', 'display', 'documentVariable', 'tenantIdVariable', 'filename'],
3213
3516
  };
3214
- function registerEpistolaDownloadComponent(injector) {
3215
- if (!customElements.get(EPISTOLA_DOWNLOAD_OPTIONS.selector)) {
3216
- registerCustomFormioComponent(EPISTOLA_DOWNLOAD_OPTIONS, EpistolaDownloadComponent, injector);
3517
+ function registerEpistolaDocumentComponent(injector) {
3518
+ if (customElements.get(EPISTOLA_DOCUMENT_OPTIONS.selector)) {
3519
+ return;
3217
3520
  }
3521
+ registerCustomFormioComponent(EPISTOLA_DOCUMENT_OPTIONS, EpistolaDocumentComponent, injector);
3218
3522
  }
3219
3523
 
3220
3524
  const EPISTOLA_RETRY_FORM_OPTIONS = {
@@ -3232,21 +3536,6 @@ function registerEpistolaRetryFormComponent(injector) {
3232
3536
  }
3233
3537
  }
3234
3538
 
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
3539
  const EPISTOLA_DOCUMENT_PREVIEW_OPTIONS = {
3251
3540
  type: 'epistola-document-preview',
3252
3541
  selector: 'epistola-document-preview-element',
@@ -3798,6 +4087,11 @@ class EpistolaPluginModule {
3798
4087
  ngModule: EpistolaPluginModule,
3799
4088
  providers: [
3800
4089
  EpistolaMenuService,
4090
+ {
4091
+ provide: HTTP_INTERCEPTORS,
4092
+ useClass: EpistolaTaskContextInterceptor,
4093
+ multi: true,
4094
+ },
3801
4095
  {
3802
4096
  provide: ENVIRONMENT_INITIALIZER,
3803
4097
  multi: true,
@@ -3805,9 +4099,8 @@ class EpistolaPluginModule {
3805
4099
  if (!isEpistolaEnabled())
3806
4100
  return;
3807
4101
  const injector = inject(Injector);
3808
- registerEpistolaDownloadComponent(injector);
4102
+ registerEpistolaDocumentComponent(injector);
3809
4103
  registerEpistolaRetryFormComponent(injector);
3810
- registerEpistolaPreviewButtonComponent(injector);
3811
4104
  registerEpistolaOverrideBuilderComponent(injector);
3812
4105
  registerEpistolaProcessLinkSelectorComponent(injector);
3813
4106
  registerEpistolaDocumentPreviewComponent(injector);
@@ -3830,17 +4123,15 @@ class EpistolaPluginModule {
3830
4123
  GenerateDocumentConfigurationComponent,
3831
4124
  CheckJobStatusConfigurationComponent,
3832
4125
  DownloadDocumentConfigurationComponent,
3833
- EpistolaDownloadComponent,
4126
+ EpistolaDocumentComponent,
3834
4127
  EpistolaRetryFormComponent,
3835
- EpistolaPreviewButtonComponent,
3836
4128
  EpistolaDocumentPreviewComponent,
3837
4129
  EpistolaAdminPageComponent], exports: [EpistolaConfigurationComponent,
3838
4130
  GenerateDocumentConfigurationComponent,
3839
4131
  CheckJobStatusConfigurationComponent,
3840
4132
  DownloadDocumentConfigurationComponent,
3841
- EpistolaDownloadComponent,
4133
+ EpistolaDocumentComponent,
3842
4134
  EpistolaRetryFormComponent,
3843
- EpistolaPreviewButtonComponent,
3844
4135
  EpistolaDocumentPreviewComponent,
3845
4136
  EpistolaAdminPageComponent] });
3846
4137
  static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaPluginModule, providers: [EpistolaPluginService, EpistolaAdminService], imports: [CommonModule,
@@ -3854,9 +4145,8 @@ class EpistolaPluginModule {
3854
4145
  GenerateDocumentConfigurationComponent,
3855
4146
  CheckJobStatusConfigurationComponent,
3856
4147
  DownloadDocumentConfigurationComponent,
3857
- EpistolaDownloadComponent,
4148
+ EpistolaDocumentComponent,
3858
4149
  EpistolaRetryFormComponent,
3859
- EpistolaPreviewButtonComponent,
3860
4150
  EpistolaDocumentPreviewComponent,
3861
4151
  EpistolaAdminPageComponent] });
3862
4152
  }
@@ -3875,9 +4165,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3875
4165
  GenerateDocumentConfigurationComponent,
3876
4166
  CheckJobStatusConfigurationComponent,
3877
4167
  DownloadDocumentConfigurationComponent,
3878
- EpistolaDownloadComponent,
4168
+ EpistolaDocumentComponent,
3879
4169
  EpistolaRetryFormComponent,
3880
- EpistolaPreviewButtonComponent,
3881
4170
  EpistolaDocumentPreviewComponent,
3882
4171
  EpistolaAdminPageComponent,
3883
4172
  ],
@@ -3886,9 +4175,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3886
4175
  GenerateDocumentConfigurationComponent,
3887
4176
  CheckJobStatusConfigurationComponent,
3888
4177
  DownloadDocumentConfigurationComponent,
3889
- EpistolaDownloadComponent,
4178
+ EpistolaDocumentComponent,
3890
4179
  EpistolaRetryFormComponent,
3891
- EpistolaPreviewButtonComponent,
3892
4180
  EpistolaDocumentPreviewComponent,
3893
4181
  EpistolaAdminPageComponent,
3894
4182
  ],
@@ -3910,9 +4198,9 @@ const epistolaPluginSpecification = {
3910
4198
  pluginLogoBase64: EPISTOLA_PLUGIN_LOGO_BASE64,
3911
4199
  // Map action keys to their configuration components
3912
4200
  functionConfigurationComponents: {
3913
- 'generate-document': GenerateDocumentConfigurationComponent,
3914
- 'check-job-status': CheckJobStatusConfigurationComponent,
3915
- 'download-document': DownloadDocumentConfigurationComponent,
4201
+ 'epistola-generate-document': GenerateDocumentConfigurationComponent,
4202
+ 'epistola-check-job-status': CheckJobStatusConfigurationComponent,
4203
+ 'epistola-download-document': DownloadDocumentConfigurationComponent,
3916
4204
  },
3917
4205
  // Translations
3918
4206
  pluginTranslations: {
@@ -3930,7 +4218,7 @@ const epistolaPluginSpecification = {
3930
4218
  defaultEnvironmentIdTooltip: 'De standaard omgeving voor documentgeneratie (3-30 tekens, alleen kleine letters, cijfers en koppeltekens, bijv. "productie")',
3931
4219
  templateSyncEnabled: 'Template synchronisatie',
3932
4220
  templateSyncEnabledTooltip: 'Synchroniseer template definities automatisch van het classpath naar Epistola bij het opstarten',
3933
- 'generate-document': 'Genereer Document',
4221
+ 'epistola-generate-document': 'Genereer Document',
3934
4222
  catalogId: 'Catalogus',
3935
4223
  catalogIdTooltip: 'Selecteer de catalogus waaruit een template gekozen wordt',
3936
4224
  templateId: 'Template',
@@ -4007,7 +4295,7 @@ const epistolaPluginSpecification = {
4007
4295
  sourceFieldPlaceholder: 'Bronveldnaam',
4008
4296
  noTemplateFields: 'Geen template velden beschikbaar',
4009
4297
  // Check job status action
4010
- 'check-job-status': 'Controleer Taakstatus',
4298
+ 'epistola-check-job-status': 'Controleer Taakstatus',
4011
4299
  requestIdVariable: 'Request ID Variabele',
4012
4300
  requestIdVariableTooltip: 'Naam van de procesvariabele met het Epistola request ID',
4013
4301
  statusVariable: 'Status Variabele',
@@ -4017,7 +4305,9 @@ const epistolaPluginSpecification = {
4017
4305
  errorMessageVariable: 'Foutmelding Variabele',
4018
4306
  errorMessageVariableTooltip: 'Naam van de procesvariabele waarin de foutmelding wordt opgeslagen (bij fout)',
4019
4307
  // Download document action
4020
- 'download-document': 'Download Document',
4308
+ 'epistola-download-document': 'Download Document',
4309
+ documentVariable: 'Document Variabele',
4310
+ 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
4311
  contentVariable: 'Inhoud Variabele',
4022
4312
  contentVariableTooltip: 'Naam van de procesvariabele waarin de documentinhoud (Base64) wordt opgeslagen',
4023
4313
  // Admin page
@@ -4044,6 +4334,31 @@ const epistolaPluginSpecification = {
4044
4334
  epistolaAdminNoPendingJobs: 'Geen wachtende taken voor deze verbinding.',
4045
4335
  epistolaAdminConfiguration: 'Configuratie',
4046
4336
  epistolaAdminRequestId: 'Request ID',
4337
+ epistolaAdminReconcile: 'Hersynchroniseer',
4338
+ epistolaAdminReconciling: 'Bezig...',
4339
+ epistolaAdminReconcileTooltip: 'Vraag de huidige status op bij Epistola en hervat het wachtende proces als het klaar is.',
4340
+ epistolaAdminConfigurations: 'Configuraties',
4341
+ epistolaAdminValidations: 'BPMN-validatie',
4342
+ epistolaAdminNoValidations: 'Geen race-onveilige procesdefinities gevonden. Alles ziet er goed uit.',
4343
+ epistolaAdminValidationWarningTitle: 'BPMN configuratie waarschuwing',
4344
+ 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.',
4345
+ epistolaAdminValidationCode: 'Code',
4346
+ epistolaAdminValidationMessage: 'Bericht',
4347
+ epistolaAdminCatalogs: 'Catalogi',
4348
+ epistolaAdminCatalogsIntro: 'Catalogi op het classpath van de applicatie. Deze worden bij het opstarten automatisch uitgerold; gebruik Opnieuw uitrollen om een catalogus handmatig (geforceerd) naar deze Epistola-omgeving te sturen, bijvoorbeeld als de automatische uitrol is mislukt.',
4349
+ epistolaAdminNoCatalogs: 'Geen catalogi gevonden op het classpath.',
4350
+ epistolaAdminCatalogSlug: 'Slug',
4351
+ epistolaAdminCatalogVersion: 'Versie',
4352
+ epistolaAdminCatalogStatus: 'Status in Epistola',
4353
+ epistolaAdminCatalogInEpistola: 'Aanwezig in Epistola',
4354
+ epistolaAdminCatalogNotInEpistola: 'Niet in Epistola',
4355
+ epistolaAdminCatalogStatusUnknown: 'Onbekend (Epistola niet bereikbaar)',
4356
+ epistolaAdminRedeploy: 'Opnieuw uitrollen',
4357
+ epistolaAdminRedeploying: 'Bezig...',
4358
+ epistolaAdminRedeployTooltip: 'Bouw deze catalogus opnieuw vanaf het classpath en stuur hem geforceerd naar Epistola, ongeacht de versie of de templateSyncEnabled-instelling.',
4359
+ epistolaAdminChangelog: 'Changelog',
4360
+ epistolaAdminRunningVersion: 'Actieve plugin-versie:',
4361
+ epistolaAdminNoChangelog: 'Geen changelog beschikbaar in deze build.',
4047
4362
  },
4048
4363
  en: {
4049
4364
  title: 'Epistola Document Suite',
@@ -4059,7 +4374,7 @@ const epistolaPluginSpecification = {
4059
4374
  defaultEnvironmentIdTooltip: 'The default environment for document generation (3-30 chars, lowercase letters, digits and hyphens only, e.g. "production")',
4060
4375
  templateSyncEnabled: 'Template sync',
4061
4376
  templateSyncEnabledTooltip: 'Automatically synchronize template definitions from classpath to Epistola on startup',
4062
- 'generate-document': 'Generate Document',
4377
+ 'epistola-generate-document': 'Generate Document',
4063
4378
  catalogId: 'Catalog',
4064
4379
  catalogIdTooltip: 'Select the catalog to choose a template from',
4065
4380
  templateId: 'Template',
@@ -4136,7 +4451,7 @@ const epistolaPluginSpecification = {
4136
4451
  sourceFieldPlaceholder: 'Source field name',
4137
4452
  noTemplateFields: 'No template fields available',
4138
4453
  // Check job status action
4139
- 'check-job-status': 'Check Job Status',
4454
+ 'epistola-check-job-status': 'Check Job Status',
4140
4455
  requestIdVariable: 'Request ID Variable',
4141
4456
  requestIdVariableTooltip: 'Name of the process variable containing the Epistola request ID',
4142
4457
  statusVariable: 'Status Variable',
@@ -4146,7 +4461,9 @@ const epistolaPluginSpecification = {
4146
4461
  errorMessageVariable: 'Error Message Variable',
4147
4462
  errorMessageVariableTooltip: 'Name of the process variable to store the error message in (when failed)',
4148
4463
  // Download document action
4149
- 'download-document': 'Download Document',
4464
+ 'epistola-download-document': 'Download Document',
4465
+ documentVariable: 'Document Variable',
4466
+ 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
4467
  contentVariable: 'Content Variable',
4151
4468
  contentVariableTooltip: 'Name of the process variable to store the document content (Base64) in',
4152
4469
  // Admin page
@@ -4173,6 +4490,31 @@ const epistolaPluginSpecification = {
4173
4490
  epistolaAdminNoPendingJobs: 'No pending jobs for this connection.',
4174
4491
  epistolaAdminConfiguration: 'Configuration',
4175
4492
  epistolaAdminRequestId: 'Request ID',
4493
+ epistolaAdminReconcile: 'Reconcile',
4494
+ epistolaAdminReconciling: 'Reconciling...',
4495
+ epistolaAdminReconcileTooltip: "Ask Epistola for this job's current status and resume the waiting process if it has finished.",
4496
+ epistolaAdminConfigurations: 'Configurations',
4497
+ epistolaAdminValidations: 'BPMN validation',
4498
+ epistolaAdminNoValidations: 'No race-unsafe process definitions detected. Everything looks good.',
4499
+ epistolaAdminValidationWarningTitle: 'BPMN configuration warning',
4500
+ 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.',
4501
+ epistolaAdminValidationCode: 'Code',
4502
+ epistolaAdminValidationMessage: 'Message',
4503
+ epistolaAdminCatalogs: 'Catalogs',
4504
+ epistolaAdminCatalogsIntro: 'Catalogs on the application classpath. These are deployed automatically on startup; use Redeploy to manually (force) push one to this Epistola installation — for example when the automatic startup deploy failed.',
4505
+ epistolaAdminNoCatalogs: 'No catalogs found on the classpath.',
4506
+ epistolaAdminCatalogSlug: 'Slug',
4507
+ epistolaAdminCatalogVersion: 'Version',
4508
+ epistolaAdminCatalogStatus: 'Status in Epistola',
4509
+ epistolaAdminCatalogInEpistola: 'Present in Epistola',
4510
+ epistolaAdminCatalogNotInEpistola: 'Not in Epistola',
4511
+ epistolaAdminCatalogStatusUnknown: 'Unknown (Epistola unreachable)',
4512
+ epistolaAdminRedeploy: 'Redeploy',
4513
+ epistolaAdminRedeploying: 'Redeploying...',
4514
+ epistolaAdminRedeployTooltip: 'Rebuild this catalog from the classpath and force-push it to Epistola, regardless of version or the templateSyncEnabled setting.',
4515
+ epistolaAdminChangelog: 'Changelog',
4516
+ epistolaAdminRunningVersion: 'Running plugin version:',
4517
+ epistolaAdminNoChangelog: 'No changelog available in this build.',
4176
4518
  },
4177
4519
  },
4178
4520
  };
@@ -4185,5 +4527,5 @@ const epistolaPluginSpecification = {
4185
4527
  * Generated bundle index. Do not edit.
4186
4528
  */
4187
4529
 
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 };
4530
+ 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
4531
  //# sourceMappingURL=epistola.app-valtimo-plugin.mjs.map