@epistola.app/valtimo-plugin 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/fesm2022/epistola.app-valtimo-plugin.mjs +1514 -456
  2. package/fesm2022/epistola.app-valtimo-plugin.mjs.map +1 -1
  3. package/lib/assets/epistola-logo.d.ts +1 -1
  4. package/lib/components/check-job-status-configuration/check-job-status-configuration.component.d.ts +4 -1
  5. package/lib/components/download-document-configuration/download-document-configuration.component.d.ts +8 -1
  6. package/lib/components/epistola-admin-page/epistola-admin-page.component.d.ts +17 -1
  7. package/lib/components/epistola-document/epistola-document.component.d.ts +65 -0
  8. package/lib/components/{epistola-download/epistola-download.formio.d.ts → epistola-document/epistola-document.formio.d.ts} +2 -2
  9. package/lib/components/epistola-document-preview/epistola-document-preview.component.d.ts +29 -18
  10. package/lib/components/epistola-document-preview/preview-utils.d.ts +18 -0
  11. package/lib/components/epistola-retry-form/epistola-retry-form.component.d.ts +3 -7
  12. package/lib/components/generate-document-configuration/generate-document-configuration.component.d.ts +20 -1
  13. package/lib/components/override-builder/override-builder.component.d.ts +42 -0
  14. package/lib/components/override-builder/override-builder.formio.d.ts +4 -0
  15. package/lib/components/process-link-selector/process-link-selector.component.d.ts +31 -0
  16. package/lib/components/process-link-selector/process-link-selector.formio.d.ts +4 -0
  17. package/lib/epistola-enabled.guard.d.ts +2 -0
  18. package/lib/epistola-runtime-config.d.ts +28 -0
  19. package/lib/epistola.module.d.ts +4 -5
  20. package/lib/models/admin.d.ts +28 -0
  21. package/lib/models/config.d.ts +27 -12
  22. package/lib/services/epistola-admin.service.d.ts +14 -1
  23. package/lib/services/epistola-plugin.service.d.ts +51 -7
  24. package/lib/services/epistola-task-context.interceptor.d.ts +29 -0
  25. package/lib/services/epistola-task-context.matcher.d.ts +19 -0
  26. package/lib/services/epistola-task-context.service.d.ts +26 -0
  27. package/lib/services/index.d.ts +2 -0
  28. package/package.json +4 -2
  29. package/public_api.d.ts +7 -4
  30. package/sbom.json +1 -0
  31. package/lib/components/epistola-download/epistola-download.component.d.ts +0 -24
  32. package/lib/components/epistola-preview-button/epistola-preview-button.component.d.ts +0 -35
  33. 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
- import { Injectable, EventEmitter, Output, Input, Component, ChangeDetectionStrategy, NgModule, ENVIRONMENT_INITIALIZER, inject, Injector } from '@angular/core';
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';
@@ -12,16 +12,16 @@ import { CommonModule } from '@angular/common';
12
12
  import * as i2$1 from '@valtimo/plugin';
13
13
  import { PluginTranslatePipeModule } from '@valtimo/plugin';
14
14
  import { startWith, delay, shareReplay, take as take$1, takeUntil as takeUntil$1, filter, map, distinctUntilChanged, tap, switchMap, catchError, debounceTime as debounceTime$1 } from 'rxjs/operators';
15
- import * as i4 from '@angular/forms';
15
+ import * as i2$2 from '@angular/forms';
16
16
  import { FormsModule } from '@angular/forms';
17
17
  import * as _jsonata from 'jsonata';
18
- import * as i2$2 from '@valtimo/process-link';
19
- import * as i7 from '@formio/angular';
18
+ import * as i2$3 from '@valtimo/process-link';
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$1 from '@angular/platform-browser';
22
- import * as i2$3 from '@angular/router';
23
- import { RouterModule } from '@angular/router';
24
- import * as i5 from 'carbon-components-angular/tabs';
22
+ import * as i2$5 from '@angular/router';
23
+ import { RouterModule, Router } from '@angular/router';
24
+ import * as i5$1 from 'carbon-components-angular/tabs';
25
25
  import { TabsModule } from 'carbon-components-angular/tabs';
26
26
  import * as i6 from 'carbon-components-angular/tag';
27
27
  import { TagModule } from 'carbon-components-angular/tag';
@@ -77,6 +77,23 @@ class EpistolaAdminService {
77
77
  getPendingJobs() {
78
78
  return this.http.get(`${this.apiEndpoint}/pending`);
79
79
  }
80
+ /**
81
+ * Manually reconcile a stuck Epistola catch event by querying Epistola for the
82
+ * job's current status and re-running message correlation. Returns 200 with
83
+ * `correlated=true` on success, 409 with `correlated=false` when the job is
84
+ * still in flight, or surfaces a validation error when the execution can't be
85
+ * recovered (no subscription, missing variable, unknown tenant).
86
+ */
87
+ reconcilePending(executionId) {
88
+ return this.http.post(`${this.apiEndpoint}/pending/${encodeURIComponent(executionId)}/reconcile`, null);
89
+ }
90
+ /**
91
+ * Get the latest BPMN race-safety validation violations across deployed
92
+ * process definitions. Empty list = healthy.
93
+ */
94
+ getValidationViolations() {
95
+ return this.http.get(`${this.apiEndpoint}/validations`);
96
+ }
80
97
  /**
81
98
  * Export a single process link as a .process-link.json file.
82
99
  */
@@ -220,9 +237,11 @@ class EpistolaPluginService {
220
237
  }
221
238
  /**
222
239
  * Get a dynamically generated Formio form for retrying a failed document generation.
240
+ *
241
+ * @param taskId Operaton user task id (required — backend authorizes via OperatonTask:VIEW)
223
242
  */
224
- getRetryForm(processInstanceId, documentId, sourceActivityId) {
225
- const params = { processInstanceId };
243
+ getRetryForm(taskId, processInstanceId, documentId, sourceActivityId) {
244
+ const params = { taskId, processInstanceId };
226
245
  if (documentId) {
227
246
  params['documentId'] = documentId;
228
247
  }
@@ -238,24 +257,43 @@ class EpistolaPluginService {
238
257
  return this.http.get(`${this.apiEndpoint}/expression-functions`);
239
258
  }
240
259
  /**
241
- * Discover all previewable document sources for a given Valtimo document.
260
+ * Validate the JSONata syntax of action-config expressions before save.
261
+ * Parse-only; runtime errors (missing variables, type mismatches) are not detected.
262
+ */
263
+ validateJsonata(request) {
264
+ return this.http.post(`${this.apiEndpoint}/validate-jsonata`, request);
265
+ }
266
+ /**
267
+ * Preview a document by dry-running the {@code generate-document} process
268
+ * link. Returns the rendered PDF as a {@link Blob}.
269
+ *
270
+ * <p>The {@code X-Skip-Interceptor: 422} header tells the global Valtimo
271
+ * error interceptor to skip its toast for validation failures so the
272
+ * caller can render an inline error message.
242
273
  */
243
- getPreviewSources(documentId) {
244
- return this.http.get(`${this.apiEndpoint}/preview-sources`, {
245
- params: { documentId },
274
+ previewToBlob(request) {
275
+ return this.http.post(`${this.apiEndpoint}/preview`, request, {
276
+ responseType: 'blob',
277
+ headers: new HttpHeaders().set('X-Skip-Interceptor', '422'),
246
278
  });
247
279
  }
248
280
  /**
249
- * Preview a document by dry-running the generate-document process link.
250
- * Returns the resolved data as a mock preview (Phase 1).
281
+ * Stream an already-generated Epistola PDF for the caller's task. The
282
+ * backend resolves the Epistola document id and tenant id from the named
283
+ * process variables on the task's process instance, so the wire never
284
+ * carries a raw PDF id.
251
285
  */
252
- previewDocument(documentId, processDefinitionKey, sourceActivityId, processInstanceId, overrides) {
253
- return this.http.post(`${this.apiEndpoint}/preview`, {
254
- documentId,
255
- processDefinitionKey,
256
- sourceActivityId,
257
- processInstanceId: processInstanceId || null,
258
- overrides: overrides || null,
286
+ downloadDocumentBlob(request) {
287
+ const params = new URLSearchParams({
288
+ taskId: request.taskId,
289
+ caseDocumentId: request.caseDocumentId,
290
+ documentVariable: request.documentVariable,
291
+ tenantIdVariable: request.tenantIdVariable,
292
+ filename: request.filename,
293
+ disposition: request.disposition,
294
+ });
295
+ return this.http.get(`${this.apiEndpoint}/documents/download?${params.toString()}`, {
296
+ responseType: 'blob',
259
297
  });
260
298
  }
261
299
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaPluginService, deps: [{ token: i1.HttpClient }, { token: i2.ConfigService }], target: i0.ɵɵFactoryTarget.Injectable });
@@ -265,6 +303,101 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
265
303
  type: Injectable
266
304
  }], ctorParameters: () => [{ type: i1.HttpClient }, { type: i2.ConfigService }] });
267
305
 
306
+ /**
307
+ * Holds the currently-open Operaton user task instance id so that custom
308
+ * Formio components rendered inside a Valtimo task form (preview, download,
309
+ * retry-form) can include it in backend requests for PBAC.
310
+ *
311
+ * Populated by {@link EpistolaTaskContextInterceptor}, which sniffs Valtimo's
312
+ * canonical "load process link for task" GET (`/api/v2/process-link/task/{taskId}`)
313
+ * — the request always fires when a task opens, before the form renders.
314
+ *
315
+ * <p><b>Why this exists:</b> Valtimo 13.21 does not expose the active task
316
+ * instance id through any service that custom Formio components can inject
317
+ * (`FormIoStateService` carries documentId and processInstanceId only;
318
+ * `TaskDetailContentComponent.taskInstanceId$` is private to that component
319
+ * and Formio elements live in their own injector tree). This service is a
320
+ * workaround until upstream exposes `taskInstanceId` via `FormIoStateService`.
321
+ */
322
+ class EpistolaTaskContextService {
323
+ _taskInstanceId$ = new BehaviorSubject(null);
324
+ taskInstanceId$ = this._taskInstanceId$.asObservable();
325
+ get taskInstanceId() {
326
+ return this._taskInstanceId$.value;
327
+ }
328
+ setTaskInstanceId(id) {
329
+ this._taskInstanceId$.next(id);
330
+ }
331
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
332
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextService, providedIn: 'root' });
333
+ }
334
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextService, decorators: [{
335
+ type: Injectable,
336
+ args: [{ providedIn: 'root' }]
337
+ }] });
338
+
339
+ /**
340
+ * Pure helpers for {@link EpistolaTaskContextInterceptor}, extracted so they
341
+ * can be unit-tested without pulling in {@code @angular/core} (which ts-jest
342
+ * cannot transform without {@code jest-preset-angular}).
343
+ */
344
+ /**
345
+ * Pattern Valtimo uses for the canonical task-open call:
346
+ * {@code GET /api/v2/process-link/task/{taskInstanceId}}.
347
+ *
348
+ * Captures the {@code taskInstanceId} (UUID v4-style 36-character hyphenated
349
+ * hex string). Anchored at the end of the URL or at a query-string delimiter
350
+ * so we don't accidentally match a longer trailing segment.
351
+ */
352
+ const TASK_PROCESS_LINK_PATTERN = /\/api\/v2\/process-link\/task\/([0-9a-fA-F-]{36})(?:\?|$)/;
353
+ /**
354
+ * Returns the captured {@code taskInstanceId} from a Valtimo task-open
355
+ * request URL, or {@code null} if the request does not match.
356
+ */
357
+ function extractTaskInstanceIdFromUrl(method, url) {
358
+ if (method !== 'GET')
359
+ return null;
360
+ const match = TASK_PROCESS_LINK_PATTERN.exec(url);
361
+ return match ? match[1] : null;
362
+ }
363
+
364
+ /**
365
+ * Sniffs Valtimo's task-open signal and pushes the active taskInstanceId into
366
+ * {@link EpistolaTaskContextService}. The signal is the canonical
367
+ * {@code GET /api/v2/process-link/task/{taskId}} call that
368
+ * {@code TaskDetailContentComponent.loadTaskDetails(...)} fires unconditionally
369
+ * before any task form is rendered (see @valtimo/task internals).
370
+ *
371
+ * <p>This interceptor does <b>not</b> modify the outgoing request. It only
372
+ * captures the taskId from the URL.
373
+ *
374
+ * <p>Workaround for Valtimo 13.21 not exposing taskInstanceId through any
375
+ * injectable service. Remove once upstream adds e.g.
376
+ * {@code FormIoStateService.setTaskInstanceId(...)}.
377
+ *
378
+ * <p>The actual URL-matching logic lives in
379
+ * {@link extractTaskInstanceIdFromUrl} so it can be unit-tested without an
380
+ * Angular harness.
381
+ */
382
+ class EpistolaTaskContextInterceptor {
383
+ taskContext;
384
+ constructor(taskContext) {
385
+ this.taskContext = taskContext;
386
+ }
387
+ intercept(request, next) {
388
+ const taskId = extractTaskInstanceIdFromUrl(request.method, request.url);
389
+ if (taskId !== null && taskId !== this.taskContext.taskInstanceId) {
390
+ this.taskContext.setTaskInstanceId(taskId);
391
+ }
392
+ return next.handle(request);
393
+ }
394
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextInterceptor, deps: [{ token: EpistolaTaskContextService }], target: i0.ɵɵFactoryTarget.Injectable });
395
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextInterceptor });
396
+ }
397
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextInterceptor, decorators: [{
398
+ type: Injectable
399
+ }], ctorParameters: () => [{ type: EpistolaTaskContextService }] });
400
+
268
401
  class EpistolaConfigurationComponent {
269
402
  save$;
270
403
  disabled$;
@@ -796,7 +929,7 @@ class BuilderFieldComponent {
796
929
  ></epistola-builder-field>
797
930
  </div>
798
931
  </div>
799
- `, isInline: true, styles: [".builder-field{margin-bottom:4px}.builder-field__name{margin-bottom:2px}.builder-field__name--clickable{cursor:pointer;-webkit-user-select:none;user-select:none}.builder-field__name--clickable:hover{color:#0f62fe}.builder-field__chevron{font-size:.7em;margin-right:4px}.builder-field__label{font-weight:500;font-size:.9em}.builder-field__required{color:#da1e28;margin-left:2px}.builder-field__type{color:#8d8d8d;font-size:.8em;margin-left:4px}.builder-field__value{display:flex;align-items:center;gap:4px}.builder-field__input{flex:1;padding:6px 8px;border:1px solid #e0e0e0;border-radius:4px;font-size:.85em;font-family:IBM Plex Mono,monospace}.builder-field__input:focus{outline:2px solid #0f62fe;border-color:#0f62fe}.builder-field__input--raw{background:#f4f4f4}.builder-field__mode-toggle{width:28px;height:28px;border:1px solid #e0e0e0;border-radius:4px;background:#fff;cursor:pointer;font-family:monospace;font-size:.8em;display:flex;align-items:center;justify-content:center}.builder-field__mode-toggle:hover{background:#f4f4f4}.builder-field__children{border-left:2px solid #e0e0e0;padding-left:12px;margin-top:4px}\n"], dependencies: [{ kind: "component", type: BuilderFieldComponent, selector: "epistola-builder-field", inputs: ["field", "path", "suggestions", "disabled", "collapsed", "required", "collapsedPaths"], outputs: ["valueChange", "modeToggle", "collapseToggle"] }, { 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: "ngmodule", type: FormsModule }, { kind: "directive", type: i4.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i4.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i4.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
932
+ `, isInline: true, styles: [".builder-field{margin-bottom:4px}.builder-field__name{margin-bottom:2px}.builder-field__name--clickable{cursor:pointer;-webkit-user-select:none;user-select:none}.builder-field__name--clickable:hover{color:#0f62fe}.builder-field__chevron{font-size:.7em;margin-right:4px}.builder-field__label{font-weight:500;font-size:.9em}.builder-field__required{color:#da1e28;margin-left:2px}.builder-field__type{color:#8d8d8d;font-size:.8em;margin-left:4px}.builder-field__value{display:flex;align-items:center;gap:4px}.builder-field__input{flex:1;padding:6px 8px;border:1px solid #e0e0e0;border-radius:4px;font-size:.85em;font-family:IBM Plex Mono,monospace}.builder-field__input:focus{outline:2px solid #0f62fe;border-color:#0f62fe}.builder-field__input--raw{background:#f4f4f4}.builder-field__mode-toggle{width:28px;height:28px;border:1px solid #e0e0e0;border-radius:4px;background:#fff;cursor:pointer;font-family:monospace;font-size:.8em;display:flex;align-items:center;justify-content:center}.builder-field__mode-toggle:hover{background:#f4f4f4}.builder-field__children{border-left:2px solid #e0e0e0;padding-left:12px;margin-top:4px}\n"], dependencies: [{ kind: "component", type: BuilderFieldComponent, selector: "epistola-builder-field", inputs: ["field", "path", "suggestions", "disabled", "collapsed", "required", "collapsedPaths"], outputs: ["valueChange", "modeToggle", "collapseToggle"] }, { 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: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
800
933
  }
801
934
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: BuilderFieldComponent, decorators: [{
802
935
  type: Component,
@@ -1379,7 +1512,7 @@ class MappingPreviewComponent {
1379
1512
  <strong>{{ missingRequired.join(', ') }}</strong>
1380
1513
  </div>
1381
1514
  </div>
1382
- `, isInline: true, styles: [".preview{border:1px solid #e0e0e0;border-radius:4px;margin-top:16px;overflow:hidden}.preview__header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:#f4f4f4;border-bottom:1px solid #e0e0e0}.preview__title{font-weight:600;font-size:.85em}.preview__controls{display:flex;gap:4px}.preview__doc-input{padding:4px 8px;border:1px solid #e0e0e0;border-radius:4px;font-size:.8em;width:220px;font-family:monospace}.preview__run-btn{padding:4px 10px;border:1px solid #0f62fe;border-radius:4px;background:#0f62fe;color:#fff;cursor:pointer;font-size:.8em}.preview__run-btn:disabled{opacity:.4;cursor:not-allowed}.preview__panels{display:grid;grid-template-columns:1fr 1fr;gap:1px;background:#e0e0e0}.preview__panel{background:#fff;padding:8px 12px;min-height:80px}.preview__panel-label{font-size:.75em;color:#6f6f6f;text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px}.preview__code{font-family:IBM Plex Mono,monospace;font-size:.8em;line-height:1.4;margin:0;white-space:pre-wrap;word-break:break-word}.preview__loading{color:#8d8d8d}.preview__error{color:#da1e28;font-size:.85em}.preview__placeholder{color:#8d8d8d;font-size:.85em;font-style:italic}.preview__warnings{padding:8px 12px;background:#fff8e1;border-top:1px solid #e0e0e0;font-size:.85em;color:#663c00}.preview__warning-icon{margin-right:4px}\n"], 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: "pipe", type: i1$1.JsonPipe, name: "json" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i4.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }] });
1515
+ `, isInline: true, styles: [".preview{border:1px solid #e0e0e0;border-radius:4px;margin-top:16px;overflow:hidden}.preview__header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:#f4f4f4;border-bottom:1px solid #e0e0e0}.preview__title{font-weight:600;font-size:.85em}.preview__controls{display:flex;gap:4px}.preview__doc-input{padding:4px 8px;border:1px solid #e0e0e0;border-radius:4px;font-size:.8em;width:220px;font-family:monospace}.preview__run-btn{padding:4px 10px;border:1px solid #0f62fe;border-radius:4px;background:#0f62fe;color:#fff;cursor:pointer;font-size:.8em}.preview__run-btn:disabled{opacity:.4;cursor:not-allowed}.preview__panels{display:grid;grid-template-columns:1fr 1fr;gap:1px;background:#e0e0e0}.preview__panel{background:#fff;padding:8px 12px;min-height:80px}.preview__panel-label{font-size:.75em;color:#6f6f6f;text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px}.preview__code{font-family:IBM Plex Mono,monospace;font-size:.8em;line-height:1.4;margin:0;white-space:pre-wrap;word-break:break-word}.preview__loading{color:#8d8d8d}.preview__error{color:#da1e28;font-size:.85em}.preview__placeholder{color:#8d8d8d;font-size:.85em;font-style:italic}.preview__warnings{padding:8px 12px;background:#fff8e1;border-top:1px solid #e0e0e0;font-size:.85em;color:#663c00}.preview__warning-icon{margin-right:4px}\n"], 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: "pipe", type: i1$1.JsonPipe, name: "json" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }] });
1383
1516
  }
1384
1517
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: MappingPreviewComponent, decorators: [{
1385
1518
  type: Component,
@@ -1450,6 +1583,60 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
1450
1583
  type: Input
1451
1584
  }] } });
1452
1585
 
1586
+ const FORM_REF_PREFIX$1 = 'form:';
1587
+ /**
1588
+ * Detect if a string value is a JSONata expression (vs a plain literal).
1589
+ * Checks for characters that indicate JSONata operators: $, &, (, {, ?, [
1590
+ */
1591
+ function isExpression(value) {
1592
+ return /[$&({?\[]/.test(value);
1593
+ }
1594
+ /**
1595
+ * Expand dot-notation keys into nested objects.
1596
+ * e.g. { "beslissing.tekst": "value" } -> { beslissing: { tekst: "value" } }
1597
+ */
1598
+ function expandDotNotation(flat) {
1599
+ const result = {};
1600
+ for (const [key, value] of Object.entries(flat)) {
1601
+ const parts = key.split('.');
1602
+ let current = result;
1603
+ for (let i = 0; i < parts.length - 1; i++) {
1604
+ if (!current[parts[i]] || typeof current[parts[i]] !== 'object') {
1605
+ current[parts[i]] = {};
1606
+ }
1607
+ current = current[parts[i]];
1608
+ }
1609
+ current[parts[parts.length - 1]] = value;
1610
+ }
1611
+ return result;
1612
+ }
1613
+ /**
1614
+ * Given an override mapping (scope -> { inputPath -> "form:<componentKey>" })
1615
+ * and form data, produce the inputOverrides object for the backend.
1616
+ * The "form:" prefix identifies form field references; the remainder is the Formio component key.
1617
+ */
1618
+ function computeInputOverrides(mapping, formData) {
1619
+ const result = {};
1620
+ for (const [scope, fields] of Object.entries(mapping)) {
1621
+ if (scope !== 'doc' && scope !== 'pv')
1622
+ continue;
1623
+ const flatOverrides = {};
1624
+ for (const [inputPath, ref] of Object.entries(fields)) {
1625
+ const formFieldKey = String(ref).startsWith(FORM_REF_PREFIX$1)
1626
+ ? String(ref).substring(FORM_REF_PREFIX$1.length)
1627
+ : String(ref);
1628
+ const value = formData[formFieldKey];
1629
+ if (value !== undefined) {
1630
+ flatOverrides[inputPath] = value;
1631
+ }
1632
+ }
1633
+ if (Object.keys(flatOverrides).length > 0) {
1634
+ result[scope] = expandDotNotation(flatOverrides);
1635
+ }
1636
+ }
1637
+ return result;
1638
+ }
1639
+
1453
1640
  class GenerateDocumentConfigurationComponent {
1454
1641
  epistolaPluginService;
1455
1642
  processLinkStateService;
@@ -1480,6 +1667,10 @@ class GenerateDocumentConfigurationComponent {
1480
1667
  selectedTemplateId$ = new BehaviorSubject('');
1481
1668
  selectedVariantId$ = new BehaviorSubject('');
1482
1669
  variantSelectionMode = 'explicit';
1670
+ variantIdExpressionMode = false;
1671
+ variantIdExpression = '';
1672
+ filenameExpressionMode = false;
1673
+ filenameExpression = '';
1483
1674
  variantAttributeEntries = [];
1484
1675
  availableAttributeKeys = [];
1485
1676
  caseDefinitionKey = null;
@@ -1488,6 +1679,7 @@ class GenerateDocumentConfigurationComponent {
1488
1679
  variableSuggestions = null;
1489
1680
  requiredFieldsStatus = { mapped: 0, total: 0 };
1490
1681
  prefillDataMapping = {};
1682
+ validationErrors$ = new BehaviorSubject([]);
1491
1683
  destroy$ = new Subject();
1492
1684
  saveSubscription;
1493
1685
  formValue$ = new BehaviorSubject(null);
@@ -1567,6 +1759,12 @@ class GenerateDocumentConfigurationComponent {
1567
1759
  onAttributeEntryChange() {
1568
1760
  this.revalidate();
1569
1761
  }
1762
+ onVariantIdExpressionChange() {
1763
+ this.revalidate();
1764
+ }
1765
+ onFilenameExpressionChange() {
1766
+ this.revalidate();
1767
+ }
1570
1768
  onKeySelected(entry, value) {
1571
1769
  if (value === '__custom__') {
1572
1770
  entry._customKey = true;
@@ -1717,6 +1915,7 @@ class GenerateDocumentConfigurationComponent {
1717
1915
  key: e.key,
1718
1916
  value: e.value,
1719
1917
  required: e.required !== false,
1918
+ _expressionMode: isExpression(e.value),
1720
1919
  }));
1721
1920
  }
1722
1921
  else {
@@ -1725,7 +1924,18 @@ class GenerateDocumentConfigurationComponent {
1725
1924
  }
1726
1925
  else if (config.variantId) {
1727
1926
  this.variantSelectionMode = 'explicit';
1728
- this.selectedVariantId$.next(config.variantId);
1927
+ if (isExpression(config.variantId)) {
1928
+ this.variantIdExpressionMode = true;
1929
+ this.variantIdExpression = config.variantId;
1930
+ }
1931
+ else {
1932
+ this.selectedVariantId$.next(config.variantId);
1933
+ }
1934
+ }
1935
+ // Detect expression mode for filename
1936
+ if (config.filename && isExpression(config.filename)) {
1937
+ this.filenameExpressionMode = true;
1938
+ this.filenameExpression = config.filename;
1729
1939
  }
1730
1940
  // Apply dataMapping prefill (JSONata expression string)
1731
1941
  if (config.dataMapping) {
@@ -1796,25 +2006,66 @@ class GenerateDocumentConfigurationComponent {
1796
2006
  environmentId: formValue.environmentId || undefined,
1797
2007
  dataMapping: dataMapping,
1798
2008
  outputFormat: formValue.outputFormat,
1799
- filename: formValue.filename,
2009
+ filename: this.filenameExpressionMode ? this.filenameExpression : formValue.filename,
1800
2010
  correlationId: formValue.correlationId || undefined,
1801
2011
  resultProcessVariable: formValue.resultProcessVariable,
1802
2012
  };
1803
2013
  if (this.variantSelectionMode === 'explicit') {
1804
- config.variantId = formValue.variantId;
2014
+ config.variantId = this.variantIdExpressionMode
2015
+ ? this.variantIdExpression
2016
+ : formValue.variantId;
1805
2017
  }
1806
2018
  else {
1807
2019
  config.variantAttributes = this.variantAttributeEntries
1808
2020
  .filter((e) => e.key && e.value)
1809
2021
  .map((e) => ({ key: e.key, value: e.value, required: e.required }));
1810
2022
  }
1811
- this.configuration.emit(config);
2023
+ this.validateAndEmit(config);
1812
2024
  }
1813
2025
  });
1814
2026
  });
1815
2027
  }
1816
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: GenerateDocumentConfigurationComponent, deps: [{ token: EpistolaPluginService }, { token: i2$2.ProcessLinkStateService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
1817
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: GenerateDocumentConfigurationComponent, isStandalone: true, selector: "epistola-generate-document-configuration", inputs: { save$: "save$", disabled$: "disabled$", pluginId: "pluginId", prefillConfiguration$: "prefillConfiguration$", selectedPluginConfigurationData$: "selectedPluginConfigurationData$", context$: "context$" }, outputs: { valid: "valid", configuration: "configuration" }, ngImport: i0, template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: disabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n catalogs: catalogs$ | async,\n templates: templates$ | async,\n variants: variants$ | async,\n environments: environments$ | async,\n templateFields: templateFields$ | async,\n selectedCatalogId: selectedCatalogId$ | async,\n selectedTemplateId: selectedTemplateId$ | async,\n } as obs\"\n>\n <v-select\n name=\"catalogId\"\n [title]=\"'catalogId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'catalogIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.catalogs.data\"\n [defaultSelectionId]=\"obs.prefill?.catalogId\"\n [disabled]=\"obs.disabled || obs.catalogs.loading\"\n [required]=\"true\"\n [loading]=\"obs.catalogs.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.catalogs.error\" class=\"loading-error\">{{ obs.catalogs.error }}</div>\n\n <v-select\n name=\"templateId\"\n [title]=\"'templateId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'templateIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.templates.data\"\n [defaultSelectionId]=\"obs.prefill?.templateId\"\n [disabled]=\"obs.disabled || obs.templates.loading || !obs.selectedCatalogId\"\n [required]=\"true\"\n [loading]=\"obs.templates.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.templates.error\" class=\"loading-error\">{{ obs.templates.error }}</div>\n\n <!-- Variant selection mode toggle -->\n <div class=\"variant-mode-toggle\" *ngIf=\"obs.selectedTemplateId\">\n <label class=\"variant-mode-label\">{{\n 'variantSelectionMode' | pluginTranslate: pluginId | async\n }}</label>\n <div class=\"variant-mode-buttons\">\n <button\n type=\"button\"\n class=\"variant-mode-btn\"\n [class.active]=\"variantSelectionMode === 'explicit'\"\n (click)=\"onVariantSelectionModeChange('explicit')\"\n [disabled]=\"obs.disabled\"\n >\n {{ 'selectByVariant' | pluginTranslate: pluginId | async }}\n </button>\n <button\n type=\"button\"\n class=\"variant-mode-btn\"\n [class.active]=\"variantSelectionMode === 'attributes'\"\n (click)=\"onVariantSelectionModeChange('attributes')\"\n [disabled]=\"obs.disabled\"\n >\n {{ 'selectByAttributes' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n </div>\n\n <!-- Explicit variant selection (dropdown) -->\n <v-select\n *ngIf=\"variantSelectionMode === 'explicit'\"\n name=\"variantId\"\n [title]=\"'variantId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'variantIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.variants.data\"\n [defaultSelectionId]=\"obs.prefill?.variantId\"\n [disabled]=\"obs.disabled || obs.variants.loading || !obs.selectedTemplateId\"\n [required]=\"false\"\n [loading]=\"obs.variants.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.variants.error\" class=\"loading-error\">{{ obs.variants.error }}</div>\n\n <!-- Attribute-based variant selection -->\n <div\n *ngIf=\"variantSelectionMode === 'attributes' && obs.selectedTemplateId\"\n class=\"variant-attributes-section\"\n >\n <label class=\"variant-attributes-label\">{{\n 'variantAttributes' | pluginTranslate: pluginId | async\n }}</label>\n <div class=\"variant-attributes-list\">\n <div\n *ngFor=\"let entry of variantAttributeEntries; let i = index\"\n class=\"variant-attribute-row\"\n >\n <select\n *ngIf=\"!entry._customKey\"\n class=\"variant-attribute-input\"\n [ngModel]=\"entry.key\"\n (ngModelChange)=\"onKeySelected(entry, $event)\"\n [disabled]=\"obs.disabled\"\n >\n <option value=\"\" disabled>\n {{ 'attributeKey' | pluginTranslate: pluginId | async }}\n </option>\n <option *ngFor=\"let key of availableAttributeKeys\" [value]=\"key\">{{ key }}</option>\n <option value=\"__custom__\">\n {{ 'attributeKeyCustom' | pluginTranslate: pluginId | async }}\n </option>\n </select>\n <div *ngIf=\"entry._customKey\" class=\"custom-key-input\">\n <input\n type=\"text\"\n class=\"variant-attribute-input\"\n [placeholder]=\"'attributeKey' | pluginTranslate: pluginId | async\"\n [(ngModel)]=\"entry.key\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <button\n type=\"button\"\n class=\"custom-key-cancel\"\n (click)=\"cancelCustomKey(entry)\"\n [disabled]=\"obs.disabled\"\n >\n &times;\n </button>\n </div>\n <input\n type=\"text\"\n class=\"variant-attribute-input\"\n [placeholder]=\"'attributeValue' | pluginTranslate: pluginId | async\"\n [(ngModel)]=\"entry.value\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <label class=\"variant-attribute-required-toggle\">\n <input\n type=\"checkbox\"\n [(ngModel)]=\"entry.required\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <span class=\"required-label\">{{\n (entry.required ? 'attributeRequired' : 'attributePreferred')\n | pluginTranslate: pluginId\n | async\n }}</span>\n </label>\n <button\n type=\"button\"\n class=\"variant-attribute-remove-btn\"\n (click)=\"removeAttributeEntry(i)\"\n [disabled]=\"obs.disabled\"\n title=\"{{ 'removeAttribute' | pluginTranslate: pluginId | async }}\"\n >\n &times;\n </button>\n </div>\n </div>\n <button\n type=\"button\"\n class=\"variant-attribute-add-btn\"\n (click)=\"addAttributeEntry()\"\n [disabled]=\"obs.disabled\"\n >\n + {{ 'addAttribute' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <v-select\n name=\"environmentId\"\n [title]=\"'environmentId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'environmentIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.environments.data\"\n [defaultSelectionId]=\"obs.prefill?.environmentId\"\n [disabled]=\"obs.disabled || obs.environments.loading\"\n [required]=\"false\"\n [loading]=\"obs.environments.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.environments.error\" class=\"loading-error\">{{ obs.environments.error }}</div>\n\n <v-select\n name=\"outputFormat\"\n [title]=\"'outputFormat' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'outputFormatTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"outputFormatOptions\"\n [defaultSelectionId]=\"obs.prefill?.outputFormat || 'PDF'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-select>\n\n <v-input\n name=\"filename\"\n [title]=\"'filename' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'filenameTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.filename\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"correlationId\"\n [title]=\"'correlationId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'correlationIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.correlationId\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"resultProcessVariable\"\n [title]=\"'resultProcessVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'resultProcessVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.resultProcessVariable\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n\n<div *ngIf=\"(templateFields$ | async)?.error as templateFieldsError\" class=\"loading-error\">\n {{ templateFieldsError }}\n</div>\n\n<div *ngIf=\"selectedTemplateId$ | async\" class=\"mapping-section\">\n <h5 class=\"mapping-section__title\">\n {{ 'dataMappingTitle' | pluginTranslate: pluginId | async }}\n </h5>\n <p class=\"mapping-section__description\">\n {{ 'dataMappingDescription' | pluginTranslate: pluginId | async }}\n </p>\n <div class=\"mapping-mode-toggle\">\n <button\n class=\"mapping-mode-toggle__btn\"\n [class.mapping-mode-toggle__btn--active]=\"mappingMode === 'simple'\"\n (click)=\"mappingMode = 'simple'\"\n >\n {{ 'mappingModeSimple' | pluginTranslate: pluginId | async }}\n </button>\n <button\n class=\"mapping-mode-toggle__btn\"\n [class.mapping-mode-toggle__btn--active]=\"mappingMode === 'advanced'\"\n (click)=\"mappingMode = 'advanced'\"\n >\n {{ 'mappingModeAdvanced' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <!-- Editor area (full width) -->\n <epistola-mapping-builder\n *ngIf=\"mappingMode === 'simple'\"\n [expression]=\"dataMapping$ | async\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n [suggestions]=\"variableSuggestions\"\n [disabled]=\"!!(disabled$ | async)\"\n (expressionChange)=\"onDataMappingChange($event)\"\n ></epistola-mapping-builder>\n\n <epistola-jsonata-editor\n *ngIf=\"mappingMode === 'advanced'\"\n [expression]=\"dataMapping$ | async\"\n [disabled]=\"!!(disabled$ | async)\"\n [suggestions]=\"variableSuggestions\"\n [functions]=\"expressionFunctions\"\n (expressionChange)=\"onDataMappingChange($event)\"\n ></epistola-jsonata-editor>\n\n <!-- Bottom tabs: Schema + Preview (collapsible) -->\n <div class=\"mapping-tools\" [class.mapping-tools--collapsed]=\"toolsCollapsed\">\n <div class=\"mapping-tools__header\" (click)=\"toolsCollapsed = !toolsCollapsed\">\n <span class=\"mapping-tools__chevron\">{{ toolsCollapsed ? '&#x25B6;' : '&#x25BC;' }}</span>\n <span>{{ 'mappingTools' | pluginTranslate: pluginId | async }}</span>\n </div>\n <div *ngIf=\"!toolsCollapsed\" class=\"mapping-tools__content\">\n <div class=\"mapping-tools__tabs\">\n <button\n class=\"mapping-tools__tab\"\n [class.mapping-tools__tab--active]=\"activeToolTab === 'schema'\"\n (click)=\"activeToolTab = 'schema'\"\n >\n {{ 'expectedStructure' | pluginTranslate: pluginId | async }}\n </button>\n <button\n class=\"mapping-tools__tab\"\n [class.mapping-tools__tab--active]=\"activeToolTab === 'preview'\"\n (click)=\"activeToolTab = 'preview'\"\n >\n {{ 'previewTitle' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <epistola-expected-structure\n *ngIf=\"activeToolTab === 'schema'\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n ></epistola-expected-structure>\n\n <epistola-mapping-preview\n *ngIf=\"activeToolTab === 'preview'\"\n [expression]=\"dataMapping$ | async\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n ></epistola-mapping-preview>\n </div>\n </div>\n</div>\n", styles: [".loading-error{padding:.25rem .75rem;font-size:.8125rem;color:#dc3545}.validation-summary{margin-top:.5rem;padding:.5rem .75rem;border-radius:4px;font-size:.875rem}.validation-summary .validation-complete{color:#198754}.validation-summary .validation-incomplete{color:#dc3545;font-weight:500}.variant-mode-toggle{margin-bottom:1rem;padding:0 .75rem}.variant-mode-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-mode-buttons{display:flex;gap:0;border:1px solid #d1d5db;border-radius:4px;overflow:hidden;width:fit-content}.variant-mode-btn{padding:.375rem .75rem;font-size:.8125rem;background:#fff;border:none;border-right:1px solid #d1d5db;cursor:pointer;color:#374151;transition:background-color .15s,color .15s}.variant-mode-btn:last-child{border-right:none}.variant-mode-btn:hover:not([disabled]){background:#f3f4f6}.variant-mode-btn.active{background:#2563eb;color:#fff}.variant-mode-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attributes-section{margin-bottom:1rem;padding:0 .75rem}.variant-attributes-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-attributes-list{display:flex;flex-direction:column;gap:.375rem}.variant-attribute-row{display:flex;gap:.375rem;align-items:center}.variant-attribute-input{flex:1;padding:.375rem .5rem;font-size:.8125rem;border:1px solid #d1d5db;border-radius:4px;outline:none}.variant-attribute-input:focus{border-color:#2563eb;box-shadow:0 0 0 1px #2563eb}.variant-attribute-input[disabled]{opacity:.5;background:#f9fafb}.custom-key-input{display:flex;flex:1;gap:.25rem}.custom-key-input .variant-attribute-input{flex:1}.custom-key-cancel{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.custom-key-cancel:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.custom-key-cancel[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-required-toggle{display:flex;align-items:center;gap:.25rem;font-size:.75rem;color:#374151;white-space:nowrap;cursor:pointer}.variant-attribute-required-toggle input[type=checkbox]{margin:0;cursor:pointer}.variant-attribute-required-toggle .required-label{-webkit-user-select:none;user-select:none}.variant-attribute-remove-btn{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-remove-btn:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.variant-attribute-remove-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-add-btn{margin-top:.375rem;padding:.25rem .5rem;font-size:.8125rem;background:none;border:1px dashed #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-add-btn:hover:not([disabled]){color:#2563eb;border-color:#2563eb}.variant-attribute-add-btn[disabled]{opacity:.5;cursor:not-allowed}.mapping-section{margin-top:1rem}.mapping-section__title{font-size:1rem;font-weight:600;margin:0 0 4px}.mapping-section__description{font-size:.85em;color:#6f6f6f;margin:0 0 12px}.mapping-mode-toggle{display:flex;gap:0;margin-bottom:12px}.mapping-mode-toggle__btn{padding:6px 16px;border:1px solid #e0e0e0;background:#fff;font-size:.85em;cursor:pointer}.mapping-mode-toggle__btn:first-child{border-radius:4px 0 0 4px}.mapping-mode-toggle__btn:last-child{border-radius:0 4px 4px 0;border-left:none}.mapping-mode-toggle__btn--active{background:#0f62fe;color:#fff;border-color:#0f62fe}.mapping-tools{margin-top:12px;border:1px solid #e0e0e0;border-radius:4px;overflow:hidden}.mapping-tools__header{display:flex;align-items:center;gap:6px;padding:8px 12px;background:#f4f4f4;cursor:pointer;font-size:.85em;font-weight:500;-webkit-user-select:none;user-select:none}.mapping-tools__header:hover{background:#e8e8e8}.mapping-tools__chevron{font-size:.7em}.mapping-tools__content{border-top:1px solid #e0e0e0}.mapping-tools__tabs{display:flex;border-bottom:1px solid #e0e0e0}.mapping-tools__tab{padding:6px 16px;border:none;background:transparent;font-size:.8em;cursor:pointer;border-bottom:2px solid transparent}.mapping-tools__tab--active{border-bottom-color:#0f62fe;font-weight:500}\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: FormsModule }, { kind: "directive", type: i4.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i4.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i4.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i4.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i4.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { 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"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i3.SelectComponent, selector: "v-select", inputs: ["items", "defaultSelection", "defaultSelectionId", "defaultSelectionIds", "disabled", "dropUp", "invalid", "multiple", "margin", "widthInPx", "notFoundText", "clearAllText", "clearText", "clearable", "name", "title", "titleTranslationKey", "clearSelectionSubject$", "tooltip", "required", "loading", "loadingText", "placeholder", "smallMargin", "carbonTheme", "appendInline", "warn", "warnText", "dataTestId"], outputs: ["selectedChange"] }, { kind: "component", type: ExpectedStructureComponent, selector: "epistola-expected-structure", inputs: ["templateFields"] }, { kind: "component", type: JsonataEditorComponent, selector: "epistola-jsonata-editor", inputs: ["expression", "disabled", "suggestions", "functions"], outputs: ["expressionChange", "validChange"] }, { kind: "component", type: MappingBuilderComponent, selector: "epistola-mapping-builder", inputs: ["expression", "templateFields", "suggestions", "disabled"], outputs: ["expressionChange"] }, { kind: "component", type: MappingPreviewComponent, selector: "epistola-mapping-preview", inputs: ["expression", "templateFields", "caseDefinitionKey"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2028
+ /**
2029
+ * Build a JSONata validation request from the config and call the backend.
2030
+ * Only fields that are JSONata expressions get validated:
2031
+ * - dataMapping is always JSONata
2032
+ * - filename / variantId only when their `fx` toggle is on
2033
+ * - variant attribute values only when isExpression() reports true
2034
+ * On invalid response, surface errors and abort the emit.
2035
+ * If the validator endpoint itself fails (network/server), proceed with the
2036
+ * emit — the validation is a quality-of-life check, not a hard gate.
2037
+ */
2038
+ validateAndEmit(config) {
2039
+ const variantAttributeValues = {};
2040
+ if (config.variantAttributes) {
2041
+ for (const attr of config.variantAttributes) {
2042
+ if (isExpression(attr.value)) {
2043
+ variantAttributeValues[attr.key] = attr.value;
2044
+ }
2045
+ }
2046
+ }
2047
+ const request = {
2048
+ dataMapping: config.dataMapping || null,
2049
+ filename: this.filenameExpressionMode ? config.filename : null,
2050
+ variantId: this.variantIdExpressionMode ? config.variantId || null : null,
2051
+ variantAttributeValues: Object.keys(variantAttributeValues).length > 0 ? variantAttributeValues : null,
2052
+ };
2053
+ this.epistolaPluginService
2054
+ .validateJsonata(request)
2055
+ .pipe(take$1(1), catchError(() => of({ valid: true, errors: [] })))
2056
+ .subscribe((result) => {
2057
+ if (result.valid) {
2058
+ this.validationErrors$.next([]);
2059
+ this.configuration.emit(config);
2060
+ }
2061
+ else {
2062
+ this.validationErrors$.next(result.errors);
2063
+ this.cdr.markForCheck();
2064
+ }
2065
+ });
2066
+ }
2067
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: GenerateDocumentConfigurationComponent, deps: [{ token: EpistolaPluginService }, { token: i2$3.ProcessLinkStateService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
2068
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: GenerateDocumentConfigurationComponent, isStandalone: true, selector: "epistola-generate-document-configuration", inputs: { save$: "save$", disabled$: "disabled$", pluginId: "pluginId", prefillConfiguration$: "prefillConfiguration$", selectedPluginConfigurationData$: "selectedPluginConfigurationData$", context$: "context$" }, outputs: { valid: "valid", configuration: "configuration" }, ngImport: i0, template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: disabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n catalogs: catalogs$ | async,\n templates: templates$ | async,\n variants: variants$ | async,\n environments: environments$ | async,\n templateFields: templateFields$ | async,\n selectedCatalogId: selectedCatalogId$ | async,\n selectedTemplateId: selectedTemplateId$ | async,\n validationErrors: validationErrors$ | async,\n } as obs\"\n>\n <div\n *ngIf=\"obs.validationErrors && obs.validationErrors.length > 0\"\n class=\"jsonata-validation-errors\"\n >\n <strong>{{ 'jsonataValidationErrorsHeading' | pluginTranslate: pluginId | async }}</strong>\n <ul>\n <li *ngFor=\"let err of obs.validationErrors\">\n <code>{{ err.field }}</code\n >: {{ err.message }}\n </li>\n </ul>\n </div>\n <v-select\n name=\"catalogId\"\n [title]=\"'catalogId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'catalogIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.catalogs.data\"\n [defaultSelectionId]=\"obs.prefill?.catalogId\"\n [disabled]=\"obs.disabled || obs.catalogs.loading\"\n [required]=\"true\"\n [loading]=\"obs.catalogs.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.catalogs.error\" class=\"loading-error\">{{ obs.catalogs.error }}</div>\n\n <v-select\n name=\"templateId\"\n [title]=\"'templateId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'templateIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.templates.data\"\n [defaultSelectionId]=\"obs.prefill?.templateId\"\n [disabled]=\"obs.disabled || obs.templates.loading || !obs.selectedCatalogId\"\n [required]=\"true\"\n [loading]=\"obs.templates.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.templates.error\" class=\"loading-error\">{{ obs.templates.error }}</div>\n\n <!-- Variant selection mode toggle -->\n <div class=\"variant-mode-toggle\" *ngIf=\"obs.selectedTemplateId\">\n <label class=\"variant-mode-label\">{{\n 'variantSelectionMode' | pluginTranslate: pluginId | async\n }}</label>\n <div class=\"variant-mode-buttons\">\n <button\n type=\"button\"\n class=\"variant-mode-btn\"\n [class.active]=\"variantSelectionMode === 'explicit'\"\n (click)=\"onVariantSelectionModeChange('explicit')\"\n [disabled]=\"obs.disabled\"\n >\n {{ 'selectByVariant' | pluginTranslate: pluginId | async }}\n </button>\n <button\n type=\"button\"\n class=\"variant-mode-btn\"\n [class.active]=\"variantSelectionMode === 'attributes'\"\n (click)=\"onVariantSelectionModeChange('attributes')\"\n [disabled]=\"obs.disabled\"\n >\n {{ 'selectByAttributes' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n </div>\n\n <!-- Explicit variant selection (dropdown or expression) -->\n <div *ngIf=\"variantSelectionMode === 'explicit'\" class=\"field-with-fx\">\n <v-select\n *ngIf=\"!variantIdExpressionMode\"\n name=\"variantId\"\n [title]=\"'variantId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'variantIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.variants.data\"\n [defaultSelectionId]=\"obs.prefill?.variantId\"\n [disabled]=\"obs.disabled || obs.variants.loading || !obs.selectedTemplateId\"\n [required]=\"false\"\n [loading]=\"obs.variants.loading\"\n >\n </v-select>\n <div *ngIf=\"variantIdExpressionMode\" class=\"fx-input-group\">\n <label class=\"fx-input-label\">{{ 'variantId' | pluginTranslate: pluginId | async }}</label>\n <input\n type=\"text\"\n class=\"fx-input\"\n [ngModel]=\"variantIdExpression\"\n (ngModelChange)=\"variantIdExpression = $event; onVariantIdExpressionChange()\"\n [disabled]=\"obs.disabled\"\n placeholder=\"$pv.letterType\"\n />\n </div>\n <button\n type=\"button\"\n class=\"fx-toggle\"\n (click)=\"variantIdExpressionMode = !variantIdExpressionMode\"\n [disabled]=\"obs.disabled\"\n [title]=\"variantIdExpressionMode ? 'Switch to dropdown' : 'Switch to expression'\"\n >\n {{ variantIdExpressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n <div *ngIf=\"obs.variants.error\" class=\"loading-error\">{{ obs.variants.error }}</div>\n\n <!-- Attribute-based variant selection -->\n <div\n *ngIf=\"variantSelectionMode === 'attributes' && obs.selectedTemplateId\"\n class=\"variant-attributes-section\"\n >\n <label class=\"variant-attributes-label\">{{\n 'variantAttributes' | pluginTranslate: pluginId | async\n }}</label>\n <div class=\"variant-attributes-list\">\n <div\n *ngFor=\"let entry of variantAttributeEntries; let i = index\"\n class=\"variant-attribute-row\"\n >\n <select\n *ngIf=\"!entry._customKey\"\n class=\"variant-attribute-input\"\n [ngModel]=\"entry.key\"\n (ngModelChange)=\"onKeySelected(entry, $event)\"\n [disabled]=\"obs.disabled\"\n >\n <option value=\"\" disabled>\n {{ 'attributeKey' | pluginTranslate: pluginId | async }}\n </option>\n <option *ngFor=\"let key of availableAttributeKeys\" [value]=\"key\">{{ key }}</option>\n <option value=\"__custom__\">\n {{ 'attributeKeyCustom' | pluginTranslate: pluginId | async }}\n </option>\n </select>\n <div *ngIf=\"entry._customKey\" class=\"custom-key-input\">\n <input\n type=\"text\"\n class=\"variant-attribute-input\"\n [placeholder]=\"'attributeKey' | pluginTranslate: pluginId | async\"\n [(ngModel)]=\"entry.key\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <button\n type=\"button\"\n class=\"custom-key-cancel\"\n (click)=\"cancelCustomKey(entry)\"\n [disabled]=\"obs.disabled\"\n >\n &times;\n </button>\n </div>\n <div class=\"attribute-value-with-fx\">\n <input\n type=\"text\"\n class=\"variant-attribute-input\"\n [class.fx-input]=\"entry._expressionMode\"\n [placeholder]=\"\n entry._expressionMode\n ? '$pv.language'\n : ('attributeValue' | pluginTranslate: pluginId | async)\n \"\n [(ngModel)]=\"entry.value\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <button\n type=\"button\"\n class=\"fx-toggle fx-toggle--inline\"\n (click)=\"entry._expressionMode = !entry._expressionMode\"\n [disabled]=\"obs.disabled\"\n [title]=\"entry._expressionMode ? 'Switch to plain value' : 'Switch to expression'\"\n >\n {{ entry._expressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n <label class=\"variant-attribute-required-toggle\">\n <input\n type=\"checkbox\"\n [(ngModel)]=\"entry.required\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <span class=\"required-label\">{{\n (entry.required ? 'attributeRequired' : 'attributePreferred')\n | pluginTranslate: pluginId\n | async\n }}</span>\n </label>\n <button\n type=\"button\"\n class=\"variant-attribute-remove-btn\"\n (click)=\"removeAttributeEntry(i)\"\n [disabled]=\"obs.disabled\"\n title=\"{{ 'removeAttribute' | pluginTranslate: pluginId | async }}\"\n >\n &times;\n </button>\n </div>\n </div>\n <button\n type=\"button\"\n class=\"variant-attribute-add-btn\"\n (click)=\"addAttributeEntry()\"\n [disabled]=\"obs.disabled\"\n >\n + {{ 'addAttribute' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <v-select\n name=\"environmentId\"\n [title]=\"'environmentId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'environmentIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.environments.data\"\n [defaultSelectionId]=\"obs.prefill?.environmentId\"\n [disabled]=\"obs.disabled || obs.environments.loading\"\n [required]=\"false\"\n [loading]=\"obs.environments.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.environments.error\" class=\"loading-error\">{{ obs.environments.error }}</div>\n\n <v-select\n name=\"outputFormat\"\n [title]=\"'outputFormat' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'outputFormatTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"outputFormatOptions\"\n [defaultSelectionId]=\"obs.prefill?.outputFormat || 'PDF'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-select>\n\n <div class=\"field-with-fx\">\n <v-input\n *ngIf=\"!filenameExpressionMode\"\n name=\"filename\"\n [title]=\"'filename' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'filenameTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.filename\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n <div *ngIf=\"filenameExpressionMode\" class=\"fx-input-group\">\n <label class=\"fx-input-label\">{{ 'filename' | pluginTranslate: pluginId | async }}</label>\n <input\n type=\"text\"\n class=\"fx-input\"\n [ngModel]=\"filenameExpression\"\n (ngModelChange)=\"filenameExpression = $event; onFilenameExpressionChange()\"\n [disabled]=\"obs.disabled\"\n placeholder='\"besluit-\" & $doc.name & \".pdf\"'\n />\n </div>\n <button\n type=\"button\"\n class=\"fx-toggle\"\n (click)=\"filenameExpressionMode = !filenameExpressionMode\"\n [disabled]=\"obs.disabled\"\n [title]=\"filenameExpressionMode ? 'Switch to plain input' : 'Switch to expression'\"\n >\n {{ filenameExpressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n\n <v-input\n name=\"correlationId\"\n [title]=\"'correlationId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'correlationIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.correlationId\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"resultProcessVariable\"\n [title]=\"'resultProcessVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'resultProcessVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.resultProcessVariable\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n\n<div *ngIf=\"(templateFields$ | async)?.error as templateFieldsError\" class=\"loading-error\">\n {{ templateFieldsError }}\n</div>\n\n<div *ngIf=\"selectedTemplateId$ | async\" class=\"mapping-section\">\n <h5 class=\"mapping-section__title\">\n {{ 'dataMappingTitle' | pluginTranslate: pluginId | async }}\n </h5>\n <p class=\"mapping-section__description\">\n {{ 'dataMappingDescription' | pluginTranslate: pluginId | async }}\n </p>\n <div class=\"mapping-mode-toggle\">\n <button\n class=\"mapping-mode-toggle__btn\"\n [class.mapping-mode-toggle__btn--active]=\"mappingMode === 'simple'\"\n (click)=\"mappingMode = 'simple'\"\n >\n {{ 'mappingModeSimple' | pluginTranslate: pluginId | async }}\n </button>\n <button\n class=\"mapping-mode-toggle__btn\"\n [class.mapping-mode-toggle__btn--active]=\"mappingMode === 'advanced'\"\n (click)=\"mappingMode = 'advanced'\"\n >\n {{ 'mappingModeAdvanced' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <!-- Editor area (full width) -->\n <epistola-mapping-builder\n *ngIf=\"mappingMode === 'simple'\"\n [expression]=\"dataMapping$ | async\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n [suggestions]=\"variableSuggestions\"\n [disabled]=\"!!(disabled$ | async)\"\n (expressionChange)=\"onDataMappingChange($event)\"\n ></epistola-mapping-builder>\n\n <epistola-jsonata-editor\n *ngIf=\"mappingMode === 'advanced'\"\n [expression]=\"dataMapping$ | async\"\n [disabled]=\"!!(disabled$ | async)\"\n [suggestions]=\"variableSuggestions\"\n [functions]=\"expressionFunctions\"\n (expressionChange)=\"onDataMappingChange($event)\"\n ></epistola-jsonata-editor>\n\n <!-- Bottom tabs: Schema + Preview (collapsible) -->\n <div class=\"mapping-tools\" [class.mapping-tools--collapsed]=\"toolsCollapsed\">\n <div class=\"mapping-tools__header\" (click)=\"toolsCollapsed = !toolsCollapsed\">\n <span class=\"mapping-tools__chevron\">{{ toolsCollapsed ? '&#x25B6;' : '&#x25BC;' }}</span>\n <span>{{ 'mappingTools' | pluginTranslate: pluginId | async }}</span>\n </div>\n <div *ngIf=\"!toolsCollapsed\" class=\"mapping-tools__content\">\n <div class=\"mapping-tools__tabs\">\n <button\n class=\"mapping-tools__tab\"\n [class.mapping-tools__tab--active]=\"activeToolTab === 'schema'\"\n (click)=\"activeToolTab = 'schema'\"\n >\n {{ 'expectedStructure' | pluginTranslate: pluginId | async }}\n </button>\n <button\n class=\"mapping-tools__tab\"\n [class.mapping-tools__tab--active]=\"activeToolTab === 'preview'\"\n (click)=\"activeToolTab = 'preview'\"\n >\n {{ 'previewTitle' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <epistola-expected-structure\n *ngIf=\"activeToolTab === 'schema'\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n ></epistola-expected-structure>\n\n <epistola-mapping-preview\n *ngIf=\"activeToolTab === 'preview'\"\n [expression]=\"dataMapping$ | async\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n ></epistola-mapping-preview>\n </div>\n </div>\n</div>\n", styles: [".loading-error{padding:.25rem .75rem;font-size:.8125rem;color:#dc3545}.jsonata-validation-errors{margin-bottom:1rem;padding:.75rem 1rem;border:1px solid #dc3545;border-radius:4px;background:#fdf3f4;color:#dc3545;font-size:.875rem}.jsonata-validation-errors ul{margin:.5rem 0 0;padding-left:1.25rem}.jsonata-validation-errors code{background:#dc35451a;padding:0 .25rem;border-radius:2px;font-family:monospace}.validation-summary{margin-top:.5rem;padding:.5rem .75rem;border-radius:4px;font-size:.875rem}.validation-summary .validation-complete{color:#198754}.validation-summary .validation-incomplete{color:#dc3545;font-weight:500}.variant-mode-toggle{margin-bottom:1rem;padding:0 .75rem}.variant-mode-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-mode-buttons{display:flex;gap:0;border:1px solid #d1d5db;border-radius:4px;overflow:hidden;width:fit-content}.variant-mode-btn{padding:.375rem .75rem;font-size:.8125rem;background:#fff;border:none;border-right:1px solid #d1d5db;cursor:pointer;color:#374151;transition:background-color .15s,color .15s}.variant-mode-btn:last-child{border-right:none}.variant-mode-btn:hover:not([disabled]){background:#f3f4f6}.variant-mode-btn.active{background:#2563eb;color:#fff}.variant-mode-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attributes-section{margin-bottom:1rem;padding:0 .75rem}.variant-attributes-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-attributes-list{display:flex;flex-direction:column;gap:.375rem}.variant-attribute-row{display:flex;gap:.375rem;align-items:center}.variant-attribute-input{flex:1;padding:.375rem .5rem;font-size:.8125rem;border:1px solid #d1d5db;border-radius:4px;outline:none}.variant-attribute-input:focus{border-color:#2563eb;box-shadow:0 0 0 1px #2563eb}.variant-attribute-input[disabled]{opacity:.5;background:#f9fafb}.custom-key-input{display:flex;flex:1;gap:.25rem}.custom-key-input .variant-attribute-input{flex:1}.custom-key-cancel{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.custom-key-cancel:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.custom-key-cancel[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-required-toggle{display:flex;align-items:center;gap:.25rem;font-size:.75rem;color:#374151;white-space:nowrap;cursor:pointer}.variant-attribute-required-toggle input[type=checkbox]{margin:0;cursor:pointer}.variant-attribute-required-toggle .required-label{-webkit-user-select:none;user-select:none}.variant-attribute-remove-btn{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-remove-btn:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.variant-attribute-remove-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-add-btn{margin-top:.375rem;padding:.25rem .5rem;font-size:.8125rem;background:none;border:1px dashed #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-add-btn:hover:not([disabled]){color:#2563eb;border-color:#2563eb}.variant-attribute-add-btn[disabled]{opacity:.5;cursor:not-allowed}.field-with-fx{display:flex;align-items:flex-start;gap:4px}.field-with-fx>*:first-child{flex:1;min-width:0}.fx-toggle{width:28px;height:28px;margin-top:22px;border:1px solid #e0e0e0;border-radius:4px;background:#fff;cursor:pointer;font-family:monospace;font-size:.8em;display:flex;align-items:center;justify-content:center;flex-shrink:0}.fx-toggle:hover{background:#f4f4f4}.fx-toggle--inline{margin-top:0}.fx-input-group{flex:1;min-width:0;margin-bottom:.75rem}.fx-input-label{display:block;font-size:.875rem;margin-bottom:.25rem;color:#525252}.fx-input{width:100%;border:1px solid #8d8d8d;border-radius:0;padding:.4rem .75rem;font-family:monospace;font-size:.875rem;background:#f4f4f4}.attribute-value-with-fx{display:flex;align-items:center;gap:4px;flex:1;min-width:0}.attribute-value-with-fx>input{flex:1;min-width:0}.mapping-section{margin-top:1rem}.mapping-section__title{font-size:1rem;font-weight:600;margin:0 0 4px}.mapping-section__description{font-size:.85em;color:#6f6f6f;margin:0 0 12px}.mapping-mode-toggle{display:flex;gap:0;margin-bottom:12px}.mapping-mode-toggle__btn{padding:6px 16px;border:1px solid #e0e0e0;background:#fff;font-size:.85em;cursor:pointer}.mapping-mode-toggle__btn:first-child{border-radius:4px 0 0 4px}.mapping-mode-toggle__btn:last-child{border-radius:0 4px 4px 0;border-left:none}.mapping-mode-toggle__btn--active{background:#0f62fe;color:#fff;border-color:#0f62fe}.mapping-tools{margin-top:12px;border:1px solid #e0e0e0;border-radius:4px;overflow:hidden}.mapping-tools__header{display:flex;align-items:center;gap:6px;padding:8px 12px;background:#f4f4f4;cursor:pointer;font-size:.85em;font-weight:500;-webkit-user-select:none;user-select:none}.mapping-tools__header:hover{background:#e8e8e8}.mapping-tools__chevron{font-size:.7em}.mapping-tools__content{border-top:1px solid #e0e0e0}.mapping-tools__tabs{display:flex;border-bottom:1px solid #e0e0e0}.mapping-tools__tab{padding:6px 16px;border:none;background:transparent;font-size:.8em;cursor:pointer;border-bottom:2px solid transparent}.mapping-tools__tab--active{border-bottom-color:#0f62fe;font-weight:500}\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: FormsModule }, { kind: "directive", type: i2$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i2$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { 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"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i3.SelectComponent, selector: "v-select", inputs: ["items", "defaultSelection", "defaultSelectionId", "defaultSelectionIds", "disabled", "dropUp", "invalid", "multiple", "margin", "widthInPx", "notFoundText", "clearAllText", "clearText", "clearable", "name", "title", "titleTranslationKey", "clearSelectionSubject$", "tooltip", "required", "loading", "loadingText", "placeholder", "smallMargin", "carbonTheme", "appendInline", "warn", "warnText", "dataTestId"], outputs: ["selectedChange"] }, { kind: "component", type: ExpectedStructureComponent, selector: "epistola-expected-structure", inputs: ["templateFields"] }, { kind: "component", type: JsonataEditorComponent, selector: "epistola-jsonata-editor", inputs: ["expression", "disabled", "suggestions", "functions"], outputs: ["expressionChange", "validChange"] }, { kind: "component", type: MappingBuilderComponent, selector: "epistola-mapping-builder", inputs: ["expression", "templateFields", "suggestions", "disabled"], outputs: ["expressionChange"] }, { kind: "component", type: MappingPreviewComponent, selector: "epistola-mapping-preview", inputs: ["expression", "templateFields", "caseDefinitionKey"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1818
2069
  }
1819
2070
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: GenerateDocumentConfigurationComponent, decorators: [{
1820
2071
  type: Component,
@@ -1829,8 +2080,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
1829
2080
  JsonataEditorComponent,
1830
2081
  MappingBuilderComponent,
1831
2082
  MappingPreviewComponent,
1832
- ], template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: disabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n catalogs: catalogs$ | async,\n templates: templates$ | async,\n variants: variants$ | async,\n environments: environments$ | async,\n templateFields: templateFields$ | async,\n selectedCatalogId: selectedCatalogId$ | async,\n selectedTemplateId: selectedTemplateId$ | async,\n } as obs\"\n>\n <v-select\n name=\"catalogId\"\n [title]=\"'catalogId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'catalogIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.catalogs.data\"\n [defaultSelectionId]=\"obs.prefill?.catalogId\"\n [disabled]=\"obs.disabled || obs.catalogs.loading\"\n [required]=\"true\"\n [loading]=\"obs.catalogs.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.catalogs.error\" class=\"loading-error\">{{ obs.catalogs.error }}</div>\n\n <v-select\n name=\"templateId\"\n [title]=\"'templateId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'templateIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.templates.data\"\n [defaultSelectionId]=\"obs.prefill?.templateId\"\n [disabled]=\"obs.disabled || obs.templates.loading || !obs.selectedCatalogId\"\n [required]=\"true\"\n [loading]=\"obs.templates.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.templates.error\" class=\"loading-error\">{{ obs.templates.error }}</div>\n\n <!-- Variant selection mode toggle -->\n <div class=\"variant-mode-toggle\" *ngIf=\"obs.selectedTemplateId\">\n <label class=\"variant-mode-label\">{{\n 'variantSelectionMode' | pluginTranslate: pluginId | async\n }}</label>\n <div class=\"variant-mode-buttons\">\n <button\n type=\"button\"\n class=\"variant-mode-btn\"\n [class.active]=\"variantSelectionMode === 'explicit'\"\n (click)=\"onVariantSelectionModeChange('explicit')\"\n [disabled]=\"obs.disabled\"\n >\n {{ 'selectByVariant' | pluginTranslate: pluginId | async }}\n </button>\n <button\n type=\"button\"\n class=\"variant-mode-btn\"\n [class.active]=\"variantSelectionMode === 'attributes'\"\n (click)=\"onVariantSelectionModeChange('attributes')\"\n [disabled]=\"obs.disabled\"\n >\n {{ 'selectByAttributes' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n </div>\n\n <!-- Explicit variant selection (dropdown) -->\n <v-select\n *ngIf=\"variantSelectionMode === 'explicit'\"\n name=\"variantId\"\n [title]=\"'variantId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'variantIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.variants.data\"\n [defaultSelectionId]=\"obs.prefill?.variantId\"\n [disabled]=\"obs.disabled || obs.variants.loading || !obs.selectedTemplateId\"\n [required]=\"false\"\n [loading]=\"obs.variants.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.variants.error\" class=\"loading-error\">{{ obs.variants.error }}</div>\n\n <!-- Attribute-based variant selection -->\n <div\n *ngIf=\"variantSelectionMode === 'attributes' && obs.selectedTemplateId\"\n class=\"variant-attributes-section\"\n >\n <label class=\"variant-attributes-label\">{{\n 'variantAttributes' | pluginTranslate: pluginId | async\n }}</label>\n <div class=\"variant-attributes-list\">\n <div\n *ngFor=\"let entry of variantAttributeEntries; let i = index\"\n class=\"variant-attribute-row\"\n >\n <select\n *ngIf=\"!entry._customKey\"\n class=\"variant-attribute-input\"\n [ngModel]=\"entry.key\"\n (ngModelChange)=\"onKeySelected(entry, $event)\"\n [disabled]=\"obs.disabled\"\n >\n <option value=\"\" disabled>\n {{ 'attributeKey' | pluginTranslate: pluginId | async }}\n </option>\n <option *ngFor=\"let key of availableAttributeKeys\" [value]=\"key\">{{ key }}</option>\n <option value=\"__custom__\">\n {{ 'attributeKeyCustom' | pluginTranslate: pluginId | async }}\n </option>\n </select>\n <div *ngIf=\"entry._customKey\" class=\"custom-key-input\">\n <input\n type=\"text\"\n class=\"variant-attribute-input\"\n [placeholder]=\"'attributeKey' | pluginTranslate: pluginId | async\"\n [(ngModel)]=\"entry.key\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <button\n type=\"button\"\n class=\"custom-key-cancel\"\n (click)=\"cancelCustomKey(entry)\"\n [disabled]=\"obs.disabled\"\n >\n &times;\n </button>\n </div>\n <input\n type=\"text\"\n class=\"variant-attribute-input\"\n [placeholder]=\"'attributeValue' | pluginTranslate: pluginId | async\"\n [(ngModel)]=\"entry.value\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <label class=\"variant-attribute-required-toggle\">\n <input\n type=\"checkbox\"\n [(ngModel)]=\"entry.required\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <span class=\"required-label\">{{\n (entry.required ? 'attributeRequired' : 'attributePreferred')\n | pluginTranslate: pluginId\n | async\n }}</span>\n </label>\n <button\n type=\"button\"\n class=\"variant-attribute-remove-btn\"\n (click)=\"removeAttributeEntry(i)\"\n [disabled]=\"obs.disabled\"\n title=\"{{ 'removeAttribute' | pluginTranslate: pluginId | async }}\"\n >\n &times;\n </button>\n </div>\n </div>\n <button\n type=\"button\"\n class=\"variant-attribute-add-btn\"\n (click)=\"addAttributeEntry()\"\n [disabled]=\"obs.disabled\"\n >\n + {{ 'addAttribute' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <v-select\n name=\"environmentId\"\n [title]=\"'environmentId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'environmentIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.environments.data\"\n [defaultSelectionId]=\"obs.prefill?.environmentId\"\n [disabled]=\"obs.disabled || obs.environments.loading\"\n [required]=\"false\"\n [loading]=\"obs.environments.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.environments.error\" class=\"loading-error\">{{ obs.environments.error }}</div>\n\n <v-select\n name=\"outputFormat\"\n [title]=\"'outputFormat' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'outputFormatTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"outputFormatOptions\"\n [defaultSelectionId]=\"obs.prefill?.outputFormat || 'PDF'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-select>\n\n <v-input\n name=\"filename\"\n [title]=\"'filename' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'filenameTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.filename\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"correlationId\"\n [title]=\"'correlationId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'correlationIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.correlationId\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"resultProcessVariable\"\n [title]=\"'resultProcessVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'resultProcessVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.resultProcessVariable\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n\n<div *ngIf=\"(templateFields$ | async)?.error as templateFieldsError\" class=\"loading-error\">\n {{ templateFieldsError }}\n</div>\n\n<div *ngIf=\"selectedTemplateId$ | async\" class=\"mapping-section\">\n <h5 class=\"mapping-section__title\">\n {{ 'dataMappingTitle' | pluginTranslate: pluginId | async }}\n </h5>\n <p class=\"mapping-section__description\">\n {{ 'dataMappingDescription' | pluginTranslate: pluginId | async }}\n </p>\n <div class=\"mapping-mode-toggle\">\n <button\n class=\"mapping-mode-toggle__btn\"\n [class.mapping-mode-toggle__btn--active]=\"mappingMode === 'simple'\"\n (click)=\"mappingMode = 'simple'\"\n >\n {{ 'mappingModeSimple' | pluginTranslate: pluginId | async }}\n </button>\n <button\n class=\"mapping-mode-toggle__btn\"\n [class.mapping-mode-toggle__btn--active]=\"mappingMode === 'advanced'\"\n (click)=\"mappingMode = 'advanced'\"\n >\n {{ 'mappingModeAdvanced' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <!-- Editor area (full width) -->\n <epistola-mapping-builder\n *ngIf=\"mappingMode === 'simple'\"\n [expression]=\"dataMapping$ | async\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n [suggestions]=\"variableSuggestions\"\n [disabled]=\"!!(disabled$ | async)\"\n (expressionChange)=\"onDataMappingChange($event)\"\n ></epistola-mapping-builder>\n\n <epistola-jsonata-editor\n *ngIf=\"mappingMode === 'advanced'\"\n [expression]=\"dataMapping$ | async\"\n [disabled]=\"!!(disabled$ | async)\"\n [suggestions]=\"variableSuggestions\"\n [functions]=\"expressionFunctions\"\n (expressionChange)=\"onDataMappingChange($event)\"\n ></epistola-jsonata-editor>\n\n <!-- Bottom tabs: Schema + Preview (collapsible) -->\n <div class=\"mapping-tools\" [class.mapping-tools--collapsed]=\"toolsCollapsed\">\n <div class=\"mapping-tools__header\" (click)=\"toolsCollapsed = !toolsCollapsed\">\n <span class=\"mapping-tools__chevron\">{{ toolsCollapsed ? '&#x25B6;' : '&#x25BC;' }}</span>\n <span>{{ 'mappingTools' | pluginTranslate: pluginId | async }}</span>\n </div>\n <div *ngIf=\"!toolsCollapsed\" class=\"mapping-tools__content\">\n <div class=\"mapping-tools__tabs\">\n <button\n class=\"mapping-tools__tab\"\n [class.mapping-tools__tab--active]=\"activeToolTab === 'schema'\"\n (click)=\"activeToolTab = 'schema'\"\n >\n {{ 'expectedStructure' | pluginTranslate: pluginId | async }}\n </button>\n <button\n class=\"mapping-tools__tab\"\n [class.mapping-tools__tab--active]=\"activeToolTab === 'preview'\"\n (click)=\"activeToolTab = 'preview'\"\n >\n {{ 'previewTitle' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <epistola-expected-structure\n *ngIf=\"activeToolTab === 'schema'\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n ></epistola-expected-structure>\n\n <epistola-mapping-preview\n *ngIf=\"activeToolTab === 'preview'\"\n [expression]=\"dataMapping$ | async\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n ></epistola-mapping-preview>\n </div>\n </div>\n</div>\n", styles: [".loading-error{padding:.25rem .75rem;font-size:.8125rem;color:#dc3545}.validation-summary{margin-top:.5rem;padding:.5rem .75rem;border-radius:4px;font-size:.875rem}.validation-summary .validation-complete{color:#198754}.validation-summary .validation-incomplete{color:#dc3545;font-weight:500}.variant-mode-toggle{margin-bottom:1rem;padding:0 .75rem}.variant-mode-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-mode-buttons{display:flex;gap:0;border:1px solid #d1d5db;border-radius:4px;overflow:hidden;width:fit-content}.variant-mode-btn{padding:.375rem .75rem;font-size:.8125rem;background:#fff;border:none;border-right:1px solid #d1d5db;cursor:pointer;color:#374151;transition:background-color .15s,color .15s}.variant-mode-btn:last-child{border-right:none}.variant-mode-btn:hover:not([disabled]){background:#f3f4f6}.variant-mode-btn.active{background:#2563eb;color:#fff}.variant-mode-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attributes-section{margin-bottom:1rem;padding:0 .75rem}.variant-attributes-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-attributes-list{display:flex;flex-direction:column;gap:.375rem}.variant-attribute-row{display:flex;gap:.375rem;align-items:center}.variant-attribute-input{flex:1;padding:.375rem .5rem;font-size:.8125rem;border:1px solid #d1d5db;border-radius:4px;outline:none}.variant-attribute-input:focus{border-color:#2563eb;box-shadow:0 0 0 1px #2563eb}.variant-attribute-input[disabled]{opacity:.5;background:#f9fafb}.custom-key-input{display:flex;flex:1;gap:.25rem}.custom-key-input .variant-attribute-input{flex:1}.custom-key-cancel{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.custom-key-cancel:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.custom-key-cancel[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-required-toggle{display:flex;align-items:center;gap:.25rem;font-size:.75rem;color:#374151;white-space:nowrap;cursor:pointer}.variant-attribute-required-toggle input[type=checkbox]{margin:0;cursor:pointer}.variant-attribute-required-toggle .required-label{-webkit-user-select:none;user-select:none}.variant-attribute-remove-btn{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-remove-btn:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.variant-attribute-remove-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-add-btn{margin-top:.375rem;padding:.25rem .5rem;font-size:.8125rem;background:none;border:1px dashed #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-add-btn:hover:not([disabled]){color:#2563eb;border-color:#2563eb}.variant-attribute-add-btn[disabled]{opacity:.5;cursor:not-allowed}.mapping-section{margin-top:1rem}.mapping-section__title{font-size:1rem;font-weight:600;margin:0 0 4px}.mapping-section__description{font-size:.85em;color:#6f6f6f;margin:0 0 12px}.mapping-mode-toggle{display:flex;gap:0;margin-bottom:12px}.mapping-mode-toggle__btn{padding:6px 16px;border:1px solid #e0e0e0;background:#fff;font-size:.85em;cursor:pointer}.mapping-mode-toggle__btn:first-child{border-radius:4px 0 0 4px}.mapping-mode-toggle__btn:last-child{border-radius:0 4px 4px 0;border-left:none}.mapping-mode-toggle__btn--active{background:#0f62fe;color:#fff;border-color:#0f62fe}.mapping-tools{margin-top:12px;border:1px solid #e0e0e0;border-radius:4px;overflow:hidden}.mapping-tools__header{display:flex;align-items:center;gap:6px;padding:8px 12px;background:#f4f4f4;cursor:pointer;font-size:.85em;font-weight:500;-webkit-user-select:none;user-select:none}.mapping-tools__header:hover{background:#e8e8e8}.mapping-tools__chevron{font-size:.7em}.mapping-tools__content{border-top:1px solid #e0e0e0}.mapping-tools__tabs{display:flex;border-bottom:1px solid #e0e0e0}.mapping-tools__tab{padding:6px 16px;border:none;background:transparent;font-size:.8em;cursor:pointer;border-bottom:2px solid transparent}.mapping-tools__tab--active{border-bottom-color:#0f62fe;font-weight:500}\n"] }]
1833
- }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$2.ProcessLinkStateService }, { type: i0.ChangeDetectorRef }], propDecorators: { save$: [{
2083
+ ], template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: disabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n catalogs: catalogs$ | async,\n templates: templates$ | async,\n variants: variants$ | async,\n environments: environments$ | async,\n templateFields: templateFields$ | async,\n selectedCatalogId: selectedCatalogId$ | async,\n selectedTemplateId: selectedTemplateId$ | async,\n validationErrors: validationErrors$ | async,\n } as obs\"\n>\n <div\n *ngIf=\"obs.validationErrors && obs.validationErrors.length > 0\"\n class=\"jsonata-validation-errors\"\n >\n <strong>{{ 'jsonataValidationErrorsHeading' | pluginTranslate: pluginId | async }}</strong>\n <ul>\n <li *ngFor=\"let err of obs.validationErrors\">\n <code>{{ err.field }}</code\n >: {{ err.message }}\n </li>\n </ul>\n </div>\n <v-select\n name=\"catalogId\"\n [title]=\"'catalogId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'catalogIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.catalogs.data\"\n [defaultSelectionId]=\"obs.prefill?.catalogId\"\n [disabled]=\"obs.disabled || obs.catalogs.loading\"\n [required]=\"true\"\n [loading]=\"obs.catalogs.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.catalogs.error\" class=\"loading-error\">{{ obs.catalogs.error }}</div>\n\n <v-select\n name=\"templateId\"\n [title]=\"'templateId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'templateIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.templates.data\"\n [defaultSelectionId]=\"obs.prefill?.templateId\"\n [disabled]=\"obs.disabled || obs.templates.loading || !obs.selectedCatalogId\"\n [required]=\"true\"\n [loading]=\"obs.templates.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.templates.error\" class=\"loading-error\">{{ obs.templates.error }}</div>\n\n <!-- Variant selection mode toggle -->\n <div class=\"variant-mode-toggle\" *ngIf=\"obs.selectedTemplateId\">\n <label class=\"variant-mode-label\">{{\n 'variantSelectionMode' | pluginTranslate: pluginId | async\n }}</label>\n <div class=\"variant-mode-buttons\">\n <button\n type=\"button\"\n class=\"variant-mode-btn\"\n [class.active]=\"variantSelectionMode === 'explicit'\"\n (click)=\"onVariantSelectionModeChange('explicit')\"\n [disabled]=\"obs.disabled\"\n >\n {{ 'selectByVariant' | pluginTranslate: pluginId | async }}\n </button>\n <button\n type=\"button\"\n class=\"variant-mode-btn\"\n [class.active]=\"variantSelectionMode === 'attributes'\"\n (click)=\"onVariantSelectionModeChange('attributes')\"\n [disabled]=\"obs.disabled\"\n >\n {{ 'selectByAttributes' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n </div>\n\n <!-- Explicit variant selection (dropdown or expression) -->\n <div *ngIf=\"variantSelectionMode === 'explicit'\" class=\"field-with-fx\">\n <v-select\n *ngIf=\"!variantIdExpressionMode\"\n name=\"variantId\"\n [title]=\"'variantId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'variantIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.variants.data\"\n [defaultSelectionId]=\"obs.prefill?.variantId\"\n [disabled]=\"obs.disabled || obs.variants.loading || !obs.selectedTemplateId\"\n [required]=\"false\"\n [loading]=\"obs.variants.loading\"\n >\n </v-select>\n <div *ngIf=\"variantIdExpressionMode\" class=\"fx-input-group\">\n <label class=\"fx-input-label\">{{ 'variantId' | pluginTranslate: pluginId | async }}</label>\n <input\n type=\"text\"\n class=\"fx-input\"\n [ngModel]=\"variantIdExpression\"\n (ngModelChange)=\"variantIdExpression = $event; onVariantIdExpressionChange()\"\n [disabled]=\"obs.disabled\"\n placeholder=\"$pv.letterType\"\n />\n </div>\n <button\n type=\"button\"\n class=\"fx-toggle\"\n (click)=\"variantIdExpressionMode = !variantIdExpressionMode\"\n [disabled]=\"obs.disabled\"\n [title]=\"variantIdExpressionMode ? 'Switch to dropdown' : 'Switch to expression'\"\n >\n {{ variantIdExpressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n <div *ngIf=\"obs.variants.error\" class=\"loading-error\">{{ obs.variants.error }}</div>\n\n <!-- Attribute-based variant selection -->\n <div\n *ngIf=\"variantSelectionMode === 'attributes' && obs.selectedTemplateId\"\n class=\"variant-attributes-section\"\n >\n <label class=\"variant-attributes-label\">{{\n 'variantAttributes' | pluginTranslate: pluginId | async\n }}</label>\n <div class=\"variant-attributes-list\">\n <div\n *ngFor=\"let entry of variantAttributeEntries; let i = index\"\n class=\"variant-attribute-row\"\n >\n <select\n *ngIf=\"!entry._customKey\"\n class=\"variant-attribute-input\"\n [ngModel]=\"entry.key\"\n (ngModelChange)=\"onKeySelected(entry, $event)\"\n [disabled]=\"obs.disabled\"\n >\n <option value=\"\" disabled>\n {{ 'attributeKey' | pluginTranslate: pluginId | async }}\n </option>\n <option *ngFor=\"let key of availableAttributeKeys\" [value]=\"key\">{{ key }}</option>\n <option value=\"__custom__\">\n {{ 'attributeKeyCustom' | pluginTranslate: pluginId | async }}\n </option>\n </select>\n <div *ngIf=\"entry._customKey\" class=\"custom-key-input\">\n <input\n type=\"text\"\n class=\"variant-attribute-input\"\n [placeholder]=\"'attributeKey' | pluginTranslate: pluginId | async\"\n [(ngModel)]=\"entry.key\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <button\n type=\"button\"\n class=\"custom-key-cancel\"\n (click)=\"cancelCustomKey(entry)\"\n [disabled]=\"obs.disabled\"\n >\n &times;\n </button>\n </div>\n <div class=\"attribute-value-with-fx\">\n <input\n type=\"text\"\n class=\"variant-attribute-input\"\n [class.fx-input]=\"entry._expressionMode\"\n [placeholder]=\"\n entry._expressionMode\n ? '$pv.language'\n : ('attributeValue' | pluginTranslate: pluginId | async)\n \"\n [(ngModel)]=\"entry.value\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <button\n type=\"button\"\n class=\"fx-toggle fx-toggle--inline\"\n (click)=\"entry._expressionMode = !entry._expressionMode\"\n [disabled]=\"obs.disabled\"\n [title]=\"entry._expressionMode ? 'Switch to plain value' : 'Switch to expression'\"\n >\n {{ entry._expressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n <label class=\"variant-attribute-required-toggle\">\n <input\n type=\"checkbox\"\n [(ngModel)]=\"entry.required\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <span class=\"required-label\">{{\n (entry.required ? 'attributeRequired' : 'attributePreferred')\n | pluginTranslate: pluginId\n | async\n }}</span>\n </label>\n <button\n type=\"button\"\n class=\"variant-attribute-remove-btn\"\n (click)=\"removeAttributeEntry(i)\"\n [disabled]=\"obs.disabled\"\n title=\"{{ 'removeAttribute' | pluginTranslate: pluginId | async }}\"\n >\n &times;\n </button>\n </div>\n </div>\n <button\n type=\"button\"\n class=\"variant-attribute-add-btn\"\n (click)=\"addAttributeEntry()\"\n [disabled]=\"obs.disabled\"\n >\n + {{ 'addAttribute' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <v-select\n name=\"environmentId\"\n [title]=\"'environmentId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'environmentIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.environments.data\"\n [defaultSelectionId]=\"obs.prefill?.environmentId\"\n [disabled]=\"obs.disabled || obs.environments.loading\"\n [required]=\"false\"\n [loading]=\"obs.environments.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.environments.error\" class=\"loading-error\">{{ obs.environments.error }}</div>\n\n <v-select\n name=\"outputFormat\"\n [title]=\"'outputFormat' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'outputFormatTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"outputFormatOptions\"\n [defaultSelectionId]=\"obs.prefill?.outputFormat || 'PDF'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-select>\n\n <div class=\"field-with-fx\">\n <v-input\n *ngIf=\"!filenameExpressionMode\"\n name=\"filename\"\n [title]=\"'filename' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'filenameTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.filename\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n <div *ngIf=\"filenameExpressionMode\" class=\"fx-input-group\">\n <label class=\"fx-input-label\">{{ 'filename' | pluginTranslate: pluginId | async }}</label>\n <input\n type=\"text\"\n class=\"fx-input\"\n [ngModel]=\"filenameExpression\"\n (ngModelChange)=\"filenameExpression = $event; onFilenameExpressionChange()\"\n [disabled]=\"obs.disabled\"\n placeholder='\"besluit-\" & $doc.name & \".pdf\"'\n />\n </div>\n <button\n type=\"button\"\n class=\"fx-toggle\"\n (click)=\"filenameExpressionMode = !filenameExpressionMode\"\n [disabled]=\"obs.disabled\"\n [title]=\"filenameExpressionMode ? 'Switch to plain input' : 'Switch to expression'\"\n >\n {{ filenameExpressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n\n <v-input\n name=\"correlationId\"\n [title]=\"'correlationId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'correlationIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.correlationId\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"resultProcessVariable\"\n [title]=\"'resultProcessVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'resultProcessVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.resultProcessVariable\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n\n<div *ngIf=\"(templateFields$ | async)?.error as templateFieldsError\" class=\"loading-error\">\n {{ templateFieldsError }}\n</div>\n\n<div *ngIf=\"selectedTemplateId$ | async\" class=\"mapping-section\">\n <h5 class=\"mapping-section__title\">\n {{ 'dataMappingTitle' | pluginTranslate: pluginId | async }}\n </h5>\n <p class=\"mapping-section__description\">\n {{ 'dataMappingDescription' | pluginTranslate: pluginId | async }}\n </p>\n <div class=\"mapping-mode-toggle\">\n <button\n class=\"mapping-mode-toggle__btn\"\n [class.mapping-mode-toggle__btn--active]=\"mappingMode === 'simple'\"\n (click)=\"mappingMode = 'simple'\"\n >\n {{ 'mappingModeSimple' | pluginTranslate: pluginId | async }}\n </button>\n <button\n class=\"mapping-mode-toggle__btn\"\n [class.mapping-mode-toggle__btn--active]=\"mappingMode === 'advanced'\"\n (click)=\"mappingMode = 'advanced'\"\n >\n {{ 'mappingModeAdvanced' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <!-- Editor area (full width) -->\n <epistola-mapping-builder\n *ngIf=\"mappingMode === 'simple'\"\n [expression]=\"dataMapping$ | async\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n [suggestions]=\"variableSuggestions\"\n [disabled]=\"!!(disabled$ | async)\"\n (expressionChange)=\"onDataMappingChange($event)\"\n ></epistola-mapping-builder>\n\n <epistola-jsonata-editor\n *ngIf=\"mappingMode === 'advanced'\"\n [expression]=\"dataMapping$ | async\"\n [disabled]=\"!!(disabled$ | async)\"\n [suggestions]=\"variableSuggestions\"\n [functions]=\"expressionFunctions\"\n (expressionChange)=\"onDataMappingChange($event)\"\n ></epistola-jsonata-editor>\n\n <!-- Bottom tabs: Schema + Preview (collapsible) -->\n <div class=\"mapping-tools\" [class.mapping-tools--collapsed]=\"toolsCollapsed\">\n <div class=\"mapping-tools__header\" (click)=\"toolsCollapsed = !toolsCollapsed\">\n <span class=\"mapping-tools__chevron\">{{ toolsCollapsed ? '&#x25B6;' : '&#x25BC;' }}</span>\n <span>{{ 'mappingTools' | pluginTranslate: pluginId | async }}</span>\n </div>\n <div *ngIf=\"!toolsCollapsed\" class=\"mapping-tools__content\">\n <div class=\"mapping-tools__tabs\">\n <button\n class=\"mapping-tools__tab\"\n [class.mapping-tools__tab--active]=\"activeToolTab === 'schema'\"\n (click)=\"activeToolTab = 'schema'\"\n >\n {{ 'expectedStructure' | pluginTranslate: pluginId | async }}\n </button>\n <button\n class=\"mapping-tools__tab\"\n [class.mapping-tools__tab--active]=\"activeToolTab === 'preview'\"\n (click)=\"activeToolTab = 'preview'\"\n >\n {{ 'previewTitle' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <epistola-expected-structure\n *ngIf=\"activeToolTab === 'schema'\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n ></epistola-expected-structure>\n\n <epistola-mapping-preview\n *ngIf=\"activeToolTab === 'preview'\"\n [expression]=\"dataMapping$ | async\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n ></epistola-mapping-preview>\n </div>\n </div>\n</div>\n", styles: [".loading-error{padding:.25rem .75rem;font-size:.8125rem;color:#dc3545}.jsonata-validation-errors{margin-bottom:1rem;padding:.75rem 1rem;border:1px solid #dc3545;border-radius:4px;background:#fdf3f4;color:#dc3545;font-size:.875rem}.jsonata-validation-errors ul{margin:.5rem 0 0;padding-left:1.25rem}.jsonata-validation-errors code{background:#dc35451a;padding:0 .25rem;border-radius:2px;font-family:monospace}.validation-summary{margin-top:.5rem;padding:.5rem .75rem;border-radius:4px;font-size:.875rem}.validation-summary .validation-complete{color:#198754}.validation-summary .validation-incomplete{color:#dc3545;font-weight:500}.variant-mode-toggle{margin-bottom:1rem;padding:0 .75rem}.variant-mode-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-mode-buttons{display:flex;gap:0;border:1px solid #d1d5db;border-radius:4px;overflow:hidden;width:fit-content}.variant-mode-btn{padding:.375rem .75rem;font-size:.8125rem;background:#fff;border:none;border-right:1px solid #d1d5db;cursor:pointer;color:#374151;transition:background-color .15s,color .15s}.variant-mode-btn:last-child{border-right:none}.variant-mode-btn:hover:not([disabled]){background:#f3f4f6}.variant-mode-btn.active{background:#2563eb;color:#fff}.variant-mode-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attributes-section{margin-bottom:1rem;padding:0 .75rem}.variant-attributes-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-attributes-list{display:flex;flex-direction:column;gap:.375rem}.variant-attribute-row{display:flex;gap:.375rem;align-items:center}.variant-attribute-input{flex:1;padding:.375rem .5rem;font-size:.8125rem;border:1px solid #d1d5db;border-radius:4px;outline:none}.variant-attribute-input:focus{border-color:#2563eb;box-shadow:0 0 0 1px #2563eb}.variant-attribute-input[disabled]{opacity:.5;background:#f9fafb}.custom-key-input{display:flex;flex:1;gap:.25rem}.custom-key-input .variant-attribute-input{flex:1}.custom-key-cancel{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.custom-key-cancel:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.custom-key-cancel[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-required-toggle{display:flex;align-items:center;gap:.25rem;font-size:.75rem;color:#374151;white-space:nowrap;cursor:pointer}.variant-attribute-required-toggle input[type=checkbox]{margin:0;cursor:pointer}.variant-attribute-required-toggle .required-label{-webkit-user-select:none;user-select:none}.variant-attribute-remove-btn{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-remove-btn:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.variant-attribute-remove-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-add-btn{margin-top:.375rem;padding:.25rem .5rem;font-size:.8125rem;background:none;border:1px dashed #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-add-btn:hover:not([disabled]){color:#2563eb;border-color:#2563eb}.variant-attribute-add-btn[disabled]{opacity:.5;cursor:not-allowed}.field-with-fx{display:flex;align-items:flex-start;gap:4px}.field-with-fx>*:first-child{flex:1;min-width:0}.fx-toggle{width:28px;height:28px;margin-top:22px;border:1px solid #e0e0e0;border-radius:4px;background:#fff;cursor:pointer;font-family:monospace;font-size:.8em;display:flex;align-items:center;justify-content:center;flex-shrink:0}.fx-toggle:hover{background:#f4f4f4}.fx-toggle--inline{margin-top:0}.fx-input-group{flex:1;min-width:0;margin-bottom:.75rem}.fx-input-label{display:block;font-size:.875rem;margin-bottom:.25rem;color:#525252}.fx-input{width:100%;border:1px solid #8d8d8d;border-radius:0;padding:.4rem .75rem;font-family:monospace;font-size:.875rem;background:#f4f4f4}.attribute-value-with-fx{display:flex;align-items:center;gap:4px;flex:1;min-width:0}.attribute-value-with-fx>input{flex:1;min-width:0}.mapping-section{margin-top:1rem}.mapping-section__title{font-size:1rem;font-weight:600;margin:0 0 4px}.mapping-section__description{font-size:.85em;color:#6f6f6f;margin:0 0 12px}.mapping-mode-toggle{display:flex;gap:0;margin-bottom:12px}.mapping-mode-toggle__btn{padding:6px 16px;border:1px solid #e0e0e0;background:#fff;font-size:.85em;cursor:pointer}.mapping-mode-toggle__btn:first-child{border-radius:4px 0 0 4px}.mapping-mode-toggle__btn:last-child{border-radius:0 4px 4px 0;border-left:none}.mapping-mode-toggle__btn--active{background:#0f62fe;color:#fff;border-color:#0f62fe}.mapping-tools{margin-top:12px;border:1px solid #e0e0e0;border-radius:4px;overflow:hidden}.mapping-tools__header{display:flex;align-items:center;gap:6px;padding:8px 12px;background:#f4f4f4;cursor:pointer;font-size:.85em;font-weight:500;-webkit-user-select:none;user-select:none}.mapping-tools__header:hover{background:#e8e8e8}.mapping-tools__chevron{font-size:.7em}.mapping-tools__content{border-top:1px solid #e0e0e0}.mapping-tools__tabs{display:flex;border-bottom:1px solid #e0e0e0}.mapping-tools__tab{padding:6px 16px;border:none;background:transparent;font-size:.8em;cursor:pointer;border-bottom:2px solid transparent}.mapping-tools__tab--active{border-bottom-color:#0f62fe;font-weight:500}\n"] }]
2084
+ }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$3.ProcessLinkStateService }, { type: i0.ChangeDetectorRef }], propDecorators: { save$: [{
1834
2085
  type: Input
1835
2086
  }], disabled$: [{
1836
2087
  type: Input
@@ -1858,9 +2109,17 @@ class CheckJobStatusConfigurationComponent {
1858
2109
  saveSubscription;
1859
2110
  formValue$ = new BehaviorSubject(null);
1860
2111
  valid$ = new BehaviorSubject(false);
2112
+ /** Resolved synchronously before the v-form renders — see download-document-configuration for the why. */
2113
+ resolvedPrefill = {};
2114
+ prefillResolved$ = new BehaviorSubject(false);
1861
2115
  safeDisabled$;
1862
2116
  ngOnInit() {
1863
2117
  this.safeDisabled$ = this.disabled$.pipe(startWith(true), delay(0));
2118
+ const prefill$ = this.prefillConfiguration$ ?? of({});
2119
+ prefill$.pipe(take(1)).subscribe((prefill) => {
2120
+ this.resolvedPrefill = prefill ?? {};
2121
+ this.prefillResolved$.next(true);
2122
+ });
1864
2123
  this.openSaveSubscription();
1865
2124
  }
1866
2125
  ngOnDestroy() {
@@ -1888,11 +2147,11 @@ class CheckJobStatusConfigurationComponent {
1888
2147
  });
1889
2148
  }
1890
2149
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: CheckJobStatusConfigurationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1891
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: CheckJobStatusConfigurationComponent, isStandalone: true, selector: "epistola-check-job-status-configuration", inputs: { save$: "save$", disabled$: "disabled$", pluginId: "pluginId", prefillConfiguration$: "prefillConfiguration$" }, outputs: { valid: "valid", configuration: "configuration" }, ngImport: i0, template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: safeDisabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n } as obs\"\n>\n <v-input\n name=\"requestIdVariable\"\n [title]=\"'requestIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'requestIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.requestIdVariable || 'epistolaRequestId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"statusVariable\"\n [title]=\"'statusVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'statusVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.statusVariable || 'epistolaStatus'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"documentIdVariable\"\n [title]=\"'documentIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.documentIdVariable || 'epistolaDocumentId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"errorMessageVariable\"\n [title]=\"'errorMessageVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'errorMessageVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.errorMessageVariable || 'epistolaErrorMessage'\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n</v-form>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: FormModule }, { kind: "component", type: i3.FormComponent, selector: "v-form", inputs: ["className"], outputs: ["valueChange"] }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i3.InputComponent, selector: "v-input", inputs: ["name", "type", "title", "titleTranslationKey", "defaultValue", "widthPx", "fullWidth", "margin", "smallMargin", "disabled", "step", "min", "maxLength", "tooltip", "required", "hideNumberSpinBox", "smallLabel", "rows", "clear$", "carbonTheme", "placeholder", "dataTestId", "trim", "presetsTitle", "presetOptions"], outputs: ["valueChange"] }] });
2150
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: CheckJobStatusConfigurationComponent, isStandalone: true, selector: "epistola-check-job-status-configuration", inputs: { save$: "save$", disabled$: "disabled$", pluginId: "pluginId", prefillConfiguration$: "prefillConfiguration$" }, outputs: { valid: "valid", configuration: "configuration" }, ngImport: i0, template: "<v-form (valueChange)=\"formValueChange($event)\" *ngIf=\"prefillResolved$ | async\">\n <v-input\n name=\"requestIdVariable\"\n [title]=\"'requestIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'requestIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.requestIdVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"statusVariable\"\n [title]=\"'statusVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'statusVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.statusVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"documentIdVariable\"\n [title]=\"'documentIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.documentIdVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"errorMessageVariable\"\n [title]=\"'errorMessageVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'errorMessageVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.errorMessageVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"false\"\n >\n </v-input>\n</v-form>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: FormModule }, { kind: "component", type: i3.FormComponent, selector: "v-form", inputs: ["className"], outputs: ["valueChange"] }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i3.InputComponent, selector: "v-input", inputs: ["name", "type", "title", "titleTranslationKey", "defaultValue", "widthPx", "fullWidth", "margin", "smallMargin", "disabled", "step", "min", "maxLength", "tooltip", "required", "hideNumberSpinBox", "smallLabel", "rows", "clear$", "carbonTheme", "placeholder", "dataTestId", "trim", "presetsTitle", "presetOptions"], outputs: ["valueChange"] }] });
1892
2151
  }
1893
2152
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: CheckJobStatusConfigurationComponent, decorators: [{
1894
2153
  type: Component,
1895
- args: [{ selector: 'epistola-check-job-status-configuration', standalone: true, imports: [CommonModule, PluginTranslatePipeModule, FormModule, InputModule], template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: safeDisabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n } as obs\"\n>\n <v-input\n name=\"requestIdVariable\"\n [title]=\"'requestIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'requestIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.requestIdVariable || 'epistolaRequestId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"statusVariable\"\n [title]=\"'statusVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'statusVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.statusVariable || 'epistolaStatus'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"documentIdVariable\"\n [title]=\"'documentIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.documentIdVariable || 'epistolaDocumentId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"errorMessageVariable\"\n [title]=\"'errorMessageVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'errorMessageVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.errorMessageVariable || 'epistolaErrorMessage'\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n</v-form>\n" }]
2154
+ args: [{ selector: 'epistola-check-job-status-configuration', standalone: true, imports: [CommonModule, PluginTranslatePipeModule, FormModule, InputModule], template: "<v-form (valueChange)=\"formValueChange($event)\" *ngIf=\"prefillResolved$ | async\">\n <v-input\n name=\"requestIdVariable\"\n [title]=\"'requestIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'requestIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.requestIdVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"statusVariable\"\n [title]=\"'statusVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'statusVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.statusVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"documentIdVariable\"\n [title]=\"'documentIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.documentIdVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"errorMessageVariable\"\n [title]=\"'errorMessageVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'errorMessageVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.errorMessageVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"false\"\n >\n </v-input>\n</v-form>\n" }]
1896
2155
  }], propDecorators: { save$: [{
1897
2156
  type: Input
1898
2157
  }], disabled$: [{
@@ -1917,9 +2176,21 @@ class DownloadDocumentConfigurationComponent {
1917
2176
  saveSubscription;
1918
2177
  formValue$ = new BehaviorSubject(null);
1919
2178
  valid$ = new BehaviorSubject(false);
2179
+ /**
2180
+ * Resolved prefill — populated synchronously before the v-form renders. Avoids the
2181
+ * v-input `[defaultValue]` async-binding race that otherwise drops one of the
2182
+ * fields when prefill arrives after mount.
2183
+ */
2184
+ resolvedPrefill = {};
2185
+ prefillResolved$ = new BehaviorSubject(false);
1920
2186
  safeDisabled$;
1921
2187
  ngOnInit() {
1922
2188
  this.safeDisabled$ = this.disabled$.pipe(startWith(true), delay(0));
2189
+ const prefill$ = this.prefillConfiguration$ ?? of({});
2190
+ prefill$.pipe(take(1)).subscribe((prefill) => {
2191
+ this.resolvedPrefill = prefill ?? {};
2192
+ this.prefillResolved$.next(true);
2193
+ });
1923
2194
  this.openSaveSubscription();
1924
2195
  }
1925
2196
  ngOnDestroy() {
@@ -1931,7 +2202,7 @@ class DownloadDocumentConfigurationComponent {
1931
2202
  this.handleValid(formValue);
1932
2203
  }
1933
2204
  handleValid(formValue) {
1934
- const valid = !!(formValue?.documentIdVariable && formValue?.contentVariable);
2205
+ const valid = !!(formValue?.documentVariable && formValue?.contentVariable);
1935
2206
  this.valid$.next(valid);
1936
2207
  this.valid.emit(valid);
1937
2208
  }
@@ -1947,11 +2218,11 @@ class DownloadDocumentConfigurationComponent {
1947
2218
  });
1948
2219
  }
1949
2220
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DownloadDocumentConfigurationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1950
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: DownloadDocumentConfigurationComponent, isStandalone: true, selector: "epistola-download-document-configuration", inputs: { save$: "save$", disabled$: "disabled$", pluginId: "pluginId", prefillConfiguration$: "prefillConfiguration$" }, outputs: { valid: "valid", configuration: "configuration" }, ngImport: i0, template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: safeDisabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n } as obs\"\n>\n <v-input\n name=\"documentIdVariable\"\n [title]=\"'documentIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.documentIdVariable || 'epistolaDocumentId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"contentVariable\"\n [title]=\"'contentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'contentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.contentVariable || 'documentContent'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: FormModule }, { kind: "component", type: i3.FormComponent, selector: "v-form", inputs: ["className"], outputs: ["valueChange"] }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i3.InputComponent, selector: "v-input", inputs: ["name", "type", "title", "titleTranslationKey", "defaultValue", "widthPx", "fullWidth", "margin", "smallMargin", "disabled", "step", "min", "maxLength", "tooltip", "required", "hideNumberSpinBox", "smallLabel", "rows", "clear$", "carbonTheme", "placeholder", "dataTestId", "trim", "presetsTitle", "presetOptions"], outputs: ["valueChange"] }] });
2221
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: DownloadDocumentConfigurationComponent, isStandalone: true, selector: "epistola-download-document-configuration", inputs: { save$: "save$", disabled$: "disabled$", pluginId: "pluginId", prefillConfiguration$: "prefillConfiguration$" }, outputs: { valid: "valid", configuration: "configuration" }, ngImport: i0, template: "<v-form (valueChange)=\"formValueChange($event)\" *ngIf=\"prefillResolved$ | async\">\n <v-input\n name=\"documentVariable\"\n [title]=\"'documentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.documentVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"contentVariable\"\n [title]=\"'contentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'contentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.contentVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: FormModule }, { kind: "component", type: i3.FormComponent, selector: "v-form", inputs: ["className"], outputs: ["valueChange"] }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i3.InputComponent, selector: "v-input", inputs: ["name", "type", "title", "titleTranslationKey", "defaultValue", "widthPx", "fullWidth", "margin", "smallMargin", "disabled", "step", "min", "maxLength", "tooltip", "required", "hideNumberSpinBox", "smallLabel", "rows", "clear$", "carbonTheme", "placeholder", "dataTestId", "trim", "presetsTitle", "presetOptions"], outputs: ["valueChange"] }] });
1951
2222
  }
1952
2223
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DownloadDocumentConfigurationComponent, decorators: [{
1953
2224
  type: Component,
1954
- args: [{ selector: 'epistola-download-document-configuration', standalone: true, imports: [CommonModule, PluginTranslatePipeModule, FormModule, InputModule], template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: safeDisabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n } as obs\"\n>\n <v-input\n name=\"documentIdVariable\"\n [title]=\"'documentIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.documentIdVariable || 'epistolaDocumentId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"contentVariable\"\n [title]=\"'contentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'contentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.contentVariable || 'documentContent'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n" }]
2225
+ args: [{ selector: 'epistola-download-document-configuration', standalone: true, imports: [CommonModule, PluginTranslatePipeModule, FormModule, InputModule], template: "<v-form (valueChange)=\"formValueChange($event)\" *ngIf=\"prefillResolved$ | async\">\n <v-input\n name=\"documentVariable\"\n [title]=\"'documentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.documentVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"contentVariable\"\n [title]=\"'contentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'contentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.contentVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n" }]
1955
2226
  }], propDecorators: { save$: [{
1956
2227
  type: Input
1957
2228
  }], disabled$: [{
@@ -1966,35 +2237,89 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
1966
2237
  type: Output
1967
2238
  }] } });
1968
2239
 
1969
- class EpistolaDownloadComponent {
1970
- http;
2240
+ /**
2241
+ * Unified Formio component for the after-generation Epistola PDF UX. Reads
2242
+ * the PDF id and tenant id from named process variables on the caller's
2243
+ * task via the {@code /documents/download} endpoint.
2244
+ *
2245
+ * <p>Three presentations, all backed by the same backend call:
2246
+ * <ul>
2247
+ * <li>{@code display="inline"} — render the PDF inline in a panel.</li>
2248
+ * <li>{@code display="button"} — show a click-to-save download button only.</li>
2249
+ * <li>{@code display="both"} (default) — inline panel with a download icon
2250
+ * in the header.</li>
2251
+ * </ul>
2252
+ *
2253
+ * <p>For the dry-run / what-would-be-generated UX use
2254
+ * {@code epistola-document-preview} instead.
2255
+ */
2256
+ class EpistolaDocumentComponent {
2257
+ epistolaPluginService;
2258
+ sanitizer;
2259
+ formIoStateService;
2260
+ taskContext;
2261
+ cdr;
1971
2262
  value;
1972
2263
  valueChange = new EventEmitter();
1973
2264
  disabled = false;
2265
+ label = 'Document';
2266
+ /** How to present the document. `both` (default) shows an inline panel with a download icon. */
2267
+ display = 'both';
2268
+ /**
2269
+ * Process-variable name holding the Epistola result. Default: `epistolaResult`.
2270
+ *
2271
+ * Type-tolerant on the backend: if the named variable holds a rich result object
2272
+ * (`Map<String, Object>` written by `generate-document` and updated by the result
2273
+ * collector), the backend digs out the `documentId` key. If it holds a plain
2274
+ * String (custom flow that wrote a bare id), the backend uses it as-is.
2275
+ */
2276
+ documentVariable = 'epistolaResult';
2277
+ /** Process-variable name holding the Epistola tenant id. Default: `epistolaTenantId`. */
2278
+ tenantIdVariable = 'epistolaTenantId';
2279
+ /** Filename used for the download disposition. */
1974
2280
  filename = 'document.pdf';
1975
- label = 'Download PDF';
2281
+ loading = false;
1976
2282
  downloading = false;
1977
2283
  error = null;
1978
- get buttonLabel() {
1979
- return this.label || 'Download PDF';
2284
+ previewUrl = null;
2285
+ currentBlobUrl = null;
2286
+ subscription;
2287
+ get designMode() {
2288
+ return !this.formIoStateService.documentId;
1980
2289
  }
1981
- constructor(http) {
1982
- this.http = http;
2290
+ constructor(epistolaPluginService, sanitizer, formIoStateService, taskContext, cdr) {
2291
+ this.epistolaPluginService = epistolaPluginService;
2292
+ this.sanitizer = sanitizer;
2293
+ this.formIoStateService = formIoStateService;
2294
+ this.taskContext = taskContext;
2295
+ this.cdr = cdr;
2296
+ }
2297
+ ngOnInit() {
2298
+ if (this.designMode) {
2299
+ return;
2300
+ }
2301
+ if (this.display !== 'button') {
2302
+ this.loadInline();
2303
+ }
2304
+ }
2305
+ ngOnDestroy() {
2306
+ this.subscription?.unsubscribe();
2307
+ this.revokeBlobUrl();
1983
2308
  }
1984
- hasRequiredData() {
1985
- return !!(this.value?.documentId && this.value?.tenantId);
2309
+ refresh() {
2310
+ this.loadInline();
1986
2311
  }
1987
2312
  download() {
1988
- if (!this.hasRequiredData() || this.downloading) {
2313
+ if (this.designMode || this.downloading) {
1989
2314
  return;
1990
2315
  }
2316
+ const request = this.buildRequest('attachment');
2317
+ if (!request)
2318
+ return;
1991
2319
  this.downloading = true;
1992
2320
  this.error = null;
1993
- const { documentId, tenantId } = this.value;
1994
- const url = `/api/v1/plugin/epistola/documents/${encodeURIComponent(documentId)}/download` +
1995
- `?tenantId=${encodeURIComponent(tenantId)}` +
1996
- `&filename=${encodeURIComponent(this.filename)}`;
1997
- this.http.get(url, { responseType: 'blob' }).subscribe({
2321
+ this.cdr.markForCheck();
2322
+ this.epistolaPluginService.downloadDocumentBlob(request).subscribe({
1998
2323
  next: (blob) => {
1999
2324
  const objectUrl = URL.createObjectURL(blob);
2000
2325
  const anchor = document.createElement('a');
@@ -2003,66 +2328,246 @@ class EpistolaDownloadComponent {
2003
2328
  anchor.click();
2004
2329
  URL.revokeObjectURL(objectUrl);
2005
2330
  this.downloading = false;
2331
+ this.cdr.markForCheck();
2006
2332
  },
2007
2333
  error: (err) => {
2008
- console.error('Download failed', err);
2009
- this.error = 'Download mislukt. Probeer opnieuw.';
2334
+ this.error = err.status === 404 ? 'Document is nog niet gegenereerd.' : 'Download mislukt.';
2010
2335
  this.downloading = false;
2336
+ this.cdr.markForCheck();
2011
2337
  },
2012
2338
  });
2013
2339
  }
2014
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDownloadComponent, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Component });
2015
- 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: `
2016
- <button
2017
- type="button"
2018
- class="btn btn-outline-primary"
2019
- [disabled]="disabled || downloading || !hasRequiredData()"
2020
- (click)="download()"
2021
- >
2022
- <i class="mdi mdi-download mr-1"></i>
2023
- {{ downloading ? 'Downloading...' : buttonLabel }}
2024
- </button>
2025
- <span *ngIf="error" class="text-danger ml-2">{{ error }}</span>
2026
- `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
2340
+ loadInline() {
2341
+ const request = this.buildRequest('inline');
2342
+ if (!request)
2343
+ return;
2344
+ this.loading = true;
2345
+ this.error = null;
2346
+ this.cdr.markForCheck();
2347
+ this.revokeBlobUrl();
2348
+ this.subscription?.unsubscribe();
2349
+ this.subscription = this.epistolaPluginService.downloadDocumentBlob(request).subscribe({
2350
+ next: (blob) => {
2351
+ this.currentBlobUrl = URL.createObjectURL(blob);
2352
+ this.previewUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.currentBlobUrl);
2353
+ this.loading = false;
2354
+ this.cdr.markForCheck();
2355
+ },
2356
+ error: (err) => {
2357
+ this.previewUrl = null;
2358
+ this.error =
2359
+ err.status === 404
2360
+ ? 'Document is nog niet gegenereerd.'
2361
+ : 'Document kon niet geladen worden.';
2362
+ this.loading = false;
2363
+ this.cdr.markForCheck();
2364
+ },
2365
+ });
2366
+ }
2367
+ buildRequest(disposition) {
2368
+ const taskId = this.taskContext.taskInstanceId;
2369
+ const caseDocumentId = this.formIoStateService.documentId;
2370
+ if (!taskId || !caseDocumentId) {
2371
+ this.error = 'Document is alleen beschikbaar binnen een taak.';
2372
+ this.cdr.markForCheck();
2373
+ return null;
2374
+ }
2375
+ return {
2376
+ taskId,
2377
+ caseDocumentId,
2378
+ documentVariable: this.documentVariable,
2379
+ tenantIdVariable: this.tenantIdVariable,
2380
+ filename: this.filename,
2381
+ disposition,
2382
+ };
2383
+ }
2384
+ revokeBlobUrl() {
2385
+ if (this.currentBlobUrl) {
2386
+ URL.revokeObjectURL(this.currentBlobUrl);
2387
+ this.currentBlobUrl = null;
2388
+ this.previewUrl = null;
2389
+ }
2390
+ }
2391
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDocumentComponent, deps: [{ token: EpistolaPluginService }, { token: i2$4.DomSanitizer }, { token: i3.FormIoStateService }, { token: EpistolaTaskContextService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
2392
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaDocumentComponent, isStandalone: true, selector: "epistola-document-component", inputs: { value: "value", disabled: "disabled", label: "label", display: "display", documentVariable: "documentVariable", tenantIdVariable: "tenantIdVariable", filename: "filename" }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: `
2393
+ <!-- Design-time placeholder -->
2394
+ <div *ngIf="designMode" class="epistola-doc-panel">
2395
+ <div class="doc-header">
2396
+ <span>{{ label || 'Document' }}</span>
2397
+ <span class="design-tag">design mode</span>
2398
+ </div>
2399
+ <div class="doc-body design-info">
2400
+ <div class="design-section">
2401
+ <div class="design-label">Display</div>
2402
+ <div class="design-value">{{ display }}</div>
2403
+ <div class="design-label">Document variable</div>
2404
+ <div class="design-value">{{ documentVariable }}</div>
2405
+ <div class="design-label">Tenant ID variable</div>
2406
+ <div class="design-value">{{ tenantIdVariable }}</div>
2407
+ </div>
2408
+ </div>
2409
+ </div>
2410
+
2411
+ <!-- Button-only display -->
2412
+ <div *ngIf="!designMode && display === 'button'">
2413
+ <button
2414
+ type="button"
2415
+ class="btn btn-outline-primary"
2416
+ [disabled]="disabled || downloading"
2417
+ (click)="download()"
2418
+ >
2419
+ <i class="mdi mdi-download mr-1"></i>
2420
+ {{ downloading ? 'Downloading...' : label || 'Download PDF' }}
2421
+ </button>
2422
+ <span *ngIf="error" class="text-danger ml-2">{{ error }}</span>
2423
+ </div>
2424
+
2425
+ <!-- Inline / both: panel with optional download icon -->
2426
+ <div *ngIf="!designMode && display !== 'button'" class="epistola-doc-panel">
2427
+ <div class="doc-header">
2428
+ <span>{{ label || 'Document' }}</span>
2429
+ <div class="doc-controls">
2430
+ <button
2431
+ *ngIf="display === 'both'"
2432
+ type="button"
2433
+ class="doc-icon-btn"
2434
+ [disabled]="disabled || downloading"
2435
+ (click)="download()"
2436
+ [title]="downloading ? 'Downloading...' : 'Download'"
2437
+ >
2438
+ <i class="mdi mdi-download"></i>
2439
+ </button>
2440
+ <button
2441
+ type="button"
2442
+ class="doc-icon-btn"
2443
+ [disabled]="loading"
2444
+ (click)="refresh()"
2445
+ title="Refresh"
2446
+ >
2447
+ <i class="mdi mdi-refresh"></i>
2448
+ </button>
2449
+ </div>
2450
+ </div>
2451
+ <div class="doc-body">
2452
+ <div *ngIf="loading" class="doc-loading">Loading document...</div>
2453
+ <div *ngIf="error && !loading" class="doc-unavailable">
2454
+ <i class="mdi mdi-information-outline"></i>
2455
+ {{ error }}
2456
+ </div>
2457
+ <object
2458
+ *ngIf="previewUrl && !loading"
2459
+ [data]="previewUrl"
2460
+ type="application/pdf"
2461
+ class="doc-pdf"
2462
+ >
2463
+ PDF preview is not supported in this browser.
2464
+ </object>
2465
+ </div>
2466
+ </div>
2467
+ `, isInline: true, styles: [".epistola-doc-panel{border:1px solid #dee2e6;border-radius:4px;background:#f8f9fa;display:flex;flex-direction:column}.doc-header{display:flex;justify-content:space-between;align-items:center;padding:.5rem 1rem;border-bottom:1px solid #dee2e6;font-weight:700;color:#495057}.doc-controls{display:flex;gap:.25rem}.doc-icon-btn{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.25rem .5rem;font-size:.9rem;cursor:pointer}.doc-icon-btn:hover:not(:disabled){background:#e9ecef}.doc-icon-btn:disabled{opacity:.5;cursor:not-allowed}.doc-body{display:flex;flex-direction:column;min-height:500px}.doc-loading,.doc-unavailable{padding:2rem;text-align:center;color:#6c757d;font-style:italic}.doc-unavailable i{margin-right:.25rem}.doc-pdf{width:100%;flex:1;min-height:500px}.design-info{padding:1rem;min-height:auto}.design-section{margin-bottom:.5rem}.design-label{font-size:.7rem;text-transform:uppercase;color:#868e96;font-weight:600;letter-spacing:.05em}.design-value{font-family:monospace;font-size:.85rem;color:#212529;margin-bottom:.25rem}.design-tag{font-size:.7rem;font-weight:400;color:#6c757d;font-style:italic}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2027
2468
  }
2028
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDownloadComponent, decorators: [{
2469
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDocumentComponent, decorators: [{
2029
2470
  type: Component,
2030
- args: [{
2031
- standalone: true,
2032
- imports: [CommonModule],
2033
- selector: 'epistola-download-component',
2034
- template: `
2035
- <button
2036
- type="button"
2037
- class="btn btn-outline-primary"
2038
- [disabled]="disabled || downloading || !hasRequiredData()"
2039
- (click)="download()"
2040
- >
2041
- <i class="mdi mdi-download mr-1"></i>
2042
- {{ downloading ? 'Downloading...' : buttonLabel }}
2043
- </button>
2044
- <span *ngIf="error" class="text-danger ml-2">{{ error }}</span>
2045
- `,
2046
- }]
2047
- }], ctorParameters: () => [{ type: i1.HttpClient }], propDecorators: { value: [{
2471
+ args: [{ standalone: true, imports: [CommonModule], selector: 'epistola-document-component', changeDetection: ChangeDetectionStrategy.OnPush, template: `
2472
+ <!-- Design-time placeholder -->
2473
+ <div *ngIf="designMode" class="epistola-doc-panel">
2474
+ <div class="doc-header">
2475
+ <span>{{ label || 'Document' }}</span>
2476
+ <span class="design-tag">design mode</span>
2477
+ </div>
2478
+ <div class="doc-body design-info">
2479
+ <div class="design-section">
2480
+ <div class="design-label">Display</div>
2481
+ <div class="design-value">{{ display }}</div>
2482
+ <div class="design-label">Document variable</div>
2483
+ <div class="design-value">{{ documentVariable }}</div>
2484
+ <div class="design-label">Tenant ID variable</div>
2485
+ <div class="design-value">{{ tenantIdVariable }}</div>
2486
+ </div>
2487
+ </div>
2488
+ </div>
2489
+
2490
+ <!-- Button-only display -->
2491
+ <div *ngIf="!designMode && display === 'button'">
2492
+ <button
2493
+ type="button"
2494
+ class="btn btn-outline-primary"
2495
+ [disabled]="disabled || downloading"
2496
+ (click)="download()"
2497
+ >
2498
+ <i class="mdi mdi-download mr-1"></i>
2499
+ {{ downloading ? 'Downloading...' : label || 'Download PDF' }}
2500
+ </button>
2501
+ <span *ngIf="error" class="text-danger ml-2">{{ error }}</span>
2502
+ </div>
2503
+
2504
+ <!-- Inline / both: panel with optional download icon -->
2505
+ <div *ngIf="!designMode && display !== 'button'" class="epistola-doc-panel">
2506
+ <div class="doc-header">
2507
+ <span>{{ label || 'Document' }}</span>
2508
+ <div class="doc-controls">
2509
+ <button
2510
+ *ngIf="display === 'both'"
2511
+ type="button"
2512
+ class="doc-icon-btn"
2513
+ [disabled]="disabled || downloading"
2514
+ (click)="download()"
2515
+ [title]="downloading ? 'Downloading...' : 'Download'"
2516
+ >
2517
+ <i class="mdi mdi-download"></i>
2518
+ </button>
2519
+ <button
2520
+ type="button"
2521
+ class="doc-icon-btn"
2522
+ [disabled]="loading"
2523
+ (click)="refresh()"
2524
+ title="Refresh"
2525
+ >
2526
+ <i class="mdi mdi-refresh"></i>
2527
+ </button>
2528
+ </div>
2529
+ </div>
2530
+ <div class="doc-body">
2531
+ <div *ngIf="loading" class="doc-loading">Loading document...</div>
2532
+ <div *ngIf="error && !loading" class="doc-unavailable">
2533
+ <i class="mdi mdi-information-outline"></i>
2534
+ {{ error }}
2535
+ </div>
2536
+ <object
2537
+ *ngIf="previewUrl && !loading"
2538
+ [data]="previewUrl"
2539
+ type="application/pdf"
2540
+ class="doc-pdf"
2541
+ >
2542
+ PDF preview is not supported in this browser.
2543
+ </object>
2544
+ </div>
2545
+ </div>
2546
+ `, styles: [".epistola-doc-panel{border:1px solid #dee2e6;border-radius:4px;background:#f8f9fa;display:flex;flex-direction:column}.doc-header{display:flex;justify-content:space-between;align-items:center;padding:.5rem 1rem;border-bottom:1px solid #dee2e6;font-weight:700;color:#495057}.doc-controls{display:flex;gap:.25rem}.doc-icon-btn{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.25rem .5rem;font-size:.9rem;cursor:pointer}.doc-icon-btn:hover:not(:disabled){background:#e9ecef}.doc-icon-btn:disabled{opacity:.5;cursor:not-allowed}.doc-body{display:flex;flex-direction:column;min-height:500px}.doc-loading,.doc-unavailable{padding:2rem;text-align:center;color:#6c757d;font-style:italic}.doc-unavailable i{margin-right:.25rem}.doc-pdf{width:100%;flex:1;min-height:500px}.design-info{padding:1rem;min-height:auto}.design-section{margin-bottom:.5rem}.design-label{font-size:.7rem;text-transform:uppercase;color:#868e96;font-weight:600;letter-spacing:.05em}.design-value{font-family:monospace;font-size:.85rem;color:#212529;margin-bottom:.25rem}.design-tag{font-size:.7rem;font-weight:400;color:#6c757d;font-style:italic}\n"] }]
2547
+ }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$4.DomSanitizer }, { type: i3.FormIoStateService }, { type: EpistolaTaskContextService }, { type: i0.ChangeDetectorRef }], propDecorators: { value: [{
2048
2548
  type: Input
2049
2549
  }], valueChange: [{
2050
2550
  type: Output
2051
2551
  }], disabled: [{
2052
2552
  type: Input
2053
- }], filename: [{
2054
- type: Input
2055
2553
  }], label: [{
2056
2554
  type: Input
2555
+ }], display: [{
2556
+ type: Input
2557
+ }], documentVariable: [{
2558
+ type: Input
2559
+ }], tenantIdVariable: [{
2560
+ type: Input
2561
+ }], filename: [{
2562
+ type: Input
2057
2563
  }] } });
2058
2564
 
2059
2565
  class EpistolaRetryFormComponent {
2060
2566
  epistolaPluginService;
2061
2567
  formIoStateService;
2062
2568
  cdr;
2063
- http;
2064
2569
  sanitizer;
2065
- configService;
2570
+ taskContext;
2066
2571
  value;
2067
2572
  valueChange = new EventEmitter();
2068
2573
  disabled = false;
@@ -2083,19 +2588,16 @@ class EpistolaRetryFormComponent {
2083
2588
  currentBlobUrl = null;
2084
2589
  resolvedSourceActivityId;
2085
2590
  processDefinitionKey;
2086
- apiEndpoint;
2087
2591
  formOptions = {
2088
2592
  noAlerts: true,
2089
2593
  buttonSettings: { showCancel: false, showSubmit: false, showPrevious: false, showNext: false },
2090
2594
  };
2091
- constructor(epistolaPluginService, formIoStateService, cdr, http, sanitizer, configService) {
2595
+ constructor(epistolaPluginService, formIoStateService, cdr, sanitizer, taskContext) {
2092
2596
  this.epistolaPluginService = epistolaPluginService;
2093
2597
  this.formIoStateService = formIoStateService;
2094
2598
  this.cdr = cdr;
2095
- this.http = http;
2096
2599
  this.sanitizer = sanitizer;
2097
- this.configService = configService;
2098
- this.apiEndpoint = `${this.configService.config.valtimoApi.endpointUri}v1/plugin/epistola`;
2600
+ this.taskContext = taskContext;
2099
2601
  // Debounce preview calls
2100
2602
  this.previewSubscription = this.previewSubject.pipe(debounceTime$1(1500)).subscribe((data) => {
2101
2603
  this.loadPreview(data);
@@ -2132,6 +2634,12 @@ class EpistolaRetryFormComponent {
2132
2634
  const processInstanceId = this.formIoStateService.processInstanceId;
2133
2635
  if (!documentId || !processInstanceId)
2134
2636
  return;
2637
+ const taskId = this.taskContext.taskInstanceId;
2638
+ if (!taskId) {
2639
+ this.previewError = 'Preview is only available from within a user task.';
2640
+ this.cdr.markForCheck();
2641
+ return;
2642
+ }
2135
2643
  this.previewLoading = true;
2136
2644
  this.previewError = null;
2137
2645
  this.cdr.markForCheck();
@@ -2140,13 +2648,14 @@ class EpistolaRetryFormComponent {
2140
2648
  URL.revokeObjectURL(this.currentBlobUrl);
2141
2649
  this.currentBlobUrl = null;
2142
2650
  }
2143
- this.http
2144
- .post(`${this.apiEndpoint}/preview`, {
2651
+ this.epistolaPluginService
2652
+ .previewToBlob({
2653
+ taskId,
2145
2654
  documentId,
2146
2655
  processInstanceId,
2147
2656
  sourceActivityId: this.sourceActivityId || null,
2148
2657
  overrides: formData,
2149
- }, { responseType: 'blob', headers: new HttpHeaders().set('X-Skip-Interceptor', '422') })
2658
+ })
2150
2659
  .subscribe({
2151
2660
  next: (blob) => {
2152
2661
  this.currentBlobUrl = URL.createObjectURL(blob);
@@ -2188,8 +2697,15 @@ class EpistolaRetryFormComponent {
2188
2697
  this.cdr.markForCheck();
2189
2698
  return;
2190
2699
  }
2700
+ const taskId = this.taskContext.taskInstanceId;
2701
+ if (!taskId) {
2702
+ this.error = 'Retry form is only available from within a user task.';
2703
+ this.loading = false;
2704
+ this.cdr.markForCheck();
2705
+ return;
2706
+ }
2191
2707
  this.loadSubscription = this.epistolaPluginService
2192
- .getRetryForm(processInstanceId, documentId ?? undefined, this.sourceActivityId)
2708
+ .getRetryForm(taskId, processInstanceId, documentId ?? undefined, this.sourceActivityId)
2193
2709
  .subscribe({
2194
2710
  next: (form) => {
2195
2711
  this.formDefinition = form;
@@ -2212,7 +2728,7 @@ class EpistolaRetryFormComponent {
2212
2728
  },
2213
2729
  });
2214
2730
  }
2215
- 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$1.DomSanitizer }, { token: i2.ConfigService }], target: i0.ɵɵFactoryTarget.Component });
2731
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaRetryFormComponent, deps: [{ token: EpistolaPluginService }, { token: i3.FormIoStateService }, { token: i0.ChangeDetectorRef }, { token: i2$4.DomSanitizer }, { token: EpistolaTaskContextService }], target: i0.ɵɵFactoryTarget.Component });
2216
2732
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaRetryFormComponent, isStandalone: true, selector: "epistola-retry-form-component", inputs: { value: "value", disabled: "disabled", label: "label", sourceActivityId: "sourceActivityId" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
2217
2733
  <div *ngIf="loading" class="epistola-retry-loading">Loading form...</div>
2218
2734
  <div *ngIf="error" class="epistola-retry-error">{{ error }}</div>
@@ -2251,7 +2767,7 @@ class EpistolaRetryFormComponent {
2251
2767
  </div>
2252
2768
  </div>
2253
2769
  </div>
2254
- `, isInline: true, styles: [".epistola-retry-loading{padding:1rem;color:#6c757d}.epistola-retry-error{padding:.5rem;color:#dc3545}.epistola-retry-container{display:flex;gap:1rem}.epistola-retry-form{flex:2;min-width:0}.epistola-retry-preview{flex:1;min-width:0;border:1px solid #dee2e6;border-radius:4px;padding:1rem;background:#f8f9fa;display:flex;flex-direction:column}.preview-expanded .epistola-retry-preview{flex:1}.preview-header{display:flex;justify-content:space-between;align-items:center;font-weight:700;margin-bottom:.5rem;color:#495057}.preview-toggle{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.2rem .5rem;font-size:.75rem;cursor:pointer}.preview-toggle:hover{background:#e9ecef}.preview-loading{color:#6c757d;font-style:italic}.preview-pdf{width:100%;flex:1;min-height:500px}.preview-expanded .preview-pdf{min-height:80vh}.preview-error{color:#dc3545}.preview-empty{color:#6c757d;font-style:italic}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormioModule }, { kind: "component", type: i7.FormioComponent, selector: "formio" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2770
+ `, isInline: true, styles: [".epistola-retry-loading{padding:1rem;color:#6c757d}.epistola-retry-error{padding:.5rem;color:#dc3545}.epistola-retry-container{display:flex;gap:1rem}.epistola-retry-form{flex:2;min-width:0}.epistola-retry-preview{flex:1;min-width:0;border:1px solid #dee2e6;border-radius:4px;padding:1rem;background:#f8f9fa;display:flex;flex-direction:column}.preview-expanded .epistola-retry-preview{flex:1}.preview-header{display:flex;justify-content:space-between;align-items:center;font-weight:700;margin-bottom:.5rem;color:#495057}.preview-toggle{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.2rem .5rem;font-size:.75rem;cursor:pointer}.preview-toggle:hover{background:#e9ecef}.preview-loading{color:#6c757d;font-style:italic}.preview-pdf{width:100%;flex:1;min-height:500px}.preview-expanded .preview-pdf{min-height:80vh}.preview-error{color:#dc3545}.preview-empty{color:#6c757d;font-style:italic}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormioModule }, { kind: "component", type: i5.FormioComponent, selector: "formio" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2255
2771
  }
2256
2772
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaRetryFormComponent, decorators: [{
2257
2773
  type: Component,
@@ -2294,7 +2810,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2294
2810
  </div>
2295
2811
  </div>
2296
2812
  `, styles: [".epistola-retry-loading{padding:1rem;color:#6c757d}.epistola-retry-error{padding:.5rem;color:#dc3545}.epistola-retry-container{display:flex;gap:1rem}.epistola-retry-form{flex:2;min-width:0}.epistola-retry-preview{flex:1;min-width:0;border:1px solid #dee2e6;border-radius:4px;padding:1rem;background:#f8f9fa;display:flex;flex-direction:column}.preview-expanded .epistola-retry-preview{flex:1}.preview-header{display:flex;justify-content:space-between;align-items:center;font-weight:700;margin-bottom:.5rem;color:#495057}.preview-toggle{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.2rem .5rem;font-size:.75rem;cursor:pointer}.preview-toggle:hover{background:#e9ecef}.preview-loading{color:#6c757d;font-style:italic}.preview-pdf{width:100%;flex:1;min-height:500px}.preview-expanded .preview-pdf{min-height:80vh}.preview-error{color:#dc3545}.preview-empty{color:#6c757d;font-style:italic}\n"] }]
2297
- }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }, { type: i1.HttpClient }, { type: i4$1.DomSanitizer }, { type: i2.ConfigService }], propDecorators: { value: [{
2813
+ }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }, { type: i2$4.DomSanitizer }, { type: EpistolaTaskContextService }], propDecorators: { value: [{
2298
2814
  type: Input
2299
2815
  }], valueChange: [{
2300
2816
  type: Output
@@ -2306,281 +2822,156 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2306
2822
  type: Input
2307
2823
  }] } });
2308
2824
 
2309
- class EpistolaPreviewButtonComponent {
2310
- http;
2825
+ class EpistolaDocumentPreviewComponent {
2826
+ epistolaPluginService;
2311
2827
  sanitizer;
2312
- configService;
2828
+ formIoStateService;
2829
+ cdr;
2830
+ taskContext;
2313
2831
  value;
2314
2832
  valueChange = new EventEmitter();
2315
2833
  disabled = false;
2316
- label = 'Preview PDF';
2317
- modalOpen = false;
2834
+ label = 'Document Preview';
2835
+ processDefinitionKey;
2836
+ sourceActivityId;
2837
+ overrideMapping;
2318
2838
  loading = false;
2319
- previewLoading = false;
2320
- previewError = null;
2839
+ error = null;
2321
2840
  previewUrl = null;
2841
+ designMode = false;
2842
+ initialized = false;
2322
2843
  currentBlobUrl = null;
2323
- apiEndpoint;
2324
- get buttonLabel() {
2325
- return this.label || 'Preview PDF';
2326
- }
2327
- constructor(http, sanitizer, configService) {
2328
- this.http = http;
2844
+ previewSubscription;
2845
+ constructor(epistolaPluginService, sanitizer, formIoStateService, cdr, taskContext) {
2846
+ this.epistolaPluginService = epistolaPluginService;
2329
2847
  this.sanitizer = sanitizer;
2330
- this.configService = configService;
2331
- this.apiEndpoint = `${this.configService.config.valtimoApi.endpointUri}v1/plugin/epistola`;
2848
+ this.formIoStateService = formIoStateService;
2849
+ this.cdr = cdr;
2850
+ this.taskContext = taskContext;
2332
2851
  }
2333
- ngOnDestroy() {
2334
- this.revokeBlobUrl();
2852
+ /**
2853
+ * Resolve the active task id from {@link EpistolaTaskContextService}, populated
2854
+ * by {@code EpistolaTaskContextInterceptor} on the canonical Valtimo task-open
2855
+ * call. Returns null when used outside a task context (e.g. Formio builder).
2856
+ */
2857
+ get currentTaskId() {
2858
+ return this.taskContext.taskInstanceId;
2335
2859
  }
2336
- hasRequiredData() {
2337
- return !!(this.value?.documentId && this.value?.tenantId);
2860
+ get overrideMappingScopes() {
2861
+ return this.overrideMapping ? Object.keys(this.overrideMapping) : [];
2338
2862
  }
2339
- openPreview() {
2340
- if (!this.hasRequiredData() || this.loading)
2341
- return;
2342
- this.modalOpen = true;
2343
- this.previewLoading = true;
2344
- this.previewError = null;
2345
- this.revokeBlobUrl();
2346
- const { documentId, tenantId } = this.value;
2347
- const url = `${this.apiEndpoint}/documents/${encodeURIComponent(documentId)}/download` +
2348
- `?tenantId=${encodeURIComponent(tenantId)}` +
2349
- `&filename=preview.pdf`;
2350
- this.http.get(url, { responseType: 'blob' }).subscribe({
2351
- next: (blob) => {
2352
- this.currentBlobUrl = URL.createObjectURL(blob);
2353
- this.previewUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.currentBlobUrl);
2354
- this.previewLoading = false;
2355
- },
2356
- error: () => {
2357
- this.previewError = 'Could not load the document.';
2358
- this.previewLoading = false;
2359
- },
2360
- });
2361
- }
2362
- closePreview() {
2363
- this.modalOpen = false;
2364
- this.revokeBlobUrl();
2365
- this.previewUrl = null;
2366
- this.previewError = null;
2367
- }
2368
- revokeBlobUrl() {
2369
- if (this.currentBlobUrl) {
2370
- URL.revokeObjectURL(this.currentBlobUrl);
2371
- this.currentBlobUrl = null;
2372
- }
2373
- }
2374
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaPreviewButtonComponent, deps: [{ token: i1.HttpClient }, { token: i4$1.DomSanitizer }, { token: i2.ConfigService }], target: i0.ɵɵFactoryTarget.Component });
2375
- 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: `
2376
- <button
2377
- type="button"
2378
- class="btn btn-outline-secondary"
2379
- [disabled]="disabled || loading || !hasRequiredData()"
2380
- (click)="openPreview()"
2381
- >
2382
- <i class="mdi mdi-eye mr-1"></i>
2383
- {{ loading ? 'Loading...' : buttonLabel }}
2384
- </button>
2385
-
2386
- <div *ngIf="modalOpen" class="preview-modal-overlay" (click)="closePreview()">
2387
- <div class="preview-modal-content" (click)="$event.stopPropagation()">
2388
- <div class="preview-modal-header">
2389
- <span>Document Preview</span>
2390
- <button type="button" class="preview-modal-close" (click)="closePreview()">
2391
- &times;
2392
- </button>
2393
- </div>
2394
- <div class="preview-modal-body">
2395
- <div *ngIf="previewLoading" class="preview-loading">Generating preview...</div>
2396
- <div *ngIf="previewError" class="preview-error">{{ previewError }}</div>
2397
- <object
2398
- *ngIf="previewUrl && !previewLoading"
2399
- [data]="previewUrl"
2400
- type="application/pdf"
2401
- class="preview-pdf"
2402
- >
2403
- PDF preview is not supported in this browser.
2404
- </object>
2405
- </div>
2406
- </div>
2407
- </div>
2408
- `, 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"] }] });
2409
- }
2410
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaPreviewButtonComponent, decorators: [{
2411
- type: Component,
2412
- args: [{ standalone: true, imports: [CommonModule], selector: 'epistola-preview-button-component', template: `
2413
- <button
2414
- type="button"
2415
- class="btn btn-outline-secondary"
2416
- [disabled]="disabled || loading || !hasRequiredData()"
2417
- (click)="openPreview()"
2418
- >
2419
- <i class="mdi mdi-eye mr-1"></i>
2420
- {{ loading ? 'Loading...' : buttonLabel }}
2421
- </button>
2422
-
2423
- <div *ngIf="modalOpen" class="preview-modal-overlay" (click)="closePreview()">
2424
- <div class="preview-modal-content" (click)="$event.stopPropagation()">
2425
- <div class="preview-modal-header">
2426
- <span>Document Preview</span>
2427
- <button type="button" class="preview-modal-close" (click)="closePreview()">
2428
- &times;
2429
- </button>
2430
- </div>
2431
- <div class="preview-modal-body">
2432
- <div *ngIf="previewLoading" class="preview-loading">Generating preview...</div>
2433
- <div *ngIf="previewError" class="preview-error">{{ previewError }}</div>
2434
- <object
2435
- *ngIf="previewUrl && !previewLoading"
2436
- [data]="previewUrl"
2437
- type="application/pdf"
2438
- class="preview-pdf"
2439
- >
2440
- PDF preview is not supported in this browser.
2441
- </object>
2442
- </div>
2443
- </div>
2444
- </div>
2445
- `, 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"] }]
2446
- }], ctorParameters: () => [{ type: i1.HttpClient }, { type: i4$1.DomSanitizer }, { type: i2.ConfigService }], propDecorators: { value: [{
2447
- type: Input
2448
- }], valueChange: [{
2449
- type: Output
2450
- }], disabled: [{
2451
- type: Input
2452
- }], label: [{
2453
- type: Input
2454
- }] } });
2455
-
2456
- class EpistolaDocumentPreviewComponent {
2457
- epistolaPluginService;
2458
- http;
2459
- sanitizer;
2460
- configService;
2461
- formIoStateService;
2462
- cdr;
2463
- value;
2464
- valueChange = new EventEmitter();
2465
- disabled = false;
2466
- label = 'Document Preview';
2467
- sources = [];
2468
- selectedIndex = 0;
2469
- discovering = false;
2470
- loading = false;
2471
- error = null;
2472
- previewUrl = null;
2473
- initialized = false;
2474
- currentBlobUrl = null;
2475
- discoverSubscription;
2476
- previewSubscription;
2477
- apiEndpoint;
2478
- constructor(epistolaPluginService, http, sanitizer, configService, formIoStateService, cdr) {
2479
- this.epistolaPluginService = epistolaPluginService;
2480
- this.http = http;
2481
- this.sanitizer = sanitizer;
2482
- this.configService = configService;
2483
- this.formIoStateService = formIoStateService;
2484
- this.cdr = cdr;
2485
- this.apiEndpoint = `${this.configService.config.valtimoApi.endpointUri}v1/plugin/epistola`;
2863
+ overrideMappingEntries(scope) {
2864
+ const fields = this.overrideMapping?.[scope];
2865
+ if (!fields || typeof fields !== 'object')
2866
+ return [];
2867
+ return Object.entries(fields).map(([path, field]) => ({ path, field: String(field) }));
2486
2868
  }
2487
2869
  ngOnChanges(changes) {
2488
2870
  if (!this.initialized) {
2489
2871
  this.initialized = true;
2490
- this.discoverSources();
2872
+ // Detect design mode: no runtime context (Formio builder)
2873
+ const documentId = this.formIoStateService.documentId;
2874
+ if (!documentId) {
2875
+ this.designMode = true;
2876
+ this.cdr.markForCheck();
2877
+ return;
2878
+ }
2879
+ if (!this.sourceActivityId) {
2880
+ this.error = 'Preview is not configured: set the source activity on the form component.';
2881
+ this.cdr.markForCheck();
2882
+ return;
2883
+ }
2884
+ this.loadPreview();
2885
+ return;
2886
+ }
2887
+ // React to value changes (input overrides from the Formio wrapper).
2888
+ if (changes['value']) {
2889
+ this.loadPreview();
2491
2890
  }
2492
2891
  }
2493
2892
  ngOnDestroy() {
2494
- this.discoverSubscription?.unsubscribe();
2495
2893
  this.previewSubscription?.unsubscribe();
2496
2894
  this.revokeBlobUrl();
2497
2895
  }
2498
- onSourceChange(event) {
2499
- this.selectedIndex = +event.target.value;
2500
- this.loadPreview();
2501
- }
2502
2896
  refresh() {
2503
2897
  this.loadPreview();
2504
2898
  }
2505
- discoverSources() {
2899
+ /**
2900
+ * Preview using the explicitly configured process link + input overrides.
2901
+ * Requires a runtime task context — the backend authorizes the request against
2902
+ * the task's process instance and case document, so all three ids must match.
2903
+ */
2904
+ loadPreview() {
2506
2905
  const documentId = this.formIoStateService.documentId;
2507
2906
  if (!documentId) {
2508
2907
  this.error = 'Could not determine document ID from context.';
2509
2908
  this.cdr.markForCheck();
2510
2909
  return;
2511
2910
  }
2512
- this.discovering = true;
2513
- this.error = null;
2514
- this.cdr.markForCheck();
2515
- this.discoverSubscription = this.epistolaPluginService.getPreviewSources(documentId).subscribe({
2516
- next: (sources) => {
2517
- this.sources = sources;
2518
- this.discovering = false;
2519
- this.cdr.markForCheck();
2520
- if (sources.length > 0) {
2521
- this.selectedIndex = 0;
2522
- this.loadPreview();
2523
- }
2524
- },
2525
- error: (err) => {
2526
- this.error = err.error?.error || 'Failed to discover preview sources';
2527
- this.discovering = false;
2528
- this.cdr.markForCheck();
2529
- },
2530
- });
2531
- }
2532
- loadPreview() {
2533
- const source = this.sources[this.selectedIndex];
2534
- if (!source)
2911
+ if (!this.sourceActivityId) {
2912
+ this.error = 'Preview is not configured: set the source activity on the form component.';
2913
+ this.cdr.markForCheck();
2535
2914
  return;
2536
- const documentId = this.formIoStateService.documentId;
2537
- if (!documentId)
2915
+ }
2916
+ const processInstanceId = this.formIoStateService.processInstanceId;
2917
+ if (!processInstanceId) {
2918
+ this.error = 'Preview is only available from within a running process.';
2919
+ this.cdr.markForCheck();
2920
+ return;
2921
+ }
2922
+ const taskId = this.currentTaskId;
2923
+ if (!taskId) {
2924
+ this.error = 'Preview is only available from within a user task.';
2925
+ this.cdr.markForCheck();
2538
2926
  return;
2927
+ }
2539
2928
  this.loading = true;
2540
2929
  this.error = null;
2541
2930
  this.cdr.markForCheck();
2542
2931
  this.revokeBlobUrl();
2543
2932
  this.previewSubscription?.unsubscribe();
2544
- this.previewSubscription = this.http
2545
- .post(`${this.apiEndpoint}/preview`, {
2933
+ this.previewSubscription = this.epistolaPluginService
2934
+ .previewToBlob({
2935
+ taskId,
2546
2936
  documentId,
2547
- processInstanceId: source.processInstanceId,
2548
- sourceActivityId: source.activityId,
2937
+ processDefinitionKey: this.processDefinitionKey || null,
2938
+ processInstanceId,
2939
+ sourceActivityId: this.sourceActivityId,
2940
+ inputOverrides: this.value || null,
2549
2941
  overrides: null,
2550
- }, {
2551
- responseType: 'blob',
2552
- headers: new HttpHeaders().set('X-Skip-Interceptor', '422'),
2553
2942
  })
2554
2943
  .subscribe({
2555
- next: (blob) => {
2556
- this.currentBlobUrl = URL.createObjectURL(blob);
2557
- this.previewUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.currentBlobUrl);
2558
- this.error = null;
2559
- this.loading = false;
2560
- this.cdr.markForCheck();
2561
- },
2562
- error: (err) => {
2563
- this.previewUrl = null;
2564
- if (err.error instanceof Blob) {
2565
- err.error.text().then((text) => {
2566
- try {
2567
- const body = JSON.parse(text);
2568
- this.error = body.details || body.error || 'Preview could not be generated';
2569
- }
2570
- catch {
2571
- this.error = 'Preview could not be generated';
2572
- }
2573
- this.loading = false;
2574
- this.cdr.markForCheck();
2575
- });
2944
+ next: (blob) => this.handlePreviewSuccess(blob),
2945
+ error: (err) => this.handlePreviewError(err),
2946
+ });
2947
+ }
2948
+ handlePreviewSuccess(blob) {
2949
+ this.currentBlobUrl = URL.createObjectURL(blob);
2950
+ this.previewUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.currentBlobUrl);
2951
+ this.error = null;
2952
+ this.loading = false;
2953
+ this.cdr.markForCheck();
2954
+ }
2955
+ handlePreviewError(err) {
2956
+ this.previewUrl = null;
2957
+ if (err.error instanceof Blob) {
2958
+ err.error.text().then((text) => {
2959
+ try {
2960
+ const body = JSON.parse(text);
2961
+ this.error = body.details || body.error || 'Preview could not be generated';
2576
2962
  }
2577
- else {
2578
- this.error = err.error?.error || 'Preview could not be generated';
2579
- this.loading = false;
2580
- this.cdr.markForCheck();
2963
+ catch {
2964
+ this.error = 'Preview could not be generated';
2581
2965
  }
2582
- },
2583
- });
2966
+ this.loading = false;
2967
+ this.cdr.markForCheck();
2968
+ });
2969
+ }
2970
+ else {
2971
+ this.error = err.error?.error || 'Preview could not be generated';
2972
+ this.loading = false;
2973
+ this.cdr.markForCheck();
2974
+ }
2584
2975
  }
2585
2976
  revokeBlobUrl() {
2586
2977
  if (this.currentBlobUrl) {
@@ -2589,111 +2980,127 @@ class EpistolaDocumentPreviewComponent {
2589
2980
  this.previewUrl = null;
2590
2981
  }
2591
2982
  }
2592
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDocumentPreviewComponent, deps: [{ token: EpistolaPluginService }, { token: i1.HttpClient }, { token: i4$1.DomSanitizer }, { token: i2.ConfigService }, { token: i3.FormIoStateService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
2593
- 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" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
2594
- <div class="epistola-preview-panel">
2983
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDocumentPreviewComponent, deps: [{ token: EpistolaPluginService }, { token: i2$4.DomSanitizer }, { token: i3.FormIoStateService }, { token: i0.ChangeDetectorRef }, { token: EpistolaTaskContextService }], target: i0.ɵɵFactoryTarget.Component });
2984
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaDocumentPreviewComponent, isStandalone: true, selector: "epistola-document-preview-component", inputs: { value: "value", disabled: "disabled", label: "label", processDefinitionKey: "processDefinitionKey", sourceActivityId: "sourceActivityId", overrideMapping: "overrideMapping" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
2985
+ <!-- Design-time view: show configuration summary when no runtime context -->
2986
+ <div *ngIf="designMode" class="epistola-preview-panel">
2987
+ <div class="preview-header">
2988
+ <span>{{ label || 'Document Preview' }}</span>
2989
+ </div>
2990
+ <div class="preview-body design-info">
2991
+ <div class="design-section" *ngIf="sourceActivityId">
2992
+ <div class="design-label">Process</div>
2993
+ <div class="design-value">{{ processDefinitionKey || '(any)' }}</div>
2994
+ <div class="design-label">Activity</div>
2995
+ <div class="design-value">{{ sourceActivityId }}</div>
2996
+ </div>
2997
+ <div class="design-section" *ngIf="overrideMapping">
2998
+ <div class="design-label">Input Overrides</div>
2999
+ <div *ngFor="let scope of overrideMappingScopes" class="design-mapping">
3000
+ <div *ngFor="let entry of overrideMappingEntries(scope)" class="design-entry">
3001
+ <span class="design-scope">{{ scope }}</span
3002
+ >.{{ entry.path }}
3003
+ <i class="mdi mdi-arrow-left"></i>
3004
+ <span class="design-field">{{ entry.field }}</span>
3005
+ </div>
3006
+ </div>
3007
+ </div>
3008
+ <div *ngIf="!sourceActivityId" class="design-unconfigured">
3009
+ Auto-discover mode (no process link configured)
3010
+ </div>
3011
+ </div>
3012
+ </div>
3013
+
3014
+ <!-- Runtime view: actual preview -->
3015
+ <div *ngIf="!designMode" class="epistola-preview-panel">
2595
3016
  <div class="preview-header">
2596
3017
  <span>{{ label || 'Document Preview' }}</span>
2597
3018
  <div class="preview-controls">
2598
- <select
2599
- *ngIf="sources.length > 1"
2600
- class="preview-select"
2601
- [value]="selectedIndex"
2602
- (change)="onSourceChange($event)"
2603
- >
2604
- <option *ngFor="let source of sources; let i = index" [value]="i">
2605
- {{ source.templateName }} ({{ source.activityId }})
2606
- </option>
2607
- </select>
2608
- <button
2609
- type="button"
2610
- class="preview-refresh"
2611
- [disabled]="loading || discovering"
2612
- (click)="refresh()"
2613
- >
3019
+ <button type="button" class="preview-refresh" [disabled]="loading" (click)="refresh()">
2614
3020
  <i class="mdi mdi-refresh mr-1"></i>
2615
3021
  {{ loading ? 'Generating...' : 'Refresh' }}
2616
3022
  </button>
2617
3023
  </div>
2618
3024
  </div>
2619
3025
  <div class="preview-body">
2620
- <div *ngIf="discovering" class="preview-loading">Discovering documents...</div>
2621
- <div *ngIf="loading && !discovering" class="preview-loading">Generating preview...</div>
2622
- <div *ngIf="error && !loading && !discovering" class="preview-unavailable">
3026
+ <div *ngIf="loading" class="preview-loading">Generating preview...</div>
3027
+ <div *ngIf="error && !loading" class="preview-unavailable">
2623
3028
  <i class="mdi mdi-information-outline"></i>
2624
- Preview is niet beschikbaar — niet alle gegevens zijn al ingevuld.
3029
+ {{ error }}
2625
3030
  </div>
2626
3031
  <object
2627
- *ngIf="previewUrl && !loading && !discovering"
3032
+ *ngIf="previewUrl && !loading"
2628
3033
  [data]="previewUrl"
2629
3034
  type="application/pdf"
2630
3035
  class="preview-pdf"
2631
3036
  >
2632
3037
  PDF preview is not supported in this browser.
2633
3038
  </object>
2634
- <div
2635
- *ngIf="!previewUrl && !loading && !discovering && !error && sources.length === 0"
2636
- class="preview-empty"
2637
- >
2638
- No previewable documents found
2639
- </div>
2640
3039
  </div>
2641
3040
  </div>
2642
- `, 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}\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 });
3041
+ `, isInline: true, styles: [".epistola-preview-panel{border:1px solid #dee2e6;border-radius:4px;background:#f8f9fa;display:flex;flex-direction:column}.preview-header{display:flex;justify-content:space-between;align-items:center;padding:.5rem 1rem;border-bottom:1px solid #dee2e6;font-weight:700;color:#495057;flex-wrap:wrap;gap:.5rem}.preview-controls{display:flex;align-items:center;gap:.5rem}.preview-select{border:1px solid #ced4da;border-radius:4px;padding:.25rem .5rem;font-size:.8rem;background:#fff;max-width:300px}.preview-refresh{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.25rem .75rem;font-size:.8rem;cursor:pointer;display:flex;align-items:center;white-space:nowrap}.preview-refresh:hover:not(:disabled){background:#e9ecef}.preview-refresh:disabled{opacity:.5;cursor:not-allowed}.preview-body{display:flex;flex-direction:column;min-height:500px}.preview-loading{padding:2rem;text-align:center;color:#6c757d;font-style:italic}.preview-unavailable{padding:1.5rem;text-align:center;color:#6c757d;font-style:italic}.preview-unavailable i{margin-right:.25rem}.preview-pdf{width:100%;flex:1;min-height:500px}.preview-empty{padding:2rem;text-align:center;color:#6c757d;font-style:italic}.design-info{padding:1rem;min-height:auto}.design-section{margin-bottom:.75rem}.design-label{font-size:.7rem;text-transform:uppercase;color:#868e96;font-weight:600;letter-spacing:.05em}.design-value{font-family:monospace;font-size:.85rem;color:#212529;margin-bottom:.25rem}.design-mapping{margin-top:.25rem}.design-entry{font-family:monospace;font-size:.8rem;color:#495057;padding:.15rem 0}.design-scope{color:#0d6efd}.design-field{color:#198754}.design-entry i{font-size:.7rem;margin:0 .25rem;color:#adb5bd}.design-unconfigured{color:#6c757d;font-style:italic;font-size:.85rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2643
3042
  }
2644
3043
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDocumentPreviewComponent, decorators: [{
2645
3044
  type: Component,
2646
3045
  args: [{ standalone: true, imports: [CommonModule], selector: 'epistola-document-preview-component', changeDetection: ChangeDetectionStrategy.OnPush, template: `
2647
- <div class="epistola-preview-panel">
3046
+ <!-- Design-time view: show configuration summary when no runtime context -->
3047
+ <div *ngIf="designMode" class="epistola-preview-panel">
3048
+ <div class="preview-header">
3049
+ <span>{{ label || 'Document Preview' }}</span>
3050
+ </div>
3051
+ <div class="preview-body design-info">
3052
+ <div class="design-section" *ngIf="sourceActivityId">
3053
+ <div class="design-label">Process</div>
3054
+ <div class="design-value">{{ processDefinitionKey || '(any)' }}</div>
3055
+ <div class="design-label">Activity</div>
3056
+ <div class="design-value">{{ sourceActivityId }}</div>
3057
+ </div>
3058
+ <div class="design-section" *ngIf="overrideMapping">
3059
+ <div class="design-label">Input Overrides</div>
3060
+ <div *ngFor="let scope of overrideMappingScopes" class="design-mapping">
3061
+ <div *ngFor="let entry of overrideMappingEntries(scope)" class="design-entry">
3062
+ <span class="design-scope">{{ scope }}</span
3063
+ >.{{ entry.path }}
3064
+ <i class="mdi mdi-arrow-left"></i>
3065
+ <span class="design-field">{{ entry.field }}</span>
3066
+ </div>
3067
+ </div>
3068
+ </div>
3069
+ <div *ngIf="!sourceActivityId" class="design-unconfigured">
3070
+ Auto-discover mode (no process link configured)
3071
+ </div>
3072
+ </div>
3073
+ </div>
3074
+
3075
+ <!-- Runtime view: actual preview -->
3076
+ <div *ngIf="!designMode" class="epistola-preview-panel">
2648
3077
  <div class="preview-header">
2649
3078
  <span>{{ label || 'Document Preview' }}</span>
2650
3079
  <div class="preview-controls">
2651
- <select
2652
- *ngIf="sources.length > 1"
2653
- class="preview-select"
2654
- [value]="selectedIndex"
2655
- (change)="onSourceChange($event)"
2656
- >
2657
- <option *ngFor="let source of sources; let i = index" [value]="i">
2658
- {{ source.templateName }} ({{ source.activityId }})
2659
- </option>
2660
- </select>
2661
- <button
2662
- type="button"
2663
- class="preview-refresh"
2664
- [disabled]="loading || discovering"
2665
- (click)="refresh()"
2666
- >
3080
+ <button type="button" class="preview-refresh" [disabled]="loading" (click)="refresh()">
2667
3081
  <i class="mdi mdi-refresh mr-1"></i>
2668
3082
  {{ loading ? 'Generating...' : 'Refresh' }}
2669
3083
  </button>
2670
3084
  </div>
2671
3085
  </div>
2672
3086
  <div class="preview-body">
2673
- <div *ngIf="discovering" class="preview-loading">Discovering documents...</div>
2674
- <div *ngIf="loading && !discovering" class="preview-loading">Generating preview...</div>
2675
- <div *ngIf="error && !loading && !discovering" class="preview-unavailable">
3087
+ <div *ngIf="loading" class="preview-loading">Generating preview...</div>
3088
+ <div *ngIf="error && !loading" class="preview-unavailable">
2676
3089
  <i class="mdi mdi-information-outline"></i>
2677
- Preview is niet beschikbaar — niet alle gegevens zijn al ingevuld.
3090
+ {{ error }}
2678
3091
  </div>
2679
3092
  <object
2680
- *ngIf="previewUrl && !loading && !discovering"
3093
+ *ngIf="previewUrl && !loading"
2681
3094
  [data]="previewUrl"
2682
3095
  type="application/pdf"
2683
3096
  class="preview-pdf"
2684
3097
  >
2685
3098
  PDF preview is not supported in this browser.
2686
3099
  </object>
2687
- <div
2688
- *ngIf="!previewUrl && !loading && !discovering && !error && sources.length === 0"
2689
- class="preview-empty"
2690
- >
2691
- No previewable documents found
2692
- </div>
2693
3100
  </div>
2694
3101
  </div>
2695
- `, 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}\n"] }]
2696
- }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i1.HttpClient }, { type: i4$1.DomSanitizer }, { type: i2.ConfigService }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }], propDecorators: { value: [{
3102
+ `, styles: [".epistola-preview-panel{border:1px solid #dee2e6;border-radius:4px;background:#f8f9fa;display:flex;flex-direction:column}.preview-header{display:flex;justify-content:space-between;align-items:center;padding:.5rem 1rem;border-bottom:1px solid #dee2e6;font-weight:700;color:#495057;flex-wrap:wrap;gap:.5rem}.preview-controls{display:flex;align-items:center;gap:.5rem}.preview-select{border:1px solid #ced4da;border-radius:4px;padding:.25rem .5rem;font-size:.8rem;background:#fff;max-width:300px}.preview-refresh{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.25rem .75rem;font-size:.8rem;cursor:pointer;display:flex;align-items:center;white-space:nowrap}.preview-refresh:hover:not(:disabled){background:#e9ecef}.preview-refresh:disabled{opacity:.5;cursor:not-allowed}.preview-body{display:flex;flex-direction:column;min-height:500px}.preview-loading{padding:2rem;text-align:center;color:#6c757d;font-style:italic}.preview-unavailable{padding:1.5rem;text-align:center;color:#6c757d;font-style:italic}.preview-unavailable i{margin-right:.25rem}.preview-pdf{width:100%;flex:1;min-height:500px}.preview-empty{padding:2rem;text-align:center;color:#6c757d;font-style:italic}.design-info{padding:1rem;min-height:auto}.design-section{margin-bottom:.75rem}.design-label{font-size:.7rem;text-transform:uppercase;color:#868e96;font-weight:600;letter-spacing:.05em}.design-value{font-family:monospace;font-size:.85rem;color:#212529;margin-bottom:.25rem}.design-mapping{margin-top:.25rem}.design-entry{font-family:monospace;font-size:.8rem;color:#495057;padding:.15rem 0}.design-scope{color:#0d6efd}.design-field{color:#198754}.design-entry i{font-size:.7rem;margin:0 .25rem;color:#adb5bd}.design-unconfigured{color:#6c757d;font-style:italic;font-size:.85rem}\n"] }]
3103
+ }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$4.DomSanitizer }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }, { type: EpistolaTaskContextService }], propDecorators: { value: [{
2697
3104
  type: Input
2698
3105
  }], valueChange: [{
2699
3106
  type: Output
@@ -2701,6 +3108,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2701
3108
  type: Input
2702
3109
  }], label: [{
2703
3110
  type: Input
3111
+ }], processDefinitionKey: [{
3112
+ type: Input
3113
+ }], sourceActivityId: [{
3114
+ type: Input
3115
+ }], overrideMapping: [{
3116
+ type: Input
2704
3117
  }] } });
2705
3118
 
2706
3119
  class EpistolaAdminPageComponent {
@@ -2710,8 +3123,12 @@ class EpistolaAdminPageComponent {
2710
3123
  cards = [];
2711
3124
  selectedCard = null;
2712
3125
  activeTab = 'actions';
3126
+ overviewTab = 'configurations';
2713
3127
  loading = false;
2714
3128
  pluginVersion = null;
3129
+ validationViolations = [];
3130
+ reconcilingExecutionIds = new Set();
3131
+ reconcileFeedback = null;
2715
3132
  connectionStatuses = [];
2716
3133
  usageEntries = [];
2717
3134
  pendingJobs = [];
@@ -2747,6 +3164,9 @@ class EpistolaAdminPageComponent {
2747
3164
  this.activeTab = tab;
2748
3165
  this.updateUrl(this.selectedCard?.configurationId ?? null, tab);
2749
3166
  }
3167
+ setOverviewTab(tab) {
3168
+ this.overviewTab = tab;
3169
+ }
2750
3170
  refresh() {
2751
3171
  this.selectedCard = null;
2752
3172
  this.loadData();
@@ -2763,6 +3183,53 @@ class EpistolaAdminPageComponent {
2763
3183
  },
2764
3184
  });
2765
3185
  }
3186
+ /**
3187
+ * Manually reconcile a single stuck catch event. Pulls the current Epistola job
3188
+ * status and re-runs message correlation if the job is in a terminal state.
3189
+ * Refreshes the Pending list on success so the row drops out of the table.
3190
+ */
3191
+ reconcilePending(job) {
3192
+ if (this.reconcilingExecutionIds.has(job.executionId)) {
3193
+ return;
3194
+ }
3195
+ this.reconcilingExecutionIds.add(job.executionId);
3196
+ this.reconcileFeedback = null;
3197
+ this.adminService.reconcilePending(job.executionId).subscribe({
3198
+ next: (result) => {
3199
+ this.reconcilingExecutionIds.delete(job.executionId);
3200
+ this.reconcileFeedback = {
3201
+ executionId: job.executionId,
3202
+ type: 'success',
3203
+ message: `OK (${result.epistolaStatus}, ${result.correlatedCount ?? 0} correlated)`,
3204
+ };
3205
+ this.loadData();
3206
+ },
3207
+ error: (err) => {
3208
+ this.reconcilingExecutionIds.delete(job.executionId);
3209
+ // 409 from the backend = job is still PENDING/IN_PROGRESS, surface as
3210
+ // informational rather than an error.
3211
+ if (err?.status === 409) {
3212
+ const status = err.error?.epistolaStatus ?? 'still pending';
3213
+ this.reconcileFeedback = {
3214
+ executionId: job.executionId,
3215
+ type: 'pending',
3216
+ message: `Epistola: ${status}. Try again in a moment.`,
3217
+ };
3218
+ }
3219
+ else {
3220
+ const message = err?.error?.detail ?? err?.error?.message ?? err?.message ?? 'unknown error';
3221
+ this.reconcileFeedback = {
3222
+ executionId: job.executionId,
3223
+ type: 'error',
3224
+ message,
3225
+ };
3226
+ }
3227
+ },
3228
+ });
3229
+ }
3230
+ isReconciling(job) {
3231
+ return this.reconcilingExecutionIds.has(job.executionId);
3232
+ }
2766
3233
  updateUrl(configurationId, tab) {
2767
3234
  this.router.navigate([], {
2768
3235
  relativeTo: this.route,
@@ -2814,6 +3281,16 @@ class EpistolaAdminPageComponent {
2814
3281
  this.tryBuildCards();
2815
3282
  },
2816
3283
  });
3284
+ // Validation violations are independent of cards — load alongside but don't
3285
+ // gate the loading flag on them.
3286
+ this.adminService.getValidationViolations().subscribe({
3287
+ next: (violations) => {
3288
+ this.validationViolations = violations;
3289
+ },
3290
+ error: () => {
3291
+ this.validationViolations = [];
3292
+ },
3293
+ });
2817
3294
  }
2818
3295
  tryBuildCards() {
2819
3296
  if (!this.connectionLoaded || !this.usageLoaded || !this.pendingLoaded) {
@@ -2854,25 +3331,60 @@ class EpistolaAdminPageComponent {
2854
3331
  },
2855
3332
  });
2856
3333
  }
2857
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminPageComponent, deps: [{ token: EpistolaAdminService }, { token: i2$3.ActivatedRoute }, { token: i2$3.Router }], target: i0.ɵɵFactoryTarget.Component });
2858
- 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$3.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: TabsModule }, { kind: "component", type: i5.Tabs, selector: "cds-tabs, ibm-tabs", inputs: ["position", "cacheActive", "followFocus", "isNavigation", "ariaLabel", "ariaLabelledby", "type", "theme", "skeleton"] }, { kind: "component", type: i5.Tab, selector: "cds-tab, ibm-tab", inputs: ["heading", "title", "context", "active", "disabled", "tabIndex", "id", "cacheActive", "tabContent", "templateContext"], outputs: ["selected"] }, { kind: "ngmodule", type: TagModule }, { kind: "component", type: i6.Tag, selector: "cds-tag, ibm-tag", inputs: ["type", "size", "class", "skeleton"] }] });
3334
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminPageComponent, deps: [{ token: EpistolaAdminService }, { token: i2$5.ActivatedRoute }, { token: i2$5.Router }], target: i0.ɵɵFactoryTarget.Component });
3335
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaAdminPageComponent, isStandalone: true, selector: "epistola-admin-page", ngImport: i0, template: "<div class=\"epistola-admin\">\n <!-- Overview: tabs (no configuration selected) -->\n <ng-container *ngIf=\"!selectedCard\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"d-flex align-items-center\">\n <h5 class=\"mb-0\">{{ 'epistolaAdminOverview' | pluginTranslate: 'epistola' | async }}</h5>\n <span *ngIf=\"pluginVersion\" class=\"version-badge ms-2\">v{{ pluginVersion }}</span>\n </div>\n <button class=\"btn btn-outline-primary btn-sm\" (click)=\"refresh()\" [disabled]=\"loading\">\n {{ 'epistolaAdminRefresh' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <ng-template #configurationsHeading>\n {{ 'epistolaAdminConfigurations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ cards.length }}</cds-tag>\n </ng-template>\n\n <ng-template #validationsHeading>\n {{ 'epistolaAdminValidations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" [type]=\"validationViolations.length > 0 ? 'red' : 'gray'\" class=\"ms-1\">\n {{ validationViolations.length }}\n </cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"configurationsHeading\"\n [active]=\"overviewTab === 'configurations'\"\n (selected)=\"setOverviewTab('configurations')\"\n >\n <div *ngIf=\"loading\" class=\"text-muted mt-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length === 0\" class=\"text-muted mt-3\">\n {{ 'epistolaAdminNoConfigurations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length > 0\" class=\"card-grid\">\n <div\n *ngFor=\"let card of cards\"\n class=\"config-card\"\n [class.config-card--ok]=\"card.reachable && card.problemCount === 0\"\n [class.config-card--warning]=\"card.reachable && card.problemCount > 0\"\n [class.config-card--error]=\"!card.reachable\"\n (click)=\"selectConfiguration(card)\"\n >\n <div class=\"config-card__header\">\n <span\n class=\"status-dot\"\n [class.status-dot--ok]=\"card.reachable\"\n [class.status-dot--error]=\"!card.reachable\"\n >\n </span>\n <h5 class=\"config-card__title\">{{ card.configurationTitle }}</h5>\n </div>\n\n <div class=\"config-card__body\">\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async\n }}</span>\n <code class=\"config-card__value\">{{ card.tenantId }}</code>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" [type]=\"card.reachable ? 'green' : 'red'\">\n {{\n card.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n </div>\n <div *ngIf=\"card.serverVersion\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.serverVersion }}</span>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.usageCount }}</span>\n </div>\n <div *ngIf=\"card.pendingJobs.length > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">{{ card.pendingJobs.length }}</cds-tag>\n </div>\n <div *ngIf=\"card.problemCount > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"red\">{{ card.problemCount }}</cds-tag>\n </div>\n </div>\n\n <div class=\"config-card__footer\">\n <span class=\"config-card__latency\">{{ card.latencyMs }} ms</span>\n </div>\n </div>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"validationsHeading\"\n [active]=\"overviewTab === 'validations'\"\n (selected)=\"setOverviewTab('validations')\"\n >\n <div *ngIf=\"validationViolations.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoValidations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"validationViolations.length > 0\" class=\"mt-3\">\n <p class=\"text-muted mb-3\">\n {{ 'epistolaAdminValidationWarningBody' | pluginTranslate: 'epistola' | async }}\n </p>\n <table class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminValidationCode' | pluginTranslate: 'epistola' | async }}</th>\n <th>\n {{ 'epistolaAdminValidationMessage' | pluginTranslate: 'epistola' | async }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let v of validationViolations\">\n <td>\n {{ v.processDefinitionName || v.processDefinitionKey }}\n <div *ngIf=\"v.processDefinitionName\" class=\"text-muted small\">\n <code>{{ v.processDefinitionKey }}</code>\n </div>\n </td>\n <td>\n <code>{{ v.activityId }}</code>\n </td>\n <td>\n <cds-tag size=\"sm\" type=\"red\">{{ v.code }}</cds-tag>\n </td>\n <td>{{ v.message }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n\n <!-- Detail view: selected configuration -->\n <ng-container *ngIf=\"selectedCard\">\n <div class=\"detail-header mb-3\">\n <button class=\"btn btn-link btn-sm p-0\" (click)=\"backToOverview()\">\n &larr; {{ 'epistolaAdminBackToOverview' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div class=\"detail-summary mb-4\">\n <h4>\n <span\n class=\"status-dot me-2\"\n [class.status-dot--ok]=\"selectedCard.reachable\"\n [class.status-dot--error]=\"!selectedCard.reachable\"\n >\n </span>\n {{ selectedCard.configurationTitle }}\n </h4>\n\n <table class=\"table table-sm detail-info-table\">\n <tbody>\n <tr>\n <th>{{ 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <code>{{ selectedCard.tenantId }}</code>\n </td>\n </tr>\n <tr>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <cds-tag size=\"sm\" [type]=\"selectedCard.reachable ? 'green' : 'red'\">\n {{\n selectedCard.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n <span class=\"text-muted ms-2\">{{ selectedCard.latencyMs }} ms</span>\n </td>\n </tr>\n <tr *ngIf=\"selectedCard.serverVersion\">\n <th>{{ 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async }}</th>\n <td>{{ selectedCard.serverVersion }}</td>\n </tr>\n <tr *ngIf=\"selectedCard.errorMessage\">\n <th>{{ 'epistolaAdminError' | pluginTranslate: 'epistola' | async }}</th>\n <td class=\"text-danger\">{{ selectedCard.errorMessage }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Tabs -->\n <ng-template #actionsHeading>\n {{ 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ selectedCard.usageEntries.length }}</cds-tag>\n </ng-template>\n\n <ng-template #pendingHeading>\n {{ 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n size=\"sm\"\n [type]=\"selectedCard.pendingJobs.length > 0 ? 'blue' : 'gray'\"\n class=\"ms-1\"\n >\n {{ selectedCard.pendingJobs.length }}\n </cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"actionsHeading\"\n [active]=\"activeTab === 'actions'\"\n (selected)=\"setActiveTab('actions')\"\n >\n <div *ngIf=\"selectedCard.usageEntries.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoUsageForConfig' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.usageEntries.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCase' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminAction' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let entry of selectedCard.usageEntries\"\n [class.table-warning]=\"entry.problems.length > 0\"\n >\n <td>{{ entry.caseDefinitionKey || '-' }}</td>\n <td>\n <a\n *ngIf=\"entry.caseDefinitionKey && entry.caseDefinitionVersionTag\"\n [routerLink]=\"[\n '/case-management',\n 'case',\n entry.caseDefinitionKey,\n 'version',\n entry.caseDefinitionVersionTag,\n 'processes',\n entry.processDefinitionKey,\n ]\"\n class=\"usage-link\"\n >\n {{ entry.processDefinitionName }}\n </a>\n <span *ngIf=\"!entry.caseDefinitionKey || !entry.caseDefinitionVersionTag\">\n {{ entry.processDefinitionName }}\n </span>\n </td>\n <td>{{ entry.activityName }}</td>\n <td>\n <code>{{ entry.actionKey }}</code>\n </td>\n <td>\n <cds-tag *ngIf=\"entry.problems.length === 0\" size=\"sm\" type=\"green\">OK</cds-tag>\n <cds-tag\n *ngFor=\"let problem of entry.problems\"\n size=\"sm\"\n type=\"red\"\n class=\"d-block mb-1\"\n >\n {{ problem }}\n </cds-tag>\n </td>\n <td>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"exportProcessLink(entry)\"\n [title]=\"'epistolaAdminExport' | pluginTranslate: 'epistola' | async\"\n >\n &#x2913;\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"pendingHeading\"\n [active]=\"activeTab === 'pending'\"\n (selected)=\"setActiveTab('pending')\"\n >\n <div *ngIf=\"selectedCard.pendingJobs.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoPendingJobs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.pendingJobs.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminRequestId' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let job of selectedCard.pendingJobs\">\n <td>{{ job.processDefinitionName }}</td>\n <td>{{ job.activityName }}</td>\n <td>\n <code>{{ job.requestId }}</code>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"reconcilePending(job)\"\n [disabled]=\"isReconciling(job)\"\n [title]=\"'epistolaAdminReconcileTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isReconciling(job)\">\n {{ 'epistolaAdminReconcile' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isReconciling(job)\">\n {{ 'epistolaAdminReconciling' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"reconcileFeedback && reconcileFeedback.executionId === job.executionId\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"reconcileFeedback.type === 'success'\"\n [class.text-warning]=\"reconcileFeedback.type === 'pending'\"\n [class.text-danger]=\"reconcileFeedback.type === 'error'\"\n >\n {{ reconcileFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n</div>\n", styles: [".epistola-admin{padding:1.5rem}.epistola-admin .version-badge{font-size:.75rem;font-weight:500;padding:.2em .6em;border-radius:4px;background-color:#e8e8e8;color:#525252}.epistola-admin .badge{font-size:.85em;padding:.35em .65em}.epistola-admin code{font-size:.9em;color:#525252}.epistola-admin table th{font-weight:600;white-space:nowrap}.epistola-admin .status-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:.5rem;flex-shrink:0;background-color:#adb5bd}.epistola-admin .status-dot--ok{background-color:#198754}.epistola-admin .status-dot--error{background-color:#dc3545}.epistola-admin .card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.epistola-admin .config-card{border:1px solid #dee2e6;border-radius:8px;padding:1.25rem;cursor:pointer;transition:box-shadow .15s ease,border-color .15s ease;background:#fff}.epistola-admin .config-card:hover{box-shadow:0 2px 8px #0000001a}.epistola-admin .config-card--ok{border-left:4px solid #198754}.epistola-admin .config-card--warning{border-left:4px solid #ffc107}.epistola-admin .config-card--error{border-left:4px solid #dc3545}.epistola-admin .config-card__header{display:flex;align-items:center;margin-bottom:1rem}.epistola-admin .config-card__title{margin:0;font-size:1.05rem;font-weight:600;color:#161616}.epistola-admin .config-card__body{display:flex;flex-direction:column;gap:.5rem}.epistola-admin .config-card__field{display:flex;justify-content:space-between;align-items:center}.epistola-admin .config-card__label{font-size:.875rem;color:#6c757d}.epistola-admin .config-card__value{font-size:.875rem;color:#161616}.epistola-admin .config-card__footer{margin-top:1rem;padding-top:.75rem;border-top:1px solid #f0f0f0;text-align:right}.epistola-admin .config-card__latency{font-size:.8rem;color:#adb5bd}.epistola-admin .usage-link{color:#0f62fe;text-decoration:none}.epistola-admin .usage-link:hover{text-decoration:underline}.epistola-admin .detail-info-table{max-width:500px}.epistola-admin .detail-info-table th{width:140px}.epistola-admin .detail-summary{padding:1rem 0;border-bottom:1px solid #dee2e6}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2$5.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: TabsModule }, { kind: "component", type: i5$1.Tabs, selector: "cds-tabs, ibm-tabs", inputs: ["position", "cacheActive", "followFocus", "isNavigation", "ariaLabel", "ariaLabelledby", "type", "theme", "skeleton"] }, { kind: "component", type: i5$1.Tab, selector: "cds-tab, ibm-tab", inputs: ["heading", "title", "context", "active", "disabled", "tabIndex", "id", "cacheActive", "tabContent", "templateContext"], outputs: ["selected"] }, { kind: "ngmodule", type: TagModule }, { kind: "component", type: i6.Tag, selector: "cds-tag, ibm-tag", inputs: ["type", "size", "class", "skeleton"] }] });
2859
3336
  }
2860
3337
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminPageComponent, decorators: [{
2861
3338
  type: Component,
2862
- 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"] }]
2863
- }], ctorParameters: () => [{ type: EpistolaAdminService }, { type: i2$3.ActivatedRoute }, { type: i2$3.Router }] });
3339
+ args: [{ selector: 'epistola-admin-page', standalone: true, imports: [CommonModule, RouterModule, PluginTranslatePipeModule, TabsModule, TagModule], template: "<div class=\"epistola-admin\">\n <!-- Overview: tabs (no configuration selected) -->\n <ng-container *ngIf=\"!selectedCard\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"d-flex align-items-center\">\n <h5 class=\"mb-0\">{{ 'epistolaAdminOverview' | pluginTranslate: 'epistola' | async }}</h5>\n <span *ngIf=\"pluginVersion\" class=\"version-badge ms-2\">v{{ pluginVersion }}</span>\n </div>\n <button class=\"btn btn-outline-primary btn-sm\" (click)=\"refresh()\" [disabled]=\"loading\">\n {{ 'epistolaAdminRefresh' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <ng-template #configurationsHeading>\n {{ 'epistolaAdminConfigurations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ cards.length }}</cds-tag>\n </ng-template>\n\n <ng-template #validationsHeading>\n {{ 'epistolaAdminValidations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" [type]=\"validationViolations.length > 0 ? 'red' : 'gray'\" class=\"ms-1\">\n {{ validationViolations.length }}\n </cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"configurationsHeading\"\n [active]=\"overviewTab === 'configurations'\"\n (selected)=\"setOverviewTab('configurations')\"\n >\n <div *ngIf=\"loading\" class=\"text-muted mt-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length === 0\" class=\"text-muted mt-3\">\n {{ 'epistolaAdminNoConfigurations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length > 0\" class=\"card-grid\">\n <div\n *ngFor=\"let card of cards\"\n class=\"config-card\"\n [class.config-card--ok]=\"card.reachable && card.problemCount === 0\"\n [class.config-card--warning]=\"card.reachable && card.problemCount > 0\"\n [class.config-card--error]=\"!card.reachable\"\n (click)=\"selectConfiguration(card)\"\n >\n <div class=\"config-card__header\">\n <span\n class=\"status-dot\"\n [class.status-dot--ok]=\"card.reachable\"\n [class.status-dot--error]=\"!card.reachable\"\n >\n </span>\n <h5 class=\"config-card__title\">{{ card.configurationTitle }}</h5>\n </div>\n\n <div class=\"config-card__body\">\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async\n }}</span>\n <code class=\"config-card__value\">{{ card.tenantId }}</code>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" [type]=\"card.reachable ? 'green' : 'red'\">\n {{\n card.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n </div>\n <div *ngIf=\"card.serverVersion\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.serverVersion }}</span>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.usageCount }}</span>\n </div>\n <div *ngIf=\"card.pendingJobs.length > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">{{ card.pendingJobs.length }}</cds-tag>\n </div>\n <div *ngIf=\"card.problemCount > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"red\">{{ card.problemCount }}</cds-tag>\n </div>\n </div>\n\n <div class=\"config-card__footer\">\n <span class=\"config-card__latency\">{{ card.latencyMs }} ms</span>\n </div>\n </div>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"validationsHeading\"\n [active]=\"overviewTab === 'validations'\"\n (selected)=\"setOverviewTab('validations')\"\n >\n <div *ngIf=\"validationViolations.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoValidations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"validationViolations.length > 0\" class=\"mt-3\">\n <p class=\"text-muted mb-3\">\n {{ 'epistolaAdminValidationWarningBody' | pluginTranslate: 'epistola' | async }}\n </p>\n <table class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminValidationCode' | pluginTranslate: 'epistola' | async }}</th>\n <th>\n {{ 'epistolaAdminValidationMessage' | pluginTranslate: 'epistola' | async }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let v of validationViolations\">\n <td>\n {{ v.processDefinitionName || v.processDefinitionKey }}\n <div *ngIf=\"v.processDefinitionName\" class=\"text-muted small\">\n <code>{{ v.processDefinitionKey }}</code>\n </div>\n </td>\n <td>\n <code>{{ v.activityId }}</code>\n </td>\n <td>\n <cds-tag size=\"sm\" type=\"red\">{{ v.code }}</cds-tag>\n </td>\n <td>{{ v.message }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n\n <!-- Detail view: selected configuration -->\n <ng-container *ngIf=\"selectedCard\">\n <div class=\"detail-header mb-3\">\n <button class=\"btn btn-link btn-sm p-0\" (click)=\"backToOverview()\">\n &larr; {{ 'epistolaAdminBackToOverview' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div class=\"detail-summary mb-4\">\n <h4>\n <span\n class=\"status-dot me-2\"\n [class.status-dot--ok]=\"selectedCard.reachable\"\n [class.status-dot--error]=\"!selectedCard.reachable\"\n >\n </span>\n {{ selectedCard.configurationTitle }}\n </h4>\n\n <table class=\"table table-sm detail-info-table\">\n <tbody>\n <tr>\n <th>{{ 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <code>{{ selectedCard.tenantId }}</code>\n </td>\n </tr>\n <tr>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <cds-tag size=\"sm\" [type]=\"selectedCard.reachable ? 'green' : 'red'\">\n {{\n selectedCard.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n <span class=\"text-muted ms-2\">{{ selectedCard.latencyMs }} ms</span>\n </td>\n </tr>\n <tr *ngIf=\"selectedCard.serverVersion\">\n <th>{{ 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async }}</th>\n <td>{{ selectedCard.serverVersion }}</td>\n </tr>\n <tr *ngIf=\"selectedCard.errorMessage\">\n <th>{{ 'epistolaAdminError' | pluginTranslate: 'epistola' | async }}</th>\n <td class=\"text-danger\">{{ selectedCard.errorMessage }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Tabs -->\n <ng-template #actionsHeading>\n {{ 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ selectedCard.usageEntries.length }}</cds-tag>\n </ng-template>\n\n <ng-template #pendingHeading>\n {{ 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n size=\"sm\"\n [type]=\"selectedCard.pendingJobs.length > 0 ? 'blue' : 'gray'\"\n class=\"ms-1\"\n >\n {{ selectedCard.pendingJobs.length }}\n </cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"actionsHeading\"\n [active]=\"activeTab === 'actions'\"\n (selected)=\"setActiveTab('actions')\"\n >\n <div *ngIf=\"selectedCard.usageEntries.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoUsageForConfig' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.usageEntries.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCase' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminAction' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let entry of selectedCard.usageEntries\"\n [class.table-warning]=\"entry.problems.length > 0\"\n >\n <td>{{ entry.caseDefinitionKey || '-' }}</td>\n <td>\n <a\n *ngIf=\"entry.caseDefinitionKey && entry.caseDefinitionVersionTag\"\n [routerLink]=\"[\n '/case-management',\n 'case',\n entry.caseDefinitionKey,\n 'version',\n entry.caseDefinitionVersionTag,\n 'processes',\n entry.processDefinitionKey,\n ]\"\n class=\"usage-link\"\n >\n {{ entry.processDefinitionName }}\n </a>\n <span *ngIf=\"!entry.caseDefinitionKey || !entry.caseDefinitionVersionTag\">\n {{ entry.processDefinitionName }}\n </span>\n </td>\n <td>{{ entry.activityName }}</td>\n <td>\n <code>{{ entry.actionKey }}</code>\n </td>\n <td>\n <cds-tag *ngIf=\"entry.problems.length === 0\" size=\"sm\" type=\"green\">OK</cds-tag>\n <cds-tag\n *ngFor=\"let problem of entry.problems\"\n size=\"sm\"\n type=\"red\"\n class=\"d-block mb-1\"\n >\n {{ problem }}\n </cds-tag>\n </td>\n <td>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"exportProcessLink(entry)\"\n [title]=\"'epistolaAdminExport' | pluginTranslate: 'epistola' | async\"\n >\n &#x2913;\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"pendingHeading\"\n [active]=\"activeTab === 'pending'\"\n (selected)=\"setActiveTab('pending')\"\n >\n <div *ngIf=\"selectedCard.pendingJobs.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoPendingJobs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.pendingJobs.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminRequestId' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let job of selectedCard.pendingJobs\">\n <td>{{ job.processDefinitionName }}</td>\n <td>{{ job.activityName }}</td>\n <td>\n <code>{{ job.requestId }}</code>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"reconcilePending(job)\"\n [disabled]=\"isReconciling(job)\"\n [title]=\"'epistolaAdminReconcileTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isReconciling(job)\">\n {{ 'epistolaAdminReconcile' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isReconciling(job)\">\n {{ 'epistolaAdminReconciling' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"reconcileFeedback && reconcileFeedback.executionId === job.executionId\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"reconcileFeedback.type === 'success'\"\n [class.text-warning]=\"reconcileFeedback.type === 'pending'\"\n [class.text-danger]=\"reconcileFeedback.type === 'error'\"\n >\n {{ reconcileFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n</div>\n", styles: [".epistola-admin{padding:1.5rem}.epistola-admin .version-badge{font-size:.75rem;font-weight:500;padding:.2em .6em;border-radius:4px;background-color:#e8e8e8;color:#525252}.epistola-admin .badge{font-size:.85em;padding:.35em .65em}.epistola-admin code{font-size:.9em;color:#525252}.epistola-admin table th{font-weight:600;white-space:nowrap}.epistola-admin .status-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:.5rem;flex-shrink:0;background-color:#adb5bd}.epistola-admin .status-dot--ok{background-color:#198754}.epistola-admin .status-dot--error{background-color:#dc3545}.epistola-admin .card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.epistola-admin .config-card{border:1px solid #dee2e6;border-radius:8px;padding:1.25rem;cursor:pointer;transition:box-shadow .15s ease,border-color .15s ease;background:#fff}.epistola-admin .config-card:hover{box-shadow:0 2px 8px #0000001a}.epistola-admin .config-card--ok{border-left:4px solid #198754}.epistola-admin .config-card--warning{border-left:4px solid #ffc107}.epistola-admin .config-card--error{border-left:4px solid #dc3545}.epistola-admin .config-card__header{display:flex;align-items:center;margin-bottom:1rem}.epistola-admin .config-card__title{margin:0;font-size:1.05rem;font-weight:600;color:#161616}.epistola-admin .config-card__body{display:flex;flex-direction:column;gap:.5rem}.epistola-admin .config-card__field{display:flex;justify-content:space-between;align-items:center}.epistola-admin .config-card__label{font-size:.875rem;color:#6c757d}.epistola-admin .config-card__value{font-size:.875rem;color:#161616}.epistola-admin .config-card__footer{margin-top:1rem;padding-top:.75rem;border-top:1px solid #f0f0f0;text-align:right}.epistola-admin .config-card__latency{font-size:.8rem;color:#adb5bd}.epistola-admin .usage-link{color:#0f62fe;text-decoration:none}.epistola-admin .usage-link:hover{text-decoration:underline}.epistola-admin .detail-info-table{max-width:500px}.epistola-admin .detail-info-table th{width:140px}.epistola-admin .detail-summary{padding:1rem 0;border-bottom:1px solid #dee2e6}\n"] }]
3340
+ }], ctorParameters: () => [{ type: EpistolaAdminService }, { type: i2$5.ActivatedRoute }, { type: i2$5.Router }] });
3341
+
3342
+ function isRuntimeWindow(value) {
3343
+ return typeof value === 'object' && value !== null;
3344
+ }
3345
+ /**
3346
+ * Reads the runtime feature flag that decides whether the Epistola plugin
3347
+ * surfaces (admin menu, /epistola route, plugin specification, Formio
3348
+ * components) should activate in the host Valtimo app.
3349
+ *
3350
+ * The flag is sourced from `window['env']['epistolaEnabled']`, populated at
3351
+ * container start by `envsubst` against `assets/config.template.js` (the
3352
+ * standard Valtimo runtime-config pattern). Defaults to enabled — only the
3353
+ * literal `false` or string `'false'` disables the plugin, matching the
3354
+ * backend's `epistola.enabled` `matchIfMissing = true` semantics.
3355
+ *
3356
+ * Exposed as a runtime helper rather than evaluated directly in `@NgModule`
3357
+ * decorator metadata because Angular's AOT compiler cannot statically resolve
3358
+ * `window` accesses (NG1010). Read from runtime code such as specification
3359
+ * property getters, route guards, or the environment initializer instead.
3360
+ */
3361
+ function isEpistolaEnabled() {
3362
+ const runtimeWindow = Reflect.get(globalThis, 'window');
3363
+ if (!runtimeWindow)
3364
+ return true;
3365
+ if (!isRuntimeWindow(runtimeWindow))
3366
+ return true;
3367
+ const flag = runtimeWindow.env ? runtimeWindow.env.epistolaEnabled : undefined;
3368
+ return flag !== false && flag !== 'false';
3369
+ }
3370
+
3371
+ const epistolaEnabledGuard = () => {
3372
+ if (isEpistolaEnabled())
3373
+ return true;
3374
+ return inject(Router).parseUrl('/');
3375
+ };
2864
3376
 
2865
3377
  const routes = [
2866
3378
  {
2867
3379
  path: 'epistola',
2868
3380
  component: EpistolaAdminPageComponent,
2869
- canActivate: [AuthGuardService],
3381
+ canActivate: [epistolaEnabledGuard, AuthGuardService],
2870
3382
  data: { title: 'Epistola', roles: ['ROLE_ADMIN'] },
2871
3383
  },
2872
3384
  ];
2873
3385
  class EpistolaAdminRoutingModule {
2874
3386
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
2875
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, imports: [i2$3.RouterModule], exports: [RouterModule] });
3387
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, imports: [i2$5.RouterModule], exports: [RouterModule] });
2876
3388
  static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, imports: [RouterModule.forChild(routes), RouterModule] });
2877
3389
  }
2878
3390
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, decorators: [{
@@ -2883,19 +3395,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2883
3395
  }]
2884
3396
  }] });
2885
3397
 
2886
- const EPISTOLA_DOWNLOAD_OPTIONS = {
2887
- type: 'epistola-download',
2888
- selector: 'epistola-download-button',
2889
- title: 'Epistola Download',
3398
+ const EPISTOLA_DOCUMENT_OPTIONS = {
3399
+ type: 'epistola-document',
3400
+ selector: 'epistola-document-element',
3401
+ title: 'Epistola Document',
2890
3402
  group: 'basic',
2891
- icon: 'download',
3403
+ icon: 'file-pdf-o',
2892
3404
  emptyValue: null,
2893
- fieldOptions: ['filename', 'label'],
3405
+ fieldOptions: ['label', 'display', 'documentVariable', 'tenantIdVariable', 'filename'],
2894
3406
  };
2895
- function registerEpistolaDownloadComponent(injector) {
2896
- if (!customElements.get(EPISTOLA_DOWNLOAD_OPTIONS.selector)) {
2897
- registerCustomFormioComponent(EPISTOLA_DOWNLOAD_OPTIONS, EpistolaDownloadComponent, injector);
3407
+ function registerEpistolaDocumentComponent(injector) {
3408
+ if (customElements.get(EPISTOLA_DOCUMENT_OPTIONS.selector)) {
3409
+ return;
2898
3410
  }
3411
+ registerCustomFormioComponent(EPISTOLA_DOCUMENT_OPTIONS, EpistolaDocumentComponent, injector);
2899
3412
  }
2900
3413
 
2901
3414
  const EPISTOLA_RETRY_FORM_OPTIONS = {
@@ -2913,33 +3426,548 @@ function registerEpistolaRetryFormComponent(injector) {
2913
3426
  }
2914
3427
  }
2915
3428
 
2916
- const EPISTOLA_PREVIEW_BUTTON_OPTIONS = {
2917
- type: 'epistola-preview-button',
2918
- selector: 'epistola-preview-button-element',
2919
- title: 'Epistola Preview',
3429
+ const EPISTOLA_DOCUMENT_PREVIEW_OPTIONS = {
3430
+ type: 'epistola-document-preview',
3431
+ selector: 'epistola-document-preview-element',
3432
+ title: 'Epistola Document Preview',
2920
3433
  group: 'basic',
2921
- icon: 'eye',
3434
+ icon: 'file-pdf-o',
2922
3435
  emptyValue: null,
2923
- fieldOptions: ['label'],
3436
+ fieldOptions: ['label', 'processDefinitionKey', 'sourceActivityId', 'overrideMapping'],
3437
+ editForm: () => ({
3438
+ components: [
3439
+ {
3440
+ type: 'epistola-process-link-selector',
3441
+ key: 'processLinkSelection',
3442
+ label: 'Process Link',
3443
+ weight: 10,
3444
+ validate: { required: true },
3445
+ },
3446
+ {
3447
+ type: 'epistola-override-builder',
3448
+ key: 'overrideMapping',
3449
+ label: 'Input Overrides',
3450
+ weight: 20,
3451
+ },
3452
+ ],
3453
+ }),
2924
3454
  };
2925
- function registerEpistolaPreviewButtonComponent(injector) {
2926
- if (!customElements.get(EPISTOLA_PREVIEW_BUTTON_OPTIONS.selector)) {
2927
- registerCustomFormioComponent(EPISTOLA_PREVIEW_BUTTON_OPTIONS, EpistolaPreviewButtonComponent, injector);
3455
+ function registerEpistolaDocumentPreviewComponent(injector) {
3456
+ if (customElements.get(EPISTOLA_DOCUMENT_PREVIEW_OPTIONS.selector)) {
3457
+ return;
2928
3458
  }
3459
+ // Register the base component (Angular element + Formio component class)
3460
+ registerCustomFormioComponent(EPISTOLA_DOCUMENT_PREVIEW_OPTIONS, EpistolaDocumentPreviewComponent, injector);
3461
+ // Get the Formio Components registry and the registered base class
3462
+ const Formio = window.Formio;
3463
+ if (!Formio?.Components)
3464
+ return;
3465
+ const BasePreviewComponent = Formio.Components.components[EPISTOLA_DOCUMENT_PREVIEW_OPTIONS.type];
3466
+ if (!BasePreviewComponent)
3467
+ return;
3468
+ // Extend the base class to listen for form data changes and compute input overrides
3469
+ class PreviewWithOverrides extends BasePreviewComponent {
3470
+ _debounceTimer = null;
3471
+ _changeListenerAttached = false;
3472
+ attach(element) {
3473
+ // Bidirectional sync between processLinkSelection object and separate properties.
3474
+ // The editForm uses processLinkSelection (single field), while the component
3475
+ // config and Angular inputs use processDefinitionKey + sourceActivityId.
3476
+ if (this.component?.processLinkSelection) {
3477
+ const sel = this.component.processLinkSelection;
3478
+ this.component.processDefinitionKey = sel.processDefinitionKey || '';
3479
+ this.component.sourceActivityId = sel.sourceActivityId || '';
3480
+ }
3481
+ else if (this.component?.processDefinitionKey && this.component?.sourceActivityId) {
3482
+ this.component.processLinkSelection = {
3483
+ processDefinitionKey: this.component.processDefinitionKey,
3484
+ sourceActivityId: this.component.sourceActivityId,
3485
+ };
3486
+ }
3487
+ const result = super.attach(element);
3488
+ if (this._customAngularElement) {
3489
+ this._customAngularElement['processDefinitionKey'] =
3490
+ this.component.processDefinitionKey || '';
3491
+ this._customAngularElement['sourceActivityId'] = this.component.sourceActivityId || '';
3492
+ }
3493
+ // Listen to form changes and compute input overrides from the mapping
3494
+ if (this.root && this.component?.overrideMapping && !this._changeListenerAttached) {
3495
+ this._changeListenerAttached = true;
3496
+ this.root.on('change', () => {
3497
+ this._computeAndSetOverrides();
3498
+ });
3499
+ // Compute initial value
3500
+ this._computeAndSetOverrides();
3501
+ }
3502
+ return result;
3503
+ }
3504
+ _computeAndSetOverrides() {
3505
+ if (this._debounceTimer) {
3506
+ clearTimeout(this._debounceTimer);
3507
+ }
3508
+ this._debounceTimer = setTimeout(() => {
3509
+ const mapping = this.component?.overrideMapping;
3510
+ const formData = this.root?.data;
3511
+ if (mapping && formData) {
3512
+ const overrides = computeInputOverrides(mapping, formData);
3513
+ if (Object.keys(overrides).length > 0) {
3514
+ this.setValue(overrides);
3515
+ }
3516
+ }
3517
+ }, 1500);
3518
+ }
3519
+ }
3520
+ // Re-register with the extended class
3521
+ Formio.Components.setComponent(EPISTOLA_DOCUMENT_PREVIEW_OPTIONS.type, PreviewWithOverrides);
2929
3522
  }
2930
3523
 
2931
- const EPISTOLA_DOCUMENT_PREVIEW_OPTIONS = {
2932
- type: 'epistola-document-preview',
2933
- selector: 'epistola-document-preview-element',
2934
- title: 'Epistola Document Preview',
3524
+ const FORM_REF_PREFIX = 'form:';
3525
+ class EpistolaOverrideBuilderComponent {
3526
+ cdr;
3527
+ value;
3528
+ valueChange = new EventEmitter();
3529
+ disabled = false;
3530
+ label = 'Input Overrides';
3531
+ availableFields = [];
3532
+ rows = [];
3533
+ advancedMode = false;
3534
+ jsonText = '';
3535
+ jsonError = null;
3536
+ initialized = false;
3537
+ constructor(cdr) {
3538
+ this.cdr = cdr;
3539
+ }
3540
+ ngOnChanges() {
3541
+ if (!this.initialized && this.value) {
3542
+ this.initialized = true;
3543
+ this.rows = this.mappingToRows(this.value);
3544
+ this.jsonText = JSON.stringify(this.value, null, 2);
3545
+ }
3546
+ this.cdr.markForCheck();
3547
+ }
3548
+ toggleMode() {
3549
+ this.advancedMode = !this.advancedMode;
3550
+ if (this.advancedMode) {
3551
+ const mapping = this.rowsToMapping();
3552
+ this.jsonText = Object.keys(mapping).length > 0 ? JSON.stringify(mapping, null, 2) : '';
3553
+ this.jsonError = null;
3554
+ }
3555
+ else {
3556
+ try {
3557
+ const parsed = this.jsonText.trim() ? JSON.parse(this.jsonText) : {};
3558
+ this.rows = this.mappingToRows(parsed);
3559
+ this.jsonError = null;
3560
+ }
3561
+ catch {
3562
+ // Keep current rows if JSON is invalid
3563
+ }
3564
+ }
3565
+ }
3566
+ addRow() {
3567
+ this.rows.push({ scope: 'doc', inputPath: '', formFieldKey: '' });
3568
+ }
3569
+ removeRow(index) {
3570
+ this.rows.splice(index, 1);
3571
+ this.emitChange();
3572
+ }
3573
+ emitChange() {
3574
+ const mapping = this.rowsToMapping();
3575
+ this.value = Object.keys(mapping).length > 0 ? mapping : null;
3576
+ this.valueChange.emit(this.value);
3577
+ }
3578
+ onJsonChange(text) {
3579
+ this.jsonText = text;
3580
+ if (!text.trim()) {
3581
+ this.jsonError = null;
3582
+ this.value = null;
3583
+ this.valueChange.emit(null);
3584
+ return;
3585
+ }
3586
+ try {
3587
+ const parsed = JSON.parse(text);
3588
+ this.jsonError = null;
3589
+ this.value = parsed;
3590
+ this.valueChange.emit(parsed);
3591
+ }
3592
+ catch (e) {
3593
+ this.jsonError = 'Invalid JSON';
3594
+ }
3595
+ }
3596
+ rowsToMapping() {
3597
+ const mapping = {};
3598
+ for (const row of this.rows) {
3599
+ if (row.inputPath && row.formFieldKey) {
3600
+ if (!mapping[row.scope]) {
3601
+ mapping[row.scope] = {};
3602
+ }
3603
+ mapping[row.scope][row.inputPath] = FORM_REF_PREFIX + row.formFieldKey;
3604
+ }
3605
+ }
3606
+ return mapping;
3607
+ }
3608
+ mappingToRows(mapping) {
3609
+ const rows = [];
3610
+ for (const [scope, fields] of Object.entries(mapping)) {
3611
+ if (scope === 'doc' || scope === 'pv') {
3612
+ for (const [path, ref] of Object.entries(fields)) {
3613
+ const formFieldKey = String(ref).startsWith(FORM_REF_PREFIX)
3614
+ ? String(ref).substring(FORM_REF_PREFIX.length)
3615
+ : String(ref);
3616
+ rows.push({ scope, inputPath: path, formFieldKey });
3617
+ }
3618
+ }
3619
+ }
3620
+ return rows;
3621
+ }
3622
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaOverrideBuilderComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
3623
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaOverrideBuilderComponent, isStandalone: true, selector: "epistola-override-builder-component", inputs: { value: "value", disabled: "disabled", label: "label", availableFields: "availableFields" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
3624
+ <div class="override-builder">
3625
+ <div class="builder-header">
3626
+ <span class="builder-label">{{ label || 'Input Overrides' }}</span>
3627
+ <button type="button" class="mode-toggle" (click)="toggleMode()">
3628
+ {{ advancedMode ? 'Simple' : 'Advanced' }}
3629
+ </button>
3630
+ </div>
3631
+
3632
+ <!-- Simple mode: table -->
3633
+ <div *ngIf="!advancedMode" class="builder-table">
3634
+ <div *ngIf="rows.length > 0" class="table-header">
3635
+ <span class="col-scope">Scope</span>
3636
+ <span class="col-path">Input Path</span>
3637
+ <span class="col-field">Form Field</span>
3638
+ <span class="col-action"></span>
3639
+ </div>
3640
+ <div *ngFor="let row of rows; let i = index" class="table-row">
3641
+ <select class="col-scope" [(ngModel)]="row.scope" (ngModelChange)="emitChange()">
3642
+ <option value="doc">doc</option>
3643
+ <option value="pv">pv</option>
3644
+ </select>
3645
+ <input
3646
+ class="col-path"
3647
+ type="text"
3648
+ [(ngModel)]="row.inputPath"
3649
+ (ngModelChange)="emitChange()"
3650
+ placeholder="e.g. beslissing.tekst"
3651
+ />
3652
+ <!-- Dropdown when form fields are available, text input as fallback -->
3653
+ <select
3654
+ *ngIf="availableFields.length > 0"
3655
+ class="col-field"
3656
+ [(ngModel)]="row.formFieldKey"
3657
+ (ngModelChange)="emitChange()"
3658
+ >
3659
+ <option value="">-- Select field --</option>
3660
+ <option *ngFor="let field of availableFields" [value]="field.key">
3661
+ {{ field.label }}
3662
+ </option>
3663
+ </select>
3664
+ <input
3665
+ *ngIf="availableFields.length === 0"
3666
+ class="col-field"
3667
+ type="text"
3668
+ [(ngModel)]="row.formFieldKey"
3669
+ (ngModelChange)="emitChange()"
3670
+ placeholder="form field key"
3671
+ />
3672
+ <button type="button" class="col-action remove-btn" (click)="removeRow(i)">
3673
+ <i class="mdi mdi-close"></i>
3674
+ </button>
3675
+ </div>
3676
+ <button type="button" class="add-btn" (click)="addRow()">
3677
+ <i class="mdi mdi-plus mr-1"></i> Add override
3678
+ </button>
3679
+ </div>
3680
+
3681
+ <!-- Advanced mode: JSON editor -->
3682
+ <div *ngIf="advancedMode" class="builder-advanced">
3683
+ <textarea
3684
+ class="json-editor"
3685
+ [ngModel]="jsonText"
3686
+ (ngModelChange)="onJsonChange($event)"
3687
+ placeholder='{ "pv": { "motivation": "form:pv:motivation" } }'
3688
+ rows="6"
3689
+ ></textarea>
3690
+ <div *ngIf="jsonError" class="json-error">{{ jsonError }}</div>
3691
+ </div>
3692
+ </div>
3693
+ `, isInline: true, styles: [".override-builder{border:1px solid #dee2e6;border-radius:4px;padding:.75rem;background:#f8f9fa}.builder-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem}.builder-label{font-weight:600;font-size:.85rem;color:#495057}.mode-toggle{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.15rem .5rem;font-size:.75rem;cursor:pointer}.mode-toggle:hover{background:#e9ecef}.table-header{display:flex;gap:.5rem;padding:.25rem 0;font-size:.75rem;color:#6c757d;font-weight:600}.table-row{display:flex;gap:.5rem;margin-bottom:.25rem;align-items:center}.col-scope{width:70px;flex-shrink:0}.col-path,.col-field{flex:1;min-width:0}.col-action{width:30px;flex-shrink:0}.table-row select,.table-row input{border:1px solid #ced4da;border-radius:4px;padding:.25rem .4rem;font-size:.8rem;background:#fff}.remove-btn{background:none;border:none;color:#dc3545;cursor:pointer;padding:.25rem;font-size:.9rem}.remove-btn:hover{color:#a71d2a}.add-btn{background:none;border:1px dashed #6c757d;border-radius:4px;color:#6c757d;padding:.25rem .75rem;font-size:.8rem;cursor:pointer;margin-top:.25rem;display:flex;align-items:center}.add-btn:hover{background:#e9ecef;border-color:#495057}.json-editor{width:100%;border:1px solid #ced4da;border-radius:4px;padding:.5rem;font-family:monospace;font-size:.8rem;resize:vertical;background:#fff}.json-error{color:#dc3545;font-size:.75rem;margin-top:.25rem}\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: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3694
+ }
3695
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaOverrideBuilderComponent, decorators: [{
3696
+ type: Component,
3697
+ args: [{ standalone: true, imports: [CommonModule, FormsModule], selector: 'epistola-override-builder-component', changeDetection: ChangeDetectionStrategy.OnPush, template: `
3698
+ <div class="override-builder">
3699
+ <div class="builder-header">
3700
+ <span class="builder-label">{{ label || 'Input Overrides' }}</span>
3701
+ <button type="button" class="mode-toggle" (click)="toggleMode()">
3702
+ {{ advancedMode ? 'Simple' : 'Advanced' }}
3703
+ </button>
3704
+ </div>
3705
+
3706
+ <!-- Simple mode: table -->
3707
+ <div *ngIf="!advancedMode" class="builder-table">
3708
+ <div *ngIf="rows.length > 0" class="table-header">
3709
+ <span class="col-scope">Scope</span>
3710
+ <span class="col-path">Input Path</span>
3711
+ <span class="col-field">Form Field</span>
3712
+ <span class="col-action"></span>
3713
+ </div>
3714
+ <div *ngFor="let row of rows; let i = index" class="table-row">
3715
+ <select class="col-scope" [(ngModel)]="row.scope" (ngModelChange)="emitChange()">
3716
+ <option value="doc">doc</option>
3717
+ <option value="pv">pv</option>
3718
+ </select>
3719
+ <input
3720
+ class="col-path"
3721
+ type="text"
3722
+ [(ngModel)]="row.inputPath"
3723
+ (ngModelChange)="emitChange()"
3724
+ placeholder="e.g. beslissing.tekst"
3725
+ />
3726
+ <!-- Dropdown when form fields are available, text input as fallback -->
3727
+ <select
3728
+ *ngIf="availableFields.length > 0"
3729
+ class="col-field"
3730
+ [(ngModel)]="row.formFieldKey"
3731
+ (ngModelChange)="emitChange()"
3732
+ >
3733
+ <option value="">-- Select field --</option>
3734
+ <option *ngFor="let field of availableFields" [value]="field.key">
3735
+ {{ field.label }}
3736
+ </option>
3737
+ </select>
3738
+ <input
3739
+ *ngIf="availableFields.length === 0"
3740
+ class="col-field"
3741
+ type="text"
3742
+ [(ngModel)]="row.formFieldKey"
3743
+ (ngModelChange)="emitChange()"
3744
+ placeholder="form field key"
3745
+ />
3746
+ <button type="button" class="col-action remove-btn" (click)="removeRow(i)">
3747
+ <i class="mdi mdi-close"></i>
3748
+ </button>
3749
+ </div>
3750
+ <button type="button" class="add-btn" (click)="addRow()">
3751
+ <i class="mdi mdi-plus mr-1"></i> Add override
3752
+ </button>
3753
+ </div>
3754
+
3755
+ <!-- Advanced mode: JSON editor -->
3756
+ <div *ngIf="advancedMode" class="builder-advanced">
3757
+ <textarea
3758
+ class="json-editor"
3759
+ [ngModel]="jsonText"
3760
+ (ngModelChange)="onJsonChange($event)"
3761
+ placeholder='{ "pv": { "motivation": "form:pv:motivation" } }'
3762
+ rows="6"
3763
+ ></textarea>
3764
+ <div *ngIf="jsonError" class="json-error">{{ jsonError }}</div>
3765
+ </div>
3766
+ </div>
3767
+ `, styles: [".override-builder{border:1px solid #dee2e6;border-radius:4px;padding:.75rem;background:#f8f9fa}.builder-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem}.builder-label{font-weight:600;font-size:.85rem;color:#495057}.mode-toggle{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.15rem .5rem;font-size:.75rem;cursor:pointer}.mode-toggle:hover{background:#e9ecef}.table-header{display:flex;gap:.5rem;padding:.25rem 0;font-size:.75rem;color:#6c757d;font-weight:600}.table-row{display:flex;gap:.5rem;margin-bottom:.25rem;align-items:center}.col-scope{width:70px;flex-shrink:0}.col-path,.col-field{flex:1;min-width:0}.col-action{width:30px;flex-shrink:0}.table-row select,.table-row input{border:1px solid #ced4da;border-radius:4px;padding:.25rem .4rem;font-size:.8rem;background:#fff}.remove-btn{background:none;border:none;color:#dc3545;cursor:pointer;padding:.25rem;font-size:.9rem}.remove-btn:hover{color:#a71d2a}.add-btn{background:none;border:1px dashed #6c757d;border-radius:4px;color:#6c757d;padding:.25rem .75rem;font-size:.8rem;cursor:pointer;margin-top:.25rem;display:flex;align-items:center}.add-btn:hover{background:#e9ecef;border-color:#495057}.json-editor{width:100%;border:1px solid #ced4da;border-radius:4px;padding:.5rem;font-family:monospace;font-size:.8rem;resize:vertical;background:#fff}.json-error{color:#dc3545;font-size:.75rem;margin-top:.25rem}\n"] }]
3768
+ }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { value: [{
3769
+ type: Input
3770
+ }], valueChange: [{
3771
+ type: Output
3772
+ }], disabled: [{
3773
+ type: Input
3774
+ }], label: [{
3775
+ type: Input
3776
+ }], availableFields: [{
3777
+ type: Input
3778
+ }] } });
3779
+
3780
+ const EPISTOLA_OVERRIDE_BUILDER_OPTIONS = {
3781
+ type: 'epistola-override-builder',
3782
+ selector: 'epistola-override-builder-element',
3783
+ title: 'Epistola Override Builder',
2935
3784
  group: 'basic',
2936
- icon: 'file-pdf-o',
3785
+ icon: 'list',
3786
+ emptyValue: null,
3787
+ fieldOptions: ['label', 'availableFields'],
3788
+ };
3789
+ /**
3790
+ * Recursively collect input field keys and labels from a Formio component tree.
3791
+ * Skips epistola custom components (which are builder UI, not form fields).
3792
+ */
3793
+ function collectFormFields(components) {
3794
+ const fields = [];
3795
+ for (const comp of components) {
3796
+ if (comp.input && comp.key && comp.type !== 'button' && !comp.type?.startsWith('epistola-')) {
3797
+ fields.push({ key: comp.key, label: comp.label || comp.key });
3798
+ }
3799
+ if (comp.components) {
3800
+ fields.push(...collectFormFields(comp.components));
3801
+ }
3802
+ if (comp.columns) {
3803
+ for (const col of comp.columns) {
3804
+ if (col.components) {
3805
+ fields.push(...collectFormFields(col.components));
3806
+ }
3807
+ }
3808
+ }
3809
+ }
3810
+ return fields;
3811
+ }
3812
+ function registerEpistolaOverrideBuilderComponent(injector) {
3813
+ if (customElements.get(EPISTOLA_OVERRIDE_BUILDER_OPTIONS.selector)) {
3814
+ return;
3815
+ }
3816
+ // Register the base component (Angular element + Formio component class)
3817
+ registerCustomFormioComponent(EPISTOLA_OVERRIDE_BUILDER_OPTIONS, EpistolaOverrideBuilderComponent, injector);
3818
+ // Get the Formio Components registry and the registered base class
3819
+ const Formio = window.Formio;
3820
+ if (!Formio?.Components)
3821
+ return;
3822
+ const BaseComponent = Formio.Components.components[EPISTOLA_OVERRIDE_BUILDER_OPTIONS.type];
3823
+ if (!BaseComponent)
3824
+ return;
3825
+ // Extend the base class to pass available form fields to the Angular component
3826
+ class OverrideBuilderWithFields extends BaseComponent {
3827
+ attach(element) {
3828
+ // Set form fields on the component BEFORE super.attach() reads fieldOptions
3829
+ this.component.availableFields = this._extractFormFields();
3830
+ return super.attach(element);
3831
+ }
3832
+ _extractFormFields() {
3833
+ // The Formio builder passes the main form schema as options.editForm
3834
+ // when opening the edit dialog (editFormOptions.editForm = this.form).
3835
+ const components = this.options?.editForm?.components;
3836
+ if (Array.isArray(components)) {
3837
+ return collectFormFields(components);
3838
+ }
3839
+ return [];
3840
+ }
3841
+ }
3842
+ // Re-register with the extended class
3843
+ Formio.Components.setComponent(EPISTOLA_OVERRIDE_BUILDER_OPTIONS.type, OverrideBuilderWithFields);
3844
+ }
3845
+
3846
+ class EpistolaProcessLinkSelectorComponent {
3847
+ adminService;
3848
+ cdr;
3849
+ value;
3850
+ valueChange = new EventEmitter();
3851
+ disabled = false;
3852
+ label = 'Process Link';
3853
+ filteredEntries = [];
3854
+ selectedKey = '';
3855
+ loading = false;
3856
+ error = null;
3857
+ initialized = false;
3858
+ loadSubscription;
3859
+ constructor(adminService, cdr) {
3860
+ this.adminService = adminService;
3861
+ this.cdr = cdr;
3862
+ }
3863
+ ngOnChanges(changes) {
3864
+ if (!this.initialized) {
3865
+ this.initialized = true;
3866
+ this.loadEntries();
3867
+ }
3868
+ // Restore selection whenever value changes (Formio may set it after init)
3869
+ if (changes['value'] && this.value) {
3870
+ this.selectedKey = `${this.value.processDefinitionKey}::${this.value.sourceActivityId}`;
3871
+ this.cdr.markForCheck();
3872
+ }
3873
+ }
3874
+ ngOnDestroy() {
3875
+ this.loadSubscription?.unsubscribe();
3876
+ }
3877
+ onSelect(key) {
3878
+ this.selectedKey = key;
3879
+ if (!key) {
3880
+ this.value = null;
3881
+ this.valueChange.emit(null);
3882
+ return;
3883
+ }
3884
+ const [processDefinitionKey, sourceActivityId] = key.split('::');
3885
+ this.value = { processDefinitionKey, sourceActivityId };
3886
+ this.valueChange.emit(this.value);
3887
+ }
3888
+ entryKey(entry) {
3889
+ return `${entry.processDefinitionKey}::${entry.activityId}`;
3890
+ }
3891
+ loadEntries() {
3892
+ this.loading = true;
3893
+ this.cdr.markForCheck();
3894
+ this.loadSubscription = this.adminService.getPluginUsage().subscribe({
3895
+ next: (entries) => {
3896
+ this.filteredEntries = entries.filter((e) => e.actionKey === 'generate-document');
3897
+ this.loading = false;
3898
+ // Restore selection from value
3899
+ if (this.value) {
3900
+ this.selectedKey = `${this.value.processDefinitionKey}::${this.value.sourceActivityId}`;
3901
+ }
3902
+ this.cdr.markForCheck();
3903
+ },
3904
+ error: (err) => {
3905
+ this.error = 'Failed to load process links';
3906
+ this.loading = false;
3907
+ this.cdr.markForCheck();
3908
+ },
3909
+ });
3910
+ }
3911
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaProcessLinkSelectorComponent, deps: [{ token: EpistolaAdminService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
3912
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaProcessLinkSelectorComponent, isStandalone: true, selector: "epistola-process-link-selector-component", inputs: { value: "value", disabled: "disabled", label: "label" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
3913
+ <div class="process-link-selector">
3914
+ <label class="selector-label">{{ label || 'Process Link' }}</label>
3915
+ <select
3916
+ class="selector-dropdown"
3917
+ [ngModel]="selectedKey"
3918
+ (ngModelChange)="onSelect($event)"
3919
+ [disabled]="disabled || loading"
3920
+ >
3921
+ <option value="">{{ loading ? 'Loading...' : '-- Select a process link --' }}</option>
3922
+ <option *ngFor="let entry of filteredEntries" [value]="entryKey(entry)">
3923
+ {{ entry.processDefinitionName }} / {{ entry.activityName }} ({{ entry.activityId }})
3924
+ </option>
3925
+ </select>
3926
+ <div *ngIf="error" class="selector-error">{{ error }}</div>
3927
+ </div>
3928
+ `, isInline: true, styles: [".process-link-selector{margin-bottom:.5rem}.selector-label{display:block;font-weight:600;font-size:.85rem;color:#495057;margin-bottom:.25rem}.selector-dropdown{width:100%;border:1px solid #ced4da;border-radius:4px;padding:.4rem .5rem;font-size:.85rem;background:#fff}.selector-error{color:#dc3545;font-size:.75rem;margin-top:.25rem}\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: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3929
+ }
3930
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaProcessLinkSelectorComponent, decorators: [{
3931
+ type: Component,
3932
+ args: [{ standalone: true, imports: [CommonModule, FormsModule], selector: 'epistola-process-link-selector-component', changeDetection: ChangeDetectionStrategy.OnPush, template: `
3933
+ <div class="process-link-selector">
3934
+ <label class="selector-label">{{ label || 'Process Link' }}</label>
3935
+ <select
3936
+ class="selector-dropdown"
3937
+ [ngModel]="selectedKey"
3938
+ (ngModelChange)="onSelect($event)"
3939
+ [disabled]="disabled || loading"
3940
+ >
3941
+ <option value="">{{ loading ? 'Loading...' : '-- Select a process link --' }}</option>
3942
+ <option *ngFor="let entry of filteredEntries" [value]="entryKey(entry)">
3943
+ {{ entry.processDefinitionName }} / {{ entry.activityName }} ({{ entry.activityId }})
3944
+ </option>
3945
+ </select>
3946
+ <div *ngIf="error" class="selector-error">{{ error }}</div>
3947
+ </div>
3948
+ `, styles: [".process-link-selector{margin-bottom:.5rem}.selector-label{display:block;font-weight:600;font-size:.85rem;color:#495057;margin-bottom:.25rem}.selector-dropdown{width:100%;border:1px solid #ced4da;border-radius:4px;padding:.4rem .5rem;font-size:.85rem;background:#fff}.selector-error{color:#dc3545;font-size:.75rem;margin-top:.25rem}\n"] }]
3949
+ }], ctorParameters: () => [{ type: EpistolaAdminService }, { type: i0.ChangeDetectorRef }], propDecorators: { value: [{
3950
+ type: Input
3951
+ }], valueChange: [{
3952
+ type: Output
3953
+ }], disabled: [{
3954
+ type: Input
3955
+ }], label: [{
3956
+ type: Input
3957
+ }] } });
3958
+
3959
+ const EPISTOLA_PROCESS_LINK_SELECTOR_OPTIONS = {
3960
+ type: 'epistola-process-link-selector',
3961
+ selector: 'epistola-process-link-selector-element',
3962
+ title: 'Epistola Process Link Selector',
3963
+ group: 'basic',
3964
+ icon: 'link',
2937
3965
  emptyValue: null,
2938
3966
  fieldOptions: ['label'],
2939
3967
  };
2940
- function registerEpistolaDocumentPreviewComponent(injector) {
2941
- if (!customElements.get(EPISTOLA_DOCUMENT_PREVIEW_OPTIONS.selector)) {
2942
- registerCustomFormioComponent(EPISTOLA_DOCUMENT_PREVIEW_OPTIONS, EpistolaDocumentPreviewComponent, injector);
3968
+ function registerEpistolaProcessLinkSelectorComponent(injector) {
3969
+ if (!customElements.get(EPISTOLA_PROCESS_LINK_SELECTOR_OPTIONS.selector)) {
3970
+ registerCustomFormioComponent(EPISTOLA_PROCESS_LINK_SELECTOR_OPTIONS, EpistolaProcessLinkSelectorComponent, injector);
2943
3971
  }
2944
3972
  }
2945
3973
 
@@ -2949,14 +3977,22 @@ class EpistolaPluginModule {
2949
3977
  ngModule: EpistolaPluginModule,
2950
3978
  providers: [
2951
3979
  EpistolaMenuService,
3980
+ {
3981
+ provide: HTTP_INTERCEPTORS,
3982
+ useClass: EpistolaTaskContextInterceptor,
3983
+ multi: true,
3984
+ },
2952
3985
  {
2953
3986
  provide: ENVIRONMENT_INITIALIZER,
2954
3987
  multi: true,
2955
3988
  useValue: () => {
3989
+ if (!isEpistolaEnabled())
3990
+ return;
2956
3991
  const injector = inject(Injector);
2957
- registerEpistolaDownloadComponent(injector);
3992
+ registerEpistolaDocumentComponent(injector);
2958
3993
  registerEpistolaRetryFormComponent(injector);
2959
- registerEpistolaPreviewButtonComponent(injector);
3994
+ registerEpistolaOverrideBuilderComponent(injector);
3995
+ registerEpistolaProcessLinkSelectorComponent(injector);
2960
3996
  registerEpistolaDocumentPreviewComponent(injector);
2961
3997
  // Eagerly create EpistolaMenuService to trigger menu registration
2962
3998
  inject(EpistolaMenuService);
@@ -2977,17 +4013,15 @@ class EpistolaPluginModule {
2977
4013
  GenerateDocumentConfigurationComponent,
2978
4014
  CheckJobStatusConfigurationComponent,
2979
4015
  DownloadDocumentConfigurationComponent,
2980
- EpistolaDownloadComponent,
4016
+ EpistolaDocumentComponent,
2981
4017
  EpistolaRetryFormComponent,
2982
- EpistolaPreviewButtonComponent,
2983
4018
  EpistolaDocumentPreviewComponent,
2984
4019
  EpistolaAdminPageComponent], exports: [EpistolaConfigurationComponent,
2985
4020
  GenerateDocumentConfigurationComponent,
2986
4021
  CheckJobStatusConfigurationComponent,
2987
4022
  DownloadDocumentConfigurationComponent,
2988
- EpistolaDownloadComponent,
4023
+ EpistolaDocumentComponent,
2989
4024
  EpistolaRetryFormComponent,
2990
- EpistolaPreviewButtonComponent,
2991
4025
  EpistolaDocumentPreviewComponent,
2992
4026
  EpistolaAdminPageComponent] });
2993
4027
  static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaPluginModule, providers: [EpistolaPluginService, EpistolaAdminService], imports: [CommonModule,
@@ -3001,9 +4035,8 @@ class EpistolaPluginModule {
3001
4035
  GenerateDocumentConfigurationComponent,
3002
4036
  CheckJobStatusConfigurationComponent,
3003
4037
  DownloadDocumentConfigurationComponent,
3004
- EpistolaDownloadComponent,
4038
+ EpistolaDocumentComponent,
3005
4039
  EpistolaRetryFormComponent,
3006
- EpistolaPreviewButtonComponent,
3007
4040
  EpistolaDocumentPreviewComponent,
3008
4041
  EpistolaAdminPageComponent] });
3009
4042
  }
@@ -3022,9 +4055,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3022
4055
  GenerateDocumentConfigurationComponent,
3023
4056
  CheckJobStatusConfigurationComponent,
3024
4057
  DownloadDocumentConfigurationComponent,
3025
- EpistolaDownloadComponent,
4058
+ EpistolaDocumentComponent,
3026
4059
  EpistolaRetryFormComponent,
3027
- EpistolaPreviewButtonComponent,
3028
4060
  EpistolaDocumentPreviewComponent,
3029
4061
  EpistolaAdminPageComponent,
3030
4062
  ],
@@ -3033,9 +4065,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3033
4065
  GenerateDocumentConfigurationComponent,
3034
4066
  CheckJobStatusConfigurationComponent,
3035
4067
  DownloadDocumentConfigurationComponent,
3036
- EpistolaDownloadComponent,
4068
+ EpistolaDocumentComponent,
3037
4069
  EpistolaRetryFormComponent,
3038
- EpistolaPreviewButtonComponent,
3039
4070
  EpistolaDocumentPreviewComponent,
3040
4071
  EpistolaAdminPageComponent,
3041
4072
  ],
@@ -3043,22 +4074,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3043
4074
  }]
3044
4075
  }] });
3045
4076
 
3046
- // Placeholder logo - a simple document icon in SVG format, base64 encoded
3047
- // TODO: Replace with actual Epistola logo
3048
- const EPISTOLA_PLUGIN_LOGO_BASE64 = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzMzNjZjYyI+PHBhdGggZD0iTTE0IDJINmMtMS4xIDAtMiAuOS0yIDJ2MTZjMCAxLjEuOSAyIDIgMmgxMmMxLjEgMCAyLS45IDItMlY4bC02LTZ6bTQgMThINlY0aDd2NWg1djExeiIvPjxwYXRoIGQ9Ik04IDEyaDh2Mkg4em0wIDRoOHYtMkg4em0wLThWNmg0djJ6Ii8+PC9zdmc+';
4077
+ const EPISTOLA_PLUGIN_LOGO_BASE64 = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAgMTIwIiB3aWR0aD0iMTIwIiBoZWlnaHQ9IjEyMCI+CiAgPCEtLSBTdGFjayBiYXNlIC0tPgogIDxyZWN0IHg9IjM2IiB5PSIxNiIgd2lkdGg9IjU0IiBoZWlnaHQ9IjcwIiByeD0iMyIgZmlsbD0iI2U2YzJiMCIgc3Ryb2tlPSIjNGYyZjJiIiBzdHJva2Utd2lkdGg9IjIiIHRyYW5zZm9ybT0icm90YXRlKDUgNjMgNTEpIi8+CiAgPHJlY3QgeD0iMzIiIHk9IjIyIiB3aWR0aD0iNTQiIGhlaWdodD0iNzAiIHJ4PSIzIiBmaWxsPSIjZjBkOGM4IiBzdHJva2U9IiM0ZjJmMmIiIHN0cm9rZS13aWR0aD0iMiIvPgogIDxyZWN0IHg9IjI4IiB5PSIyOCIgd2lkdGg9IjU0IiBoZWlnaHQ9IjcwIiByeD0iMyIgZmlsbD0iI2Y1ZWJlMyIgc3Ryb2tlPSIjNGYyZjJiIiBzdHJva2Utd2lkdGg9IjIuNSIvPgogIDxsaW5lIHgxPSIzOCIgeTE9IjQ0IiB4Mj0iNzIiIHkyPSI0NCIgc3Ryb2tlPSIjYzRhODgyIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgogIDxsaW5lIHgxPSIzOCIgeTE9IjU0IiB4Mj0iNzIiIHkyPSI1NCIgc3Ryb2tlPSIjYzRhODgyIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgogIDxsaW5lIHgxPSIzOCIgeTE9IjY0IiB4Mj0iNTgiIHkyPSI2NCIgc3Ryb2tlPSIjYzRhODgyIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgogIDwhLS0gV2F4IHNlYWwgd2l0aCBjaGVja21hcmsgLS0+CiAgPGNpcmNsZSBjeD0iNTUiIGN5PSI4NCIgcj0iMTUiIGZpbGw9IiNiODVjM2MiIHN0cm9rZT0iIzRmMmYyYiIgc3Ryb2tlLXdpZHRoPSIyIi8+CiAgPGNpcmNsZSBjeD0iNTUiIGN5PSI4NCIgcj0iMTAuNSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZDQ4MzZhIiBzdHJva2Utd2lkdGg9IjEiLz4KICA8cG9seWxpbmUgcG9pbnRzPSI0OSw4NCA1Myw4OSA2Miw3OCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNGYyZjJiIiBzdHJva2Utd2lkdGg9IjIuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo=';
3049
4078
 
4079
+ const EPISTOLA_PLUGIN_ID = 'epistola';
4080
+ const DISABLED_EPISTOLA_PLUGIN_ID = '__epistola_disabled__';
3050
4081
  const epistolaPluginSpecification = {
3051
- // Must match backend @Plugin(key = "epistola")
3052
- pluginId: 'epistola',
4082
+ get pluginId() {
4083
+ return isEpistolaEnabled() ? EPISTOLA_PLUGIN_ID : DISABLED_EPISTOLA_PLUGIN_ID;
4084
+ },
3053
4085
  // Component for plugin-level configuration (tenantId)
3054
4086
  pluginConfigurationComponent: EpistolaConfigurationComponent,
3055
4087
  // Plugin logo
3056
4088
  pluginLogoBase64: EPISTOLA_PLUGIN_LOGO_BASE64,
3057
4089
  // Map action keys to their configuration components
3058
4090
  functionConfigurationComponents: {
3059
- 'generate-document': GenerateDocumentConfigurationComponent,
3060
- 'check-job-status': CheckJobStatusConfigurationComponent,
3061
- 'download-document': DownloadDocumentConfigurationComponent,
4091
+ 'epistola-generate-document': GenerateDocumentConfigurationComponent,
4092
+ 'epistola-check-job-status': CheckJobStatusConfigurationComponent,
4093
+ 'epistola-download-document': DownloadDocumentConfigurationComponent,
3062
4094
  },
3063
4095
  // Translations
3064
4096
  pluginTranslations: {
@@ -3076,7 +4108,7 @@ const epistolaPluginSpecification = {
3076
4108
  defaultEnvironmentIdTooltip: 'De standaard omgeving voor documentgeneratie (3-30 tekens, alleen kleine letters, cijfers en koppeltekens, bijv. "productie")',
3077
4109
  templateSyncEnabled: 'Template synchronisatie',
3078
4110
  templateSyncEnabledTooltip: 'Synchroniseer template definities automatisch van het classpath naar Epistola bij het opstarten',
3079
- 'generate-document': 'Genereer Document',
4111
+ 'epistola-generate-document': 'Genereer Document',
3080
4112
  catalogId: 'Catalogus',
3081
4113
  catalogIdTooltip: 'Selecteer de catalogus waaruit een template gekozen wordt',
3082
4114
  templateId: 'Template',
@@ -3139,6 +4171,7 @@ const epistolaPluginSpecification = {
3139
4171
  requiredFieldsMissing: 'Niet alle verplichte velden zijn gekoppeld',
3140
4172
  requiredFieldsComplete: 'Alle verplichte velden zijn gekoppeld',
3141
4173
  validationSummary: 'verplichte velden gekoppeld',
4174
+ jsonataValidationErrorsHeading: 'Ongeldige JSONata-expressies:',
3142
4175
  fieldRequired: 'Verplicht',
3143
4176
  fieldOptional: 'Optioneel',
3144
4177
  mapCollectionTo: 'Koppel collectie aan',
@@ -3152,7 +4185,7 @@ const epistolaPluginSpecification = {
3152
4185
  sourceFieldPlaceholder: 'Bronveldnaam',
3153
4186
  noTemplateFields: 'Geen template velden beschikbaar',
3154
4187
  // Check job status action
3155
- 'check-job-status': 'Controleer Taakstatus',
4188
+ 'epistola-check-job-status': 'Controleer Taakstatus',
3156
4189
  requestIdVariable: 'Request ID Variabele',
3157
4190
  requestIdVariableTooltip: 'Naam van de procesvariabele met het Epistola request ID',
3158
4191
  statusVariable: 'Status Variabele',
@@ -3162,7 +4195,9 @@ const epistolaPluginSpecification = {
3162
4195
  errorMessageVariable: 'Foutmelding Variabele',
3163
4196
  errorMessageVariableTooltip: 'Naam van de procesvariabele waarin de foutmelding wordt opgeslagen (bij fout)',
3164
4197
  // Download document action
3165
- 'download-document': 'Download Document',
4198
+ 'epistola-download-document': 'Download Document',
4199
+ documentVariable: 'Document Variabele',
4200
+ documentVariableTooltip: 'Naam van de procesvariabele met het Epistola resultaat. Mag een String document ID zijn (legacy) of een rich-result object met een documentId-veld; de actie haalt het document ID eruit.',
3166
4201
  contentVariable: 'Inhoud Variabele',
3167
4202
  contentVariableTooltip: 'Naam van de procesvariabele waarin de documentinhoud (Base64) wordt opgeslagen',
3168
4203
  // Admin page
@@ -3189,6 +4224,16 @@ const epistolaPluginSpecification = {
3189
4224
  epistolaAdminNoPendingJobs: 'Geen wachtende taken voor deze verbinding.',
3190
4225
  epistolaAdminConfiguration: 'Configuratie',
3191
4226
  epistolaAdminRequestId: 'Request ID',
4227
+ epistolaAdminReconcile: 'Hersynchroniseer',
4228
+ epistolaAdminReconciling: 'Bezig...',
4229
+ epistolaAdminReconcileTooltip: 'Vraag de huidige status op bij Epistola en hervat het wachtende proces als het klaar is.',
4230
+ epistolaAdminConfigurations: 'Configuraties',
4231
+ epistolaAdminValidations: 'BPMN-validatie',
4232
+ epistolaAdminNoValidations: 'Geen race-onveilige procesdefinities gevonden. Alles ziet er goed uit.',
4233
+ epistolaAdminValidationWarningTitle: 'BPMN configuratie waarschuwing',
4234
+ epistolaAdminValidationWarningBody: 'In deze procesdefinities is de grens tussen de generate-document service task en de EpistolaDocumentGenerated catch event niet synchroon. Resultaten kunnen verloren gaan; gebruik in dat geval de Hersynchroniseer-knop in de Wachtende taken-tab.',
4235
+ epistolaAdminValidationCode: 'Code',
4236
+ epistolaAdminValidationMessage: 'Bericht',
3192
4237
  },
3193
4238
  en: {
3194
4239
  title: 'Epistola Document Suite',
@@ -3204,7 +4249,7 @@ const epistolaPluginSpecification = {
3204
4249
  defaultEnvironmentIdTooltip: 'The default environment for document generation (3-30 chars, lowercase letters, digits and hyphens only, e.g. "production")',
3205
4250
  templateSyncEnabled: 'Template sync',
3206
4251
  templateSyncEnabledTooltip: 'Automatically synchronize template definitions from classpath to Epistola on startup',
3207
- 'generate-document': 'Generate Document',
4252
+ 'epistola-generate-document': 'Generate Document',
3208
4253
  catalogId: 'Catalog',
3209
4254
  catalogIdTooltip: 'Select the catalog to choose a template from',
3210
4255
  templateId: 'Template',
@@ -3267,6 +4312,7 @@ const epistolaPluginSpecification = {
3267
4312
  requiredFieldsMissing: 'Not all required fields are mapped',
3268
4313
  requiredFieldsComplete: 'All required fields are mapped',
3269
4314
  validationSummary: 'required fields mapped',
4315
+ jsonataValidationErrorsHeading: 'Invalid JSONata expressions:',
3270
4316
  fieldRequired: 'Required',
3271
4317
  fieldOptional: 'Optional',
3272
4318
  mapCollectionTo: 'Map collection to',
@@ -3280,7 +4326,7 @@ const epistolaPluginSpecification = {
3280
4326
  sourceFieldPlaceholder: 'Source field name',
3281
4327
  noTemplateFields: 'No template fields available',
3282
4328
  // Check job status action
3283
- 'check-job-status': 'Check Job Status',
4329
+ 'epistola-check-job-status': 'Check Job Status',
3284
4330
  requestIdVariable: 'Request ID Variable',
3285
4331
  requestIdVariableTooltip: 'Name of the process variable containing the Epistola request ID',
3286
4332
  statusVariable: 'Status Variable',
@@ -3290,7 +4336,9 @@ const epistolaPluginSpecification = {
3290
4336
  errorMessageVariable: 'Error Message Variable',
3291
4337
  errorMessageVariableTooltip: 'Name of the process variable to store the error message in (when failed)',
3292
4338
  // Download document action
3293
- 'download-document': 'Download Document',
4339
+ 'epistola-download-document': 'Download Document',
4340
+ documentVariable: 'Document Variable',
4341
+ documentVariableTooltip: 'Name of the process variable holding the Epistola result. May be a String document id (legacy) or a rich result object with a `documentId` key — the action extracts the document id from it.',
3294
4342
  contentVariable: 'Content Variable',
3295
4343
  contentVariableTooltip: 'Name of the process variable to store the document content (Base64) in',
3296
4344
  // Admin page
@@ -3317,6 +4365,16 @@ const epistolaPluginSpecification = {
3317
4365
  epistolaAdminNoPendingJobs: 'No pending jobs for this connection.',
3318
4366
  epistolaAdminConfiguration: 'Configuration',
3319
4367
  epistolaAdminRequestId: 'Request ID',
4368
+ epistolaAdminReconcile: 'Reconcile',
4369
+ epistolaAdminReconciling: 'Reconciling...',
4370
+ epistolaAdminReconcileTooltip: "Ask Epistola for this job's current status and resume the waiting process if it has finished.",
4371
+ epistolaAdminConfigurations: 'Configurations',
4372
+ epistolaAdminValidations: 'BPMN validation',
4373
+ epistolaAdminNoValidations: 'No race-unsafe process definitions detected. Everything looks good.',
4374
+ epistolaAdminValidationWarningTitle: 'BPMN configuration warning',
4375
+ epistolaAdminValidationWarningBody: 'These process definitions have a non-synchronous boundary between the generate-document service task and the EpistolaDocumentGenerated catch event. Results can be missed; use the Reconcile button on the Pending Jobs tab to recover.',
4376
+ epistolaAdminValidationCode: 'Code',
4377
+ epistolaAdminValidationMessage: 'Message',
3320
4378
  },
3321
4379
  },
3322
4380
  };
@@ -3329,5 +4387,5 @@ const epistolaPluginSpecification = {
3329
4387
  * Generated bundle index. Do not edit.
3330
4388
  */
3331
4389
 
3332
- export { CheckJobStatusConfigurationComponent, DownloadDocumentConfigurationComponent, EPISTOLA_DOCUMENT_PREVIEW_OPTIONS, EPISTOLA_DOWNLOAD_OPTIONS, EPISTOLA_PREVIEW_BUTTON_OPTIONS, EPISTOLA_RETRY_FORM_OPTIONS, EpistolaAdminPageComponent, EpistolaAdminRoutingModule, EpistolaAdminService, EpistolaConfigurationComponent, EpistolaDocumentPreviewComponent, EpistolaDownloadComponent, EpistolaMenuService, EpistolaPluginModule, EpistolaPluginService, EpistolaPreviewButtonComponent, EpistolaRetryFormComponent, GenerateDocumentConfigurationComponent, JsonataEditorComponent, MappingBuilderComponent, epistolaPluginSpecification, errorResource, initialResource, loadingResource, registerEpistolaDocumentPreviewComponent, registerEpistolaDownloadComponent, registerEpistolaPreviewButtonComponent, registerEpistolaRetryFormComponent, successResource };
4390
+ export { CheckJobStatusConfigurationComponent, DownloadDocumentConfigurationComponent, EPISTOLA_DOCUMENT_OPTIONS, EPISTOLA_DOCUMENT_PREVIEW_OPTIONS, EPISTOLA_OVERRIDE_BUILDER_OPTIONS, EPISTOLA_PROCESS_LINK_SELECTOR_OPTIONS, EPISTOLA_RETRY_FORM_OPTIONS, EpistolaAdminPageComponent, EpistolaAdminRoutingModule, EpistolaAdminService, EpistolaConfigurationComponent, EpistolaDocumentComponent, EpistolaDocumentPreviewComponent, EpistolaMenuService, EpistolaOverrideBuilderComponent, EpistolaPluginModule, EpistolaPluginService, EpistolaProcessLinkSelectorComponent, EpistolaRetryFormComponent, EpistolaTaskContextInterceptor, EpistolaTaskContextService, GenerateDocumentConfigurationComponent, JsonataEditorComponent, MappingBuilderComponent, epistolaPluginSpecification, errorResource, initialResource, isEpistolaEnabled, loadingResource, registerEpistolaDocumentComponent, registerEpistolaDocumentPreviewComponent, registerEpistolaOverrideBuilderComponent, registerEpistolaProcessLinkSelectorComponent, registerEpistolaRetryFormComponent, successResource };
3333
4391
  //# sourceMappingURL=epistola.app-valtimo-plugin.mjs.map