@epistola.app/valtimo-plugin 0.9.4 → 0.11.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 (30) hide show
  1. package/fesm2022/epistola.app-valtimo-plugin.mjs +2430 -395
  2. package/fesm2022/epistola.app-valtimo-plugin.mjs.map +1 -1
  3. package/lib/components/check-job-status-configuration/check-job-status-configuration.component.d.ts +2 -2
  4. package/lib/components/download-document-configuration/download-document-config.util.d.ts +15 -0
  5. package/lib/components/download-document-configuration/download-document-configuration.component.d.ts +12 -3
  6. package/lib/components/epistola-admin-page/epistola-admin-page.component.d.ts +28 -4
  7. package/lib/components/epistola-configuration/epistola-configuration.component.d.ts +2 -2
  8. package/lib/components/epistola-document/epistola-document.component.d.ts +11 -7
  9. package/lib/components/epistola-document-preview/epistola-document-preview.component.d.ts +57 -13
  10. package/lib/components/epistola-document-preview/preview-utils.d.ts +39 -6
  11. package/lib/components/epistola-retry-form/epistola-retry-form.component.d.ts +9 -7
  12. package/lib/components/formio-builder-utils.d.ts +14 -0
  13. package/lib/components/generate-document-configuration/generate-document-configuration.component.d.ts +4 -2
  14. package/lib/components/jsonata-editor/jsonata-editor.component.d.ts +11 -3
  15. package/lib/components/override-builder/legacy-override-converter.d.ts +23 -0
  16. package/lib/components/override-builder/override-builder.component.d.ts +52 -20
  17. package/lib/components/override-builder/override-jsonata.d.ts +25 -0
  18. package/lib/models/admin.d.ts +57 -0
  19. package/lib/models/config.d.ts +25 -1
  20. package/lib/services/epistola-admin.service.d.ts +13 -4
  21. package/lib/services/epistola-plugin.service.d.ts +16 -10
  22. package/lib/services/index.d.ts +1 -2
  23. package/lib/services/prefilled-task-id.d.ts +52 -0
  24. package/lib/utils/extract-referenced-paths.d.ts +19 -0
  25. package/lib/utils/jsonata-monaco.d.ts +2 -2
  26. package/package.json +2 -1
  27. package/sbom.json +1 -1
  28. package/lib/services/epistola-task-context.interceptor.d.ts +0 -29
  29. package/lib/services/epistola-task-context.matcher.d.ts +0 -19
  30. package/lib/services/epistola-task-context.service.d.ts +0 -26
@@ -1,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { Injectable, EventEmitter, Output, Input, Component, ChangeDetectionStrategy, inject, NgModule, ENVIRONMENT_INITIALIZER, Injector } from '@angular/core';
3
3
  import * as i1 from '@angular/common/http';
4
- import { HttpHeaders, HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
4
+ import { HttpHeaders, 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,21 +12,56 @@ 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 i2$2 from '@angular/forms';
15
+ import * as i3$1 from '@angular/forms';
16
16
  import { FormsModule } from '@angular/forms';
17
17
  import * as _jsonata from 'jsonata';
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';
18
+ import * as i2$2 from '@valtimo/process-link';
19
+ import * as i2$3 from '@angular/platform-browser';
20
+ import * as i4 from '@formio/angular';
21
21
  import { FormioModule } from '@formio/angular';
22
- import * as i2$5 from '@angular/router';
22
+ import * as i2$4 from '@angular/router';
23
23
  import { RouterModule, Router } from '@angular/router';
24
- import * as i5$1 from 'carbon-components-angular/tabs';
24
+ import * as i5 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';
28
28
  import { AuthGuardService } from '@valtimo/security';
29
29
 
30
+ /*
31
+ * Copyright 2025 Epistola.
32
+ *
33
+ * Licensed under EUPL, Version 1.2 (the "License");
34
+ * you may not use this file except in compliance with the License.
35
+ * You may obtain a copy of the License at
36
+ *
37
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
38
+ *
39
+ * Unless required by applicable law or agreed to in writing, software
40
+ * distributed under the License is distributed on an "AS IS" basis,
41
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
42
+ * See the License for the specific language governing permissions and
43
+ * limitations under the License.
44
+ *
45
+ * SPDX-License-Identifier: EUPL-1.2
46
+ */
47
+
48
+ /*
49
+ * Copyright 2025 Epistola.
50
+ *
51
+ * Licensed under EUPL, Version 1.2 (the "License");
52
+ * you may not use this file except in compliance with the License.
53
+ * You may obtain a copy of the License at
54
+ *
55
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
56
+ *
57
+ * Unless required by applicable law or agreed to in writing, software
58
+ * distributed under the License is distributed on an "AS IS" basis,
59
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
60
+ * See the License for the specific language governing permissions and
61
+ * limitations under the License.
62
+ *
63
+ * SPDX-License-Identifier: EUPL-1.2
64
+ */
30
65
  function initialResource(empty) {
31
66
  return { data: empty, loading: false, error: null };
32
67
  }
@@ -40,6 +75,95 @@ function errorResource(current, error) {
40
75
  return { data: current, loading: false, error };
41
76
  }
42
77
 
78
+ /*
79
+ * Copyright 2025 Epistola.
80
+ *
81
+ * Licensed under EUPL, Version 1.2 (the "License");
82
+ * you may not use this file except in compliance with the License.
83
+ * You may obtain a copy of the License at
84
+ *
85
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
86
+ *
87
+ * Unless required by applicable law or agreed to in writing, software
88
+ * distributed under the License is distributed on an "AS IS" basis,
89
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
90
+ * See the License for the specific language governing permissions and
91
+ * limitations under the License.
92
+ *
93
+ * SPDX-License-Identifier: EUPL-1.2
94
+ */
95
+
96
+ /*
97
+ * Copyright 2025 Epistola.
98
+ *
99
+ * Licensed under EUPL, Version 1.2 (the "License");
100
+ * you may not use this file except in compliance with the License.
101
+ * You may obtain a copy of the License at
102
+ *
103
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
104
+ *
105
+ * Unless required by applicable law or agreed to in writing, software
106
+ * distributed under the License is distributed on an "AS IS" basis,
107
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
108
+ * See the License for the specific language governing permissions and
109
+ * limitations under the License.
110
+ *
111
+ * SPDX-License-Identifier: EUPL-1.2
112
+ */
113
+
114
+ /*
115
+ * Copyright 2025 Epistola.
116
+ *
117
+ * Licensed under EUPL, Version 1.2 (the "License");
118
+ * you may not use this file except in compliance with the License.
119
+ * You may obtain a copy of the License at
120
+ *
121
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
122
+ *
123
+ * Unless required by applicable law or agreed to in writing, software
124
+ * distributed under the License is distributed on an "AS IS" basis,
125
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
126
+ * See the License for the specific language governing permissions and
127
+ * limitations under the License.
128
+ *
129
+ * SPDX-License-Identifier: EUPL-1.2
130
+ */
131
+
132
+ /*
133
+ * Copyright 2025 Epistola.
134
+ *
135
+ * Licensed under EUPL, Version 1.2 (the "License");
136
+ * you may not use this file except in compliance with the License.
137
+ * You may obtain a copy of the License at
138
+ *
139
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
140
+ *
141
+ * Unless required by applicable law or agreed to in writing, software
142
+ * distributed under the License is distributed on an "AS IS" basis,
143
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
144
+ * See the License for the specific language governing permissions and
145
+ * limitations under the License.
146
+ *
147
+ * SPDX-License-Identifier: EUPL-1.2
148
+ */
149
+
150
+ /*
151
+ * Copyright 2025 Epistola.
152
+ *
153
+ * Licensed under EUPL, Version 1.2 (the "License");
154
+ * you may not use this file except in compliance with the License.
155
+ * You may obtain a copy of the License at
156
+ *
157
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
158
+ *
159
+ * Unless required by applicable law or agreed to in writing, software
160
+ * distributed under the License is distributed on an "AS IS" basis,
161
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
162
+ * See the License for the specific language governing permissions and
163
+ * limitations under the License.
164
+ *
165
+ * SPDX-License-Identifier: EUPL-1.2
166
+ */
43
167
  /**
44
168
  * Service for Epistola plugin administrative operations.
45
169
  * Provides health checks, version info, and usage overview.
@@ -88,10 +212,11 @@ class EpistolaAdminService {
88
212
  return this.http.post(`${this.apiEndpoint}/pending/${encodeURIComponent(executionId)}/reconcile`, null);
89
213
  }
90
214
  /**
91
- * Get the latest BPMN race-safety validation violations across deployed
92
- * process definitions. Empty list = healthy.
215
+ * Get the latest BPMN race-safety validation report across deployed process
216
+ * definitions: the violations (empty = healthy) plus when it was last checked
217
+ * and how often it refreshes.
93
218
  */
94
- getValidationViolations() {
219
+ getValidationReport() {
95
220
  return this.http.get(`${this.apiEndpoint}/validations`);
96
221
  }
97
222
  /**
@@ -126,6 +251,24 @@ class EpistolaAdminService {
126
251
  responseType: 'blob',
127
252
  });
128
253
  }
254
+ // ---- TEMPORARY (removed in 1.0.0): task-id carrier detection + repair ----
255
+ /** Forms whose Epistola components are missing the task-id carrier. */
256
+ getFormCarrierIssues() {
257
+ return this.http.get(`${this.apiEndpoint}/forms/carrier-issues`);
258
+ }
259
+ /** Inject the task-id carrier into a single form's Epistola components. */
260
+ repairFormCarrier(formId) {
261
+ return this.http.post(`${this.apiEndpoint}/forms/${encodeURIComponent(formId)}/repair-carrier`, null);
262
+ }
263
+ /** Repair every flagged form. */
264
+ repairAllFormCarriers() {
265
+ return this.http.post(`${this.apiEndpoint}/forms/repair-carrier`, null);
266
+ }
267
+ // ---- TEMPORARY: legacy override-mapping format detection ----
268
+ /** Forms whose preview components still use the legacy override-mapping object format. */
269
+ getLegacyOverrideForms() {
270
+ return this.http.get(`${this.apiEndpoint}/forms/legacy-override`);
271
+ }
129
272
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminService, deps: [{ token: i1.HttpClient }, { token: i2.ConfigService }], target: i0.ɵɵFactoryTarget.Injectable });
130
273
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminService });
131
274
  }
@@ -133,6 +276,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
133
276
  type: Injectable
134
277
  }], ctorParameters: () => [{ type: i1.HttpClient }, { type: i2.ConfigService }] });
135
278
 
279
+ /*
280
+ * Copyright 2025 Epistola.
281
+ *
282
+ * Licensed under EUPL, Version 1.2 (the "License");
283
+ * you may not use this file except in compliance with the License.
284
+ * You may obtain a copy of the License at
285
+ *
286
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
287
+ *
288
+ * Unless required by applicable law or agreed to in writing, software
289
+ * distributed under the License is distributed on an "AS IS" basis,
290
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
291
+ * See the License for the specific language governing permissions and
292
+ * limitations under the License.
293
+ *
294
+ * SPDX-License-Identifier: EUPL-1.2
295
+ */
136
296
  /**
137
297
  * Registers the Epistola admin page menu item under the Admin > Other section.
138
298
  * Instantiated eagerly via ENVIRONMENT_INITIALIZER so the menu item
@@ -169,6 +329,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
169
329
  type: Injectable
170
330
  }], ctorParameters: () => [{ type: i3.MenuService }] });
171
331
 
332
+ /*
333
+ * Copyright 2025 Epistola.
334
+ *
335
+ * Licensed under EUPL, Version 1.2 (the "License");
336
+ * you may not use this file except in compliance with the License.
337
+ * You may obtain a copy of the License at
338
+ *
339
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
340
+ *
341
+ * Unless required by applicable law or agreed to in writing, software
342
+ * distributed under the License is distributed on an "AS IS" basis,
343
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
344
+ * See the License for the specific language governing permissions and
345
+ * limitations under the License.
346
+ *
347
+ * SPDX-License-Identifier: EUPL-1.2
348
+ */
172
349
  /**
173
350
  * Service for interacting with Epistola plugin API endpoints.
174
351
  * Provides methods to fetch templates, environments, variants,
@@ -236,6 +413,16 @@ class EpistolaPluginService {
236
413
  params: { processDefinitionKey },
237
414
  });
238
415
  }
416
+ /**
417
+ * Get the raw `dataMapping` JSONata of a generate-document process link, identified by its
418
+ * process definition key and activity id. The override builder extracts the referenced
419
+ * `$doc`/`$pv` paths from it to guide the author. Returns an empty mapping when unresolved.
420
+ */
421
+ getProcessLinkMapping(processDefinitionKey, activityId) {
422
+ return this.http.get(`${this.apiEndpoint}/process-link-mapping`, {
423
+ params: { processDefinitionKey, activityId },
424
+ });
425
+ }
239
426
  /**
240
427
  * Get variable suggestions for JSONata autocompletion.
241
428
  */
@@ -262,13 +449,13 @@ class EpistolaPluginService {
262
449
  /**
263
450
  * Get a dynamically generated Formio form for retrying a failed document generation.
264
451
  *
452
+ * The backend derives the process instance and case document from the authorized task,
453
+ * so only the task id (and optionally the source activity) is sent.
454
+ *
265
455
  * @param taskId Operaton user task id (required — backend authorizes via OperatonTask:VIEW)
266
456
  */
267
- getRetryForm(taskId, processInstanceId, documentId, sourceActivityId) {
268
- const params = { taskId, processInstanceId };
269
- if (documentId) {
270
- params['documentId'] = documentId;
271
- }
457
+ getRetryForm(taskId, sourceActivityId) {
458
+ const params = { taskId };
272
459
  if (sourceActivityId) {
273
460
  params['sourceActivityId'] = sourceActivityId;
274
461
  }
@@ -327,107 +514,158 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
327
514
  type: Injectable
328
515
  }], ctorParameters: () => [{ type: i1.HttpClient }, { type: i2.ConfigService }] });
329
516
 
330
- /**
331
- * Holds the currently-open Operaton user task instance id so that custom
332
- * Formio components rendered inside a Valtimo task form (preview, download,
333
- * retry-form) can include it in backend requests for PBAC.
334
- *
335
- * Populated by {@link EpistolaTaskContextInterceptor}, which sniffs Valtimo's
336
- * canonical "load process link for task" GET (`/api/v2/process-link/task/{taskId}`)
337
- * — the request always fires when a task opens, before the form renders.
338
- *
339
- * <p><b>Why this exists:</b> Valtimo 13.21 does not expose the active task
340
- * instance id through any service that custom Formio components can inject
341
- * (`FormIoStateService` carries documentId and processInstanceId only;
342
- * `TaskDetailContentComponent.taskInstanceId$` is private to that component
343
- * and Formio elements live in their own injector tree). This service is a
344
- * workaround until upstream exposes `taskInstanceId` via `FormIoStateService`.
517
+ /*
518
+ * Copyright 2025 Epistola.
519
+ *
520
+ * Licensed under EUPL, Version 1.2 (the "License");
521
+ * you may not use this file except in compliance with the License.
522
+ * You may obtain a copy of the License at
523
+ *
524
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
525
+ *
526
+ * Unless required by applicable law or agreed to in writing, software
527
+ * distributed under the License is distributed on an "AS IS" basis,
528
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
529
+ * See the License for the specific language governing permissions and
530
+ * limitations under the License.
531
+ *
532
+ * SPDX-License-Identifier: EUPL-1.2
345
533
  */
346
- class EpistolaTaskContextService {
347
- _taskInstanceId$ = new BehaviorSubject(null);
348
- taskInstanceId$ = this._taskInstanceId$.asObservable();
349
- get taskInstanceId() {
350
- return this._taskInstanceId$.value;
351
- }
352
- setTaskInstanceId(id) {
353
- this._taskInstanceId$.next(id);
354
- }
355
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
356
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextService, providedIn: 'root' });
357
- }
358
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextService, decorators: [{
359
- type: Injectable,
360
- args: [{ providedIn: 'root' }]
361
- }] });
362
-
363
534
  /**
364
- * Pure helpers for {@link EpistolaTaskContextInterceptor}, extracted so they
365
- * can be unit-tested without pulling in {@code @angular/core} (which ts-jest
366
- * cannot transform without {@code jest-preset-angular}).
535
+ * Helpers for reading the active user task's id out of a Valtimo task form that was
536
+ * prefilled server-side by the {@code epistola:} value resolver (see the backend
537
+ * {@code EpistolaTaskValueResolverFactory}).
538
+ *
539
+ * <p>Background: the Epistola Formio components need the id of the user task whose form
540
+ * they're rendered in, to authorize their backend requests ({@code OperatonTask:VIEW}).
541
+ * Valtimo exposes no service that carries the task id to a custom Formio component at
542
+ * runtime, and earlier URL-sniffing only worked in the direct task-open flow (the
543
+ * task-list / case-detail flow bulk-fetches process links and never fires the per-task
544
+ * call).
545
+ *
546
+ * <p>Form prefill, however, runs server-side in every flow. A form field with
547
+ * {@code properties.sourceKey = "epistola:taskId"} is filled with the task id at prefill
548
+ * time (by the backend {@code EpistolaTaskValueResolverFactory}); this helper reads it back
549
+ * from the Formio root — robustly, regardless of how the task was opened.
367
550
  */
551
+ /** The value-resolver source key that yields the current task id at prefill time. */
552
+ const PREFILLED_TASK_ID_SOURCE_KEY = 'epistola:taskId';
553
+ /** Conventional key of the hidden carrier field that holds the prefilled task id. */
554
+ const PREFILLED_TASK_ID_DATA_KEY = 'epistolaTaskId';
368
555
  /**
369
- * Pattern Valtimo uses for the canonical task-open call:
370
- * {@code GET /api/v2/process-link/task/{taskInstanceId}}.
556
+ * Hidden Formio child component that carries the prefilled task id. It is embedded as a
557
+ * nested component inside each Epistola task component's schema, so dropping that component
558
+ * brings the carrier with it — the form author never adds a separate field. Valtimo's
559
+ * server-side prefill fills its {@code defaultValue} from the {@code epistola:taskId}
560
+ * value resolver; {@link readPrefilledTaskId} reads it back from the form definition.
371
561
  *
372
- * Captures the {@code taskInstanceId} (UUID v4-style 36-character hyphenated
373
- * hex string). Anchored at the end of the URL or at a query-string delimiter
374
- * so we don't accidentally match a longer trailing segment.
562
+ * {@code persistent: false} keeps the value out of the submission, so the task id never
563
+ * lands in the case document / process variables.
375
564
  */
376
- const TASK_PROCESS_LINK_PATTERN = /\/api\/v2\/process-link\/task\/([0-9a-fA-F-]{36})(?:\?|$)/;
565
+ const PREFILLED_TASK_ID_CARRIER = {
566
+ type: 'hidden',
567
+ key: PREFILLED_TASK_ID_DATA_KEY,
568
+ input: true,
569
+ persistent: false,
570
+ label: 'Epistola Task Id',
571
+ properties: { sourceKey: PREFILLED_TASK_ID_SOURCE_KEY },
572
+ };
377
573
  /**
378
- * Returns the captured {@code taskInstanceId} from a Valtimo task-open
379
- * request URL, or {@code null} if the request does not match.
574
+ * Reads the prefilled task id from a Formio webform/wizard root, or null when absent.
575
+ *
576
+ * Looks in two places, in order:
577
+ * 1. The (prefilled) form definition — any component whose {@code properties.sourceKey}
578
+ * is {@code epistola:taskId} carries the task id in its {@code defaultValue}. This works
579
+ * even when the carrier is a hidden field that Formio doesn't surface into submission data.
580
+ * 2. The submission data under {@link PREFILLED_TASK_ID_DATA_KEY}, for a rendered sibling
581
+ * hidden field whose value Formio copied into {@code root.data}.
380
582
  */
381
- function extractTaskInstanceIdFromUrl(method, url) {
382
- if (method !== 'GET')
583
+ function readPrefilledTaskId(root) {
584
+ if (!root) {
383
585
  return null;
384
- const match = TASK_PROCESS_LINK_PATTERN.exec(url);
385
- return match ? match[1] : null;
586
+ }
587
+ const fromForm = findSourceKeyDefaultValue(root.form, PREFILLED_TASK_ID_SOURCE_KEY);
588
+ if (typeof fromForm === 'string' && fromForm.length > 0) {
589
+ return fromForm;
590
+ }
591
+ const fromData = root.data?.[PREFILLED_TASK_ID_DATA_KEY];
592
+ if (typeof fromData === 'string' && fromData.length > 0) {
593
+ return fromData;
594
+ }
595
+ return null;
386
596
  }
387
-
388
597
  /**
389
- * Sniffs Valtimo's task-open signal and pushes the active taskInstanceId into
390
- * {@link EpistolaTaskContextService}. The signal is the canonical
391
- * {@code GET /api/v2/process-link/task/{taskId}} call that
392
- * {@code TaskDetailContentComponent.loadTaskDetails(...)} fires unconditionally
393
- * before any task form is rendered (see @valtimo/task internals).
394
- *
395
- * <p>This interceptor does <b>not</b> modify the outgoing request. It only
396
- * captures the taskId from the URL.
397
- *
398
- * <p>Workaround for Valtimo 13.21 not exposing taskInstanceId through any
399
- * injectable service. Remove once upstream adds e.g.
400
- * {@code FormIoStateService.setTaskInstanceId(...)}.
401
- *
402
- * <p>The actual URL-matching logic lives in
403
- * {@link extractTaskInstanceIdFromUrl} so it can be unit-tested without an
404
- * Angular harness.
598
+ * Deep-walks a form definition node looking for a component whose
599
+ * {@code properties.sourceKey} equals {@code sourceKey}, and returns its
600
+ * {@code defaultValue} (the prefilled value). Returns null when not found.
405
601
  */
406
- class EpistolaTaskContextInterceptor {
407
- taskContext;
408
- constructor(taskContext) {
409
- this.taskContext = taskContext;
410
- }
411
- intercept(request, next) {
412
- const taskId = extractTaskInstanceIdFromUrl(request.method, request.url);
413
- if (taskId !== null && taskId !== this.taskContext.taskInstanceId) {
414
- this.taskContext.setTaskInstanceId(taskId);
602
+ function findSourceKeyDefaultValue(node, sourceKey) {
603
+ if (Array.isArray(node)) {
604
+ for (const item of node) {
605
+ const found = findSourceKeyDefaultValue(item, sourceKey);
606
+ if (found != null) {
607
+ return found;
608
+ }
609
+ }
610
+ return null;
611
+ }
612
+ if (node && typeof node === 'object') {
613
+ if (node.properties?.sourceKey === sourceKey && typeof node.defaultValue === 'string') {
614
+ return node.defaultValue;
615
+ }
616
+ for (const key of Object.keys(node)) {
617
+ const found = findSourceKeyDefaultValue(node[key], sourceKey);
618
+ if (found != null) {
619
+ return found;
620
+ }
415
621
  }
416
- return next.handle(request);
417
622
  }
418
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextInterceptor, deps: [{ token: EpistolaTaskContextService }], target: i0.ɵɵFactoryTarget.Injectable });
419
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextInterceptor });
623
+ return null;
420
624
  }
421
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaTaskContextInterceptor, decorators: [{
422
- type: Injectable
423
- }], ctorParameters: () => [{ type: EpistolaTaskContextService }] });
424
625
 
626
+ /*
627
+ * Copyright 2025 Epistola.
628
+ *
629
+ * Licensed under EUPL, Version 1.2 (the "License");
630
+ * you may not use this file except in compliance with the License.
631
+ * You may obtain a copy of the License at
632
+ *
633
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
634
+ *
635
+ * Unless required by applicable law or agreed to in writing, software
636
+ * distributed under the License is distributed on an "AS IS" basis,
637
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
638
+ * See the License for the specific language governing permissions and
639
+ * limitations under the License.
640
+ *
641
+ * SPDX-License-Identifier: EUPL-1.2
642
+ */
643
+
644
+ /*
645
+ * Copyright 2025 Epistola.
646
+ *
647
+ * Licensed under EUPL, Version 1.2 (the "License");
648
+ * you may not use this file except in compliance with the License.
649
+ * You may obtain a copy of the License at
650
+ *
651
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
652
+ *
653
+ * Unless required by applicable law or agreed to in writing, software
654
+ * distributed under the License is distributed on an "AS IS" basis,
655
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
656
+ * See the License for the specific language governing permissions and
657
+ * limitations under the License.
658
+ *
659
+ * SPDX-License-Identifier: EUPL-1.2
660
+ */
425
661
  class EpistolaConfigurationComponent {
426
662
  save$;
427
663
  disabled$;
428
664
  pluginId;
429
665
  prefillConfiguration$;
430
666
  valid = new EventEmitter();
667
+ // Framework's PluginConfigurationData (index type) to satisfy the invariant
668
+ // EventEmitter contract under strict mode; emitted values remain the typed config.
431
669
  configuration = new EventEmitter();
432
670
  /** Epistola slug pattern: lowercase alphanumeric with hyphens, no leading/trailing hyphens. */
433
671
  static SLUG_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
@@ -476,7 +714,7 @@ class EpistolaConfigurationComponent {
476
714
  combineLatest([this.formValue$, this.valid$])
477
715
  .pipe(take(1))
478
716
  .subscribe(([formValue, valid]) => {
479
- if (valid) {
717
+ if (valid && formValue) {
480
718
  this.configuration.emit(formValue);
481
719
  }
482
720
  });
@@ -502,12 +740,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
502
740
  type: Output
503
741
  }] } });
504
742
 
743
+ /*
744
+ * Copyright 2025 Epistola.
745
+ *
746
+ * Licensed under EUPL, Version 1.2 (the "License");
747
+ * you may not use this file except in compliance with the License.
748
+ * You may obtain a copy of the License at
749
+ *
750
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
751
+ *
752
+ * Unless required by applicable law or agreed to in writing, software
753
+ * distributed under the License is distributed on an "AS IS" basis,
754
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
755
+ * See the License for the specific language governing permissions and
756
+ * limitations under the License.
757
+ *
758
+ * SPDX-License-Identifier: EUPL-1.2
759
+ */
505
760
  /**
506
761
  * Shared state for the JSONata completion provider.
507
762
  * Updated by the editor component when suggestions/functions change.
508
763
  */
509
764
  const jsonataCompletionData = {
510
- suggestions: null,
765
+ // Context variables in scope, keyed by name (without the `$`), each mapping to
766
+ // its known field/path suggestions. The completion provider derives both the
767
+ // `$`-variable list and the `$<name>.` field list from this — adding a new
768
+ // context variable needs no provider change, just another key here.
769
+ // e.g. { doc: ['name', 'address.street'], pv: ['amount'], form: ['voornaam'] }
770
+ variables: {},
511
771
  functions: [],
512
772
  };
513
773
  /**
@@ -590,7 +850,8 @@ function registerJsonataLanguage(monaco) {
590
850
  const CompletionItemKind = monaco.languages.CompletionItemKind;
591
851
  // After "$" — suggest variables and functions
592
852
  if (textUntilPosition.endsWith('$')) {
593
- suggestions.push(...['doc', 'pv', 'case'].map((v) => ({
853
+ // Variables are whatever the host put in scope (doc/pv/case/form/…).
854
+ suggestions.push(...Object.keys(jsonataCompletionData.variables).map((v) => ({
594
855
  label: `$${v}`,
595
856
  kind: CompletionItemKind.Variable,
596
857
  insertText: v,
@@ -645,27 +906,20 @@ function registerJsonataLanguage(monaco) {
645
906
  });
646
907
  }
647
908
  }
648
- // After "$doc." — suggest document paths
649
- if (/\$doc\.\s*$/.test(textUntilPosition) || /\$doc\.[a-zA-Z_]*$/.test(textUntilPosition)) {
650
- const docPaths = jsonataCompletionData.suggestions?.doc || [];
651
- for (const path of docPaths) {
909
+ // After "$<name>." — suggest that variable's fields. The variable name is
910
+ // captured generically, so doc/pv/case/form/… all work from one branch.
911
+ const fieldMatch = textUntilPosition.match(/\$([a-zA-Z_]\w*)\.[a-zA-Z_]*$/);
912
+ if (fieldMatch) {
913
+ const fields = jsonataCompletionData.variables[fieldMatch[1]] || [];
914
+ for (const field of fields) {
652
915
  suggestions.push({
653
- label: path,
916
+ label: field,
654
917
  kind: CompletionItemKind.Field,
655
- insertText: path,
656
- detail: 'Document field',
657
- });
658
- }
659
- }
660
- // After "$pv." — suggest process variables
661
- if (/\$pv\.\s*$/.test(textUntilPosition) || /\$pv\.[a-zA-Z_]*$/.test(textUntilPosition)) {
662
- const pvNames = jsonataCompletionData.suggestions?.pv || [];
663
- for (const name of pvNames) {
664
- suggestions.push({
665
- label: name,
666
- kind: CompletionItemKind.Variable,
667
- insertText: name,
668
- detail: 'Process variable',
918
+ // Path-style suggestions (doc/pv: `a.b`, `a[].b`) insert as-is; keys
919
+ // with characters invalid in a bare JSONata name (e.g. "pv:motivation")
920
+ // are backtick-quoted so they resolve as a single property.
921
+ insertText: /[^A-Za-z0-9_.[\]]/.test(field) ? '`' + field + '`' : field,
922
+ detail: `$${fieldMatch[1]} field`,
669
923
  });
670
924
  }
671
925
  }
@@ -674,12 +928,37 @@ function registerJsonataLanguage(monaco) {
674
928
  });
675
929
  }
676
930
 
677
- const jsonata$1 = _jsonata.default || _jsonata;
931
+ /*
932
+ * Copyright 2025 Epistola.
933
+ *
934
+ * Licensed under EUPL, Version 1.2 (the "License");
935
+ * you may not use this file except in compliance with the License.
936
+ * You may obtain a copy of the License at
937
+ *
938
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
939
+ *
940
+ * Unless required by applicable law or agreed to in writing, software
941
+ * distributed under the License is distributed on an "AS IS" basis,
942
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
943
+ * See the License for the specific language governing permissions and
944
+ * limitations under the License.
945
+ *
946
+ * SPDX-License-Identifier: EUPL-1.2
947
+ */
948
+ const jsonata$4 = _jsonata.default || _jsonata;
678
949
  class JsonataEditorComponent {
679
950
  expression = '';
680
951
  disabled = false;
681
- suggestions = null;
952
+ /**
953
+ * Context variables in scope, keyed by name (without `$`), each mapping to its
954
+ * field/path suggestions — e.g. `{ doc: [...], pv: [...] }` for the data
955
+ * mapping, `{ form: [...] }` for the override builder. Drives both the
956
+ * `$`-variable list and `$<name>.` field completion.
957
+ */
958
+ contextVariables = {};
682
959
  functions = [];
960
+ /** Footer hint listing the context variables in scope. */
961
+ variablesHint = '$doc · $pv · $case';
683
962
  expressionChange = new EventEmitter();
684
963
  validChange = new EventEmitter();
685
964
  editorModel = { value: '', language: 'jsonata' };
@@ -709,8 +988,8 @@ class JsonataEditorComponent {
709
988
  this.editorModel = { value: this.expression || '', language: 'jsonata' };
710
989
  this.validate$.next(this.expression);
711
990
  }
712
- if (changes['suggestions']) {
713
- jsonataCompletionData.suggestions = this.suggestions;
991
+ if (changes['contextVariables']) {
992
+ jsonataCompletionData.variables = this.contextVariables || {};
714
993
  }
715
994
  if (changes['functions']) {
716
995
  jsonataCompletionData.functions = this.functions;
@@ -738,7 +1017,7 @@ class JsonataEditorComponent {
738
1017
  if (m) {
739
1018
  registerJsonataLanguage(m);
740
1019
  this.languageRegistered = true;
741
- jsonataCompletionData.suggestions = this.suggestions;
1020
+ jsonataCompletionData.variables = this.contextVariables || {};
742
1021
  jsonataCompletionData.functions = this.functions;
743
1022
  }
744
1023
  }
@@ -749,7 +1028,7 @@ class JsonataEditorComponent {
749
1028
  return;
750
1029
  }
751
1030
  try {
752
- jsonata$1(value);
1031
+ jsonata$4(value);
753
1032
  this.error = null;
754
1033
  this.validChange.emit(true);
755
1034
  }
@@ -759,7 +1038,7 @@ class JsonataEditorComponent {
759
1038
  }
760
1039
  }
761
1040
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: JsonataEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
762
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: JsonataEditorComponent, isStandalone: true, selector: "epistola-jsonata-editor", inputs: { expression: "expression", disabled: "disabled", suggestions: "suggestions", functions: "functions" }, outputs: { expressionChange: "expressionChange", validChange: "validChange" }, usesOnChanges: true, ngImport: i0, template: `
1041
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: JsonataEditorComponent, isStandalone: true, selector: "epistola-jsonata-editor", inputs: { expression: "expression", disabled: "disabled", contextVariables: "contextVariables", functions: "functions", variablesHint: "variablesHint" }, outputs: { expressionChange: "expressionChange", validChange: "validChange" }, usesOnChanges: true, ngImport: i0, template: `
763
1042
  <div class="jsonata-editor">
764
1043
  <valtimo-editor
765
1044
  [model]="editorModel"
@@ -772,7 +1051,7 @@ class JsonataEditorComponent {
772
1051
  <div class="jsonata-editor__footer">
773
1052
  <span *ngIf="error" class="jsonata-editor__error">{{ error }}</span>
774
1053
  <span *ngIf="!error && expression" class="jsonata-editor__valid">&#x2713;</span>
775
- <span class="jsonata-editor__variables">$doc · $pv · $case</span>
1054
+ <span class="jsonata-editor__variables">{{ variablesHint }}</span>
776
1055
  </div>
777
1056
  </div>
778
1057
  `, isInline: true, styles: [".jsonata-editor__footer{display:flex;align-items:center;gap:8px;margin-top:4px;font-size:.8em}.jsonata-editor__error{color:#da1e28}.jsonata-editor__valid{color:#198038}.jsonata-editor__variables{margin-left:auto;color:#8d8d8d;font-family:monospace}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "ngmodule", type: EditorModule }, { kind: "component", type: i3.EditorComponent, selector: "valtimo-editor", inputs: ["editorOptions", "model", "disabled", "formatOnLoad", "widthPx", "heightPx", "heightStyle", "jsonSchema", "fitPage", "fitPageSpaceAdjustment"], outputs: ["validEvent", "valueChangeEvent"] }] });
@@ -792,7 +1071,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
792
1071
  <div class="jsonata-editor__footer">
793
1072
  <span *ngIf="error" class="jsonata-editor__error">{{ error }}</span>
794
1073
  <span *ngIf="!error && expression" class="jsonata-editor__valid">&#x2713;</span>
795
- <span class="jsonata-editor__variables">$doc · $pv · $case</span>
1074
+ <span class="jsonata-editor__variables">{{ variablesHint }}</span>
796
1075
  </div>
797
1076
  </div>
798
1077
  `, styles: [".jsonata-editor__footer{display:flex;align-items:center;gap:8px;margin-top:4px;font-size:.8em}.jsonata-editor__error{color:#da1e28}.jsonata-editor__valid{color:#198038}.jsonata-editor__variables{margin-left:auto;color:#8d8d8d;font-family:monospace}\n"] }]
@@ -800,16 +1079,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
800
1079
  type: Input
801
1080
  }], disabled: [{
802
1081
  type: Input
803
- }], suggestions: [{
1082
+ }], contextVariables: [{
804
1083
  type: Input
805
1084
  }], functions: [{
806
1085
  type: Input
1086
+ }], variablesHint: [{
1087
+ type: Input
807
1088
  }], expressionChange: [{
808
1089
  type: Output
809
1090
  }], validChange: [{
810
1091
  type: Output
811
1092
  }] } });
812
1093
 
1094
+ /*
1095
+ * Copyright 2025 Epistola.
1096
+ *
1097
+ * Licensed under EUPL, Version 1.2 (the "License");
1098
+ * you may not use this file except in compliance with the License.
1099
+ * You may obtain a copy of the License at
1100
+ *
1101
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
1102
+ *
1103
+ * Unless required by applicable law or agreed to in writing, software
1104
+ * distributed under the License is distributed on an "AS IS" basis,
1105
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1106
+ * See the License for the specific language governing permissions and
1107
+ * limitations under the License.
1108
+ *
1109
+ * SPDX-License-Identifier: EUPL-1.2
1110
+ */
813
1111
  class ExpectedStructureComponent {
814
1112
  templateFields = [];
815
1113
  structureText = '{}';
@@ -874,6 +1172,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
874
1172
  type: Input
875
1173
  }] } });
876
1174
 
1175
+ /*
1176
+ * Copyright 2025 Epistola.
1177
+ *
1178
+ * Licensed under EUPL, Version 1.2 (the "License");
1179
+ * you may not use this file except in compliance with the License.
1180
+ * You may obtain a copy of the License at
1181
+ *
1182
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
1183
+ *
1184
+ * Unless required by applicable law or agreed to in writing, software
1185
+ * distributed under the License is distributed on an "AS IS" basis,
1186
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1187
+ * See the License for the specific language governing permissions and
1188
+ * limitations under the License.
1189
+ *
1190
+ * SPDX-License-Identifier: EUPL-1.2
1191
+ */
877
1192
  class BuilderFieldComponent {
878
1193
  field;
879
1194
  path = [];
@@ -953,7 +1268,7 @@ class BuilderFieldComponent {
953
1268
  ></epistola-builder-field>
954
1269
  </div>
955
1270
  </div>
956
- `, 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"] }] });
1271
+ `, 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: i3$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3$1.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: i3$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
957
1272
  }
958
1273
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: BuilderFieldComponent, decorators: [{
959
1274
  type: Component,
@@ -1044,7 +1359,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
1044
1359
  type: Output
1045
1360
  }] } });
1046
1361
 
1047
- const jsonata = _jsonata.default || _jsonata;
1362
+ /*
1363
+ * Copyright 2025 Epistola.
1364
+ *
1365
+ * Licensed under EUPL, Version 1.2 (the "License");
1366
+ * you may not use this file except in compliance with the License.
1367
+ * You may obtain a copy of the License at
1368
+ *
1369
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
1370
+ *
1371
+ * Unless required by applicable law or agreed to in writing, software
1372
+ * distributed under the License is distributed on an "AS IS" basis,
1373
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1374
+ * See the License for the specific language governing permissions and
1375
+ * limitations under the License.
1376
+ *
1377
+ * SPDX-License-Identifier: EUPL-1.2
1378
+ */
1379
+ const jsonata$3 = _jsonata.default || _jsonata;
1048
1380
  /**
1049
1381
  * Parse a JSONata expression into BuilderField array.
1050
1382
  * Only supports top-level object literals with simple path references or nested objects.
@@ -1055,7 +1387,7 @@ function parseJsonataToBuilder(expression) {
1055
1387
  return [];
1056
1388
  }
1057
1389
  try {
1058
- const ast = jsonata(expression).ast();
1390
+ const ast = jsonata$3(expression).ast();
1059
1391
  if (ast.type === 'unary' && ast.value === '{') {
1060
1392
  return parseObjectEntries(ast.lhs, expression);
1061
1393
  }
@@ -1201,6 +1533,23 @@ function formatFieldEntry(field, indent = ' ') {
1201
1533
  return `${indent}"${field.name}": ${value}`;
1202
1534
  }
1203
1535
 
1536
+ /*
1537
+ * Copyright 2025 Epistola.
1538
+ *
1539
+ * Licensed under EUPL, Version 1.2 (the "License");
1540
+ * you may not use this file except in compliance with the License.
1541
+ * You may obtain a copy of the License at
1542
+ *
1543
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
1544
+ *
1545
+ * Unless required by applicable law or agreed to in writing, software
1546
+ * distributed under the License is distributed on an "AS IS" basis,
1547
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1548
+ * See the License for the specific language governing permissions and
1549
+ * limitations under the License.
1550
+ *
1551
+ * SPDX-License-Identifier: EUPL-1.2
1552
+ */
1204
1553
  class MappingBuilderComponent {
1205
1554
  expression = '';
1206
1555
  templateFields = [];
@@ -1402,6 +1751,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
1402
1751
  type: Output
1403
1752
  }] } });
1404
1753
 
1754
+ /*
1755
+ * Copyright 2025 Epistola.
1756
+ *
1757
+ * Licensed under EUPL, Version 1.2 (the "License");
1758
+ * you may not use this file except in compliance with the License.
1759
+ * You may obtain a copy of the License at
1760
+ *
1761
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
1762
+ *
1763
+ * Unless required by applicable law or agreed to in writing, software
1764
+ * distributed under the License is distributed on an "AS IS" basis,
1765
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1766
+ * See the License for the specific language governing permissions and
1767
+ * limitations under the License.
1768
+ *
1769
+ * SPDX-License-Identifier: EUPL-1.2
1770
+ */
1405
1771
  class MappingPreviewComponent {
1406
1772
  epistolaPluginService;
1407
1773
  expression = '';
@@ -1536,7 +1902,7 @@ class MappingPreviewComponent {
1536
1902
  <strong>{{ missingRequired.join(', ') }}</strong>
1537
1903
  </div>
1538
1904
  </div>
1539
- `, 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" }] });
1905
+ `, 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: i3$1.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: i3$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3$1.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" }] });
1540
1906
  }
1541
1907
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: MappingPreviewComponent, decorators: [{
1542
1908
  type: Component,
@@ -1607,7 +1973,227 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
1607
1973
  type: Input
1608
1974
  }] } });
1609
1975
 
1610
- const FORM_REF_PREFIX$1 = 'form:';
1976
+ /*
1977
+ * Copyright 2025 Epistola.
1978
+ *
1979
+ * Licensed under EUPL, Version 1.2 (the "License");
1980
+ * you may not use this file except in compliance with the License.
1981
+ * You may obtain a copy of the License at
1982
+ *
1983
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
1984
+ *
1985
+ * Unless required by applicable law or agreed to in writing, software
1986
+ * distributed under the License is distributed on an "AS IS" basis,
1987
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1988
+ * See the License for the specific language governing permissions and
1989
+ * limitations under the License.
1990
+ *
1991
+ * SPDX-License-Identifier: EUPL-1.2
1992
+ */
1993
+ const jsonata$2 = _jsonata.default || _jsonata;
1994
+ const BARE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
1995
+ /**
1996
+ * Render a `$form` reference for a form-field key. Keys that aren't bare
1997
+ * identifiers (e.g. `pv:motivation`) are backtick-quoted so JSONata treats the
1998
+ * whole key as a single property name — matching the old flat `formData[key]`
1999
+ * lookup rather than a nested path traversal.
2000
+ */
2001
+ function formRef(key) {
2002
+ return BARE_IDENTIFIER.test(key) ? `$form.${key}` : '$form.`' + key + '`';
2003
+ }
2004
+ function insert(tree, segments, leaf) {
2005
+ let node = tree;
2006
+ for (let i = 0; i < segments.length - 1; i++) {
2007
+ const seg = segments[i];
2008
+ if (typeof node[seg] !== 'object') {
2009
+ node[seg] = {};
2010
+ }
2011
+ node = node[seg];
2012
+ }
2013
+ node[segments[segments.length - 1]] = leaf;
2014
+ }
2015
+ function emit(node, indent) {
2016
+ if (typeof node === 'string') {
2017
+ return node;
2018
+ }
2019
+ const inner = indent + ' ';
2020
+ const entries = Object.entries(node).map(([key, value]) => `${inner}"${key}": ${emit(value, inner)}`);
2021
+ return `{\n${entries.join(',\n')}\n${indent}}`;
2022
+ }
2023
+ /**
2024
+ * Serialize simple-table rows into a JSONata expression that maps `$form` onto
2025
+ * a `{ doc, pv }` overlay. Dot-notation input paths expand into nested object
2026
+ * literals (so `beslissing.tekst` becomes `{ "beslissing": { "tekst": ... } }`),
2027
+ * preserving the legacy override semantics.
2028
+ */
2029
+ function serializeOverrideRows(rows) {
2030
+ const scopes = {};
2031
+ for (const row of rows) {
2032
+ if (!row.inputPath || !row.formFieldKey)
2033
+ continue;
2034
+ if (row.scope !== 'doc' && row.scope !== 'pv')
2035
+ continue;
2036
+ if (typeof scopes[row.scope] !== 'object') {
2037
+ scopes[row.scope] = {};
2038
+ }
2039
+ insert(scopes[row.scope], row.inputPath.split('.'), formRef(row.formFieldKey));
2040
+ }
2041
+ if (Object.keys(scopes).length === 0) {
2042
+ return '';
2043
+ }
2044
+ return emit(scopes, '');
2045
+ }
2046
+ /**
2047
+ * Parse a JSONata override expression back into simple-table rows, or `null`
2048
+ * when the expression is richer than the simple table can represent (anything
2049
+ * beyond `doc`/`pv` objects whose leaves are plain `$form.<key>` references).
2050
+ * A `null` result is the builder's signal to fall back to advanced mode.
2051
+ */
2052
+ function parseOverrideJsonata(expression) {
2053
+ if (!expression || !expression.trim()) {
2054
+ return [];
2055
+ }
2056
+ let ast;
2057
+ try {
2058
+ ast = jsonata$2(expression).ast();
2059
+ }
2060
+ catch {
2061
+ return null;
2062
+ }
2063
+ if (!(ast?.type === 'unary' && ast.value === '{')) {
2064
+ return null;
2065
+ }
2066
+ const rows = [];
2067
+ for (const entry of ast.lhs || []) {
2068
+ const scope = entry?.[0]?.value;
2069
+ const valueNode = entry?.[1];
2070
+ if (scope !== 'doc' && scope !== 'pv') {
2071
+ return null;
2072
+ }
2073
+ if (!(valueNode?.type === 'unary' && valueNode.value === '{')) {
2074
+ return null;
2075
+ }
2076
+ if (!collectLeaves(valueNode.lhs || [], [], scope, rows)) {
2077
+ return null;
2078
+ }
2079
+ }
2080
+ return rows;
2081
+ }
2082
+ function collectLeaves(entries, prefix, scope, rows) {
2083
+ for (const [keyNode, valueNode] of entries) {
2084
+ const segment = keyNode?.value;
2085
+ if (typeof segment !== 'string') {
2086
+ return false;
2087
+ }
2088
+ const path = [...prefix, segment];
2089
+ if (valueNode?.type === 'unary' && valueNode.value === '{') {
2090
+ if (!collectLeaves(valueNode.lhs || [], path, scope, rows)) {
2091
+ return false;
2092
+ }
2093
+ }
2094
+ else {
2095
+ const formFieldKey = formKeyOf(valueNode);
2096
+ if (formFieldKey === null) {
2097
+ return false;
2098
+ }
2099
+ rows.push({ scope, inputPath: path.join('.'), formFieldKey });
2100
+ }
2101
+ }
2102
+ return true;
2103
+ }
2104
+ /** Extract the form-field key from a `$form.<key>` path node, or null. */
2105
+ function formKeyOf(node) {
2106
+ if (node?.type === 'path' &&
2107
+ node.steps?.length === 2 &&
2108
+ node.steps[0].type === 'variable' &&
2109
+ node.steps[0].value === 'form' &&
2110
+ typeof node.steps[1]?.value === 'string') {
2111
+ return node.steps[1].value;
2112
+ }
2113
+ return null;
2114
+ }
2115
+ /** Whether the expression can be edited in the simple table (round-trippable). */
2116
+ function isRoundTrippable(expression) {
2117
+ return parseOverrideJsonata(expression) !== null;
2118
+ }
2119
+
2120
+ /*
2121
+ * Copyright 2025 Epistola.
2122
+ *
2123
+ * Licensed under EUPL, Version 1.2 (the "License");
2124
+ * you may not use this file except in compliance with the License.
2125
+ * You may obtain a copy of the License at
2126
+ *
2127
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
2128
+ *
2129
+ * Unless required by applicable law or agreed to in writing, software
2130
+ * distributed under the License is distributed on an "AS IS" basis,
2131
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2132
+ * See the License for the specific language governing permissions and
2133
+ * limitations under the License.
2134
+ *
2135
+ * SPDX-License-Identifier: EUPL-1.2
2136
+ */
2137
+ /**
2138
+ * Prefix that marked a form-field reference in the legacy override-mapping
2139
+ * object format (e.g. `"form:motivationField"`).
2140
+ */
2141
+ const FORM_REF_PREFIX = 'form:';
2142
+ /**
2143
+ * Whether a stored override-mapping value is in the legacy **object** format
2144
+ * (`{ scope: { inputPath: "form:fieldKey" } }`) rather than the new JSONata
2145
+ * **string** format.
2146
+ */
2147
+ function isLegacyOverrideMapping(value) {
2148
+ return !!value && typeof value === 'object' && !Array.isArray(value);
2149
+ }
2150
+ /**
2151
+ * TEMPORARY migration shim.
2152
+ *
2153
+ * Converts a legacy override-mapping object into the equivalent JSONata string
2154
+ * over `$form`. Funnelling every legacy value through this one function keeps
2155
+ * the rest of the codebase JSONata-only.
2156
+ *
2157
+ * @deprecated Remove once all deployed forms have been re-saved in the JSONata
2158
+ * format. The admin page's "legacy override format" warning tracks which
2159
+ * forms still need migrating.
2160
+ */
2161
+ function legacyOverrideToJsonata(mapping) {
2162
+ const rows = [];
2163
+ for (const [scope, fields] of Object.entries(mapping || {})) {
2164
+ if (scope !== 'doc' && scope !== 'pv')
2165
+ continue;
2166
+ if (!fields || typeof fields !== 'object')
2167
+ continue;
2168
+ for (const [inputPath, ref] of Object.entries(fields)) {
2169
+ const raw = String(ref);
2170
+ const formFieldKey = raw.startsWith(FORM_REF_PREFIX)
2171
+ ? raw.substring(FORM_REF_PREFIX.length)
2172
+ : raw;
2173
+ rows.push({ scope, inputPath, formFieldKey });
2174
+ }
2175
+ }
2176
+ return serializeOverrideRows(rows);
2177
+ }
2178
+
2179
+ /*
2180
+ * Copyright 2025 Epistola.
2181
+ *
2182
+ * Licensed under EUPL, Version 1.2 (the "License");
2183
+ * you may not use this file except in compliance with the License.
2184
+ * You may obtain a copy of the License at
2185
+ *
2186
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
2187
+ *
2188
+ * Unless required by applicable law or agreed to in writing, software
2189
+ * distributed under the License is distributed on an "AS IS" basis,
2190
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2191
+ * See the License for the specific language governing permissions and
2192
+ * limitations under the License.
2193
+ *
2194
+ * SPDX-License-Identifier: EUPL-1.2
2195
+ */
2196
+ const jsonata$1 = _jsonata.default || _jsonata;
1611
2197
  /**
1612
2198
  * Detect if a string value is a JSONata expression (vs a plain literal).
1613
2199
  * Checks for characters that indicate JSONata operators: $, &, (, {, ?, [
@@ -1635,32 +2221,98 @@ function expandDotNotation(flat) {
1635
2221
  return result;
1636
2222
  }
1637
2223
  /**
1638
- * Given an override mapping (scope -> { inputPath -> "form:<componentKey>" })
1639
- * and form data, produce the inputOverrides object for the backend.
1640
- * The "form:" prefix identifies form field references; the remainder is the Formio component key.
2224
+ * A preview is "override-driven" when it has a non-empty override mapping: its
2225
+ * input data comes from the form via the mapping, so it must wait for that data
2226
+ * before it can render. Previews without a mapping load straight from the base
2227
+ * doc/case data.
1641
2228
  */
1642
- function computeInputOverrides(mapping, formData) {
1643
- const result = {};
1644
- for (const [scope, fields] of Object.entries(mapping)) {
1645
- if (scope !== 'doc' && scope !== 'pv')
1646
- continue;
1647
- const flatOverrides = {};
1648
- for (const [inputPath, ref] of Object.entries(fields)) {
1649
- const formFieldKey = String(ref).startsWith(FORM_REF_PREFIX$1)
1650
- ? String(ref).substring(FORM_REF_PREFIX$1.length)
1651
- : String(ref);
1652
- const value = formData[formFieldKey];
1653
- if (value !== undefined) {
1654
- flatOverrides[inputPath] = value;
1655
- }
1656
- }
1657
- if (Object.keys(flatOverrides).length > 0) {
1658
- result[scope] = expandDotNotation(flatOverrides);
2229
+ function isOverrideDriven(mapping) {
2230
+ if (!mapping)
2231
+ return false;
2232
+ if (typeof mapping === 'string')
2233
+ return mapping.trim().length > 0;
2234
+ return Object.keys(mapping).length > 0;
2235
+ }
2236
+ /**
2237
+ * Whether the computed input overrides carry any usable data yet.
2238
+ */
2239
+ function hasUsableOverrides(overrides) {
2240
+ return !!overrides && Object.keys(overrides).length > 0;
2241
+ }
2242
+ /**
2243
+ * Decide whether a preview request should fire given the configured override
2244
+ * mapping and the currently computed overrides.
2245
+ *
2246
+ * - Override-driven previews only load once the mapped form data is present;
2247
+ * before that they show a "complete the form" placeholder and fire nothing
2248
+ * (avoids a doomed request that Epistola rejects with a 400 for missing
2249
+ * required fields).
2250
+ * - Previews without a mapping always load (base data is the whole input).
2251
+ */
2252
+ function shouldLoadPreview(mapping, overrides) {
2253
+ if (isOverrideDriven(mapping)) {
2254
+ return hasUsableOverrides(overrides);
2255
+ }
2256
+ return true;
2257
+ }
2258
+ /**
2259
+ * Given an override mapping and the live form data, produce the inputOverrides
2260
+ * object (`{ doc, pv }`) the backend overlays onto the real document / process
2261
+ * variables before the data mapping runs.
2262
+ *
2263
+ * The mapping is a JSONata expression over `$form`; legacy `form:`-ref objects
2264
+ * are converted on the fly via {@link legacyOverrideToJsonata}. Evaluation is
2265
+ * asynchronous because `jsonata().evaluate()` returns a Promise. Only `doc` and
2266
+ * `pv` scopes (with at least one resolved field) are kept — matching what the
2267
+ * backend consumes.
2268
+ */
2269
+ async function computeInputOverrides(mapping, formData) {
2270
+ if (!mapping) {
2271
+ return {};
2272
+ }
2273
+ const expression = isLegacyOverrideMapping(mapping)
2274
+ ? legacyOverrideToJsonata(mapping)
2275
+ : String(mapping);
2276
+ if (!expression.trim()) {
2277
+ return {};
2278
+ }
2279
+ let evaluated;
2280
+ try {
2281
+ evaluated = await jsonata$1(expression).evaluate({}, { form: formData ?? {} });
2282
+ }
2283
+ catch {
2284
+ return {};
2285
+ }
2286
+ if (!evaluated || typeof evaluated !== 'object' || Array.isArray(evaluated)) {
2287
+ return {};
2288
+ }
2289
+ const result = {};
2290
+ for (const scope of ['doc', 'pv']) {
2291
+ const value = evaluated[scope];
2292
+ if (value && typeof value === 'object' && Object.keys(value).length > 0) {
2293
+ result[scope] = value;
1659
2294
  }
1660
2295
  }
1661
2296
  return result;
1662
2297
  }
1663
2298
 
2299
+ /*
2300
+ * Copyright 2025 Epistola.
2301
+ *
2302
+ * Licensed under EUPL, Version 1.2 (the "License");
2303
+ * you may not use this file except in compliance with the License.
2304
+ * You may obtain a copy of the License at
2305
+ *
2306
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
2307
+ *
2308
+ * Unless required by applicable law or agreed to in writing, software
2309
+ * distributed under the License is distributed on an "AS IS" basis,
2310
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2311
+ * See the License for the specific language governing permissions and
2312
+ * limitations under the License.
2313
+ *
2314
+ * SPDX-License-Identifier: EUPL-1.2
2315
+ */
1664
2316
  class GenerateDocumentConfigurationComponent {
1665
2317
  epistolaPluginService;
1666
2318
  processLinkStateService;
@@ -1672,6 +2324,8 @@ class GenerateDocumentConfigurationComponent {
1672
2324
  selectedPluginConfigurationData$;
1673
2325
  context$;
1674
2326
  valid = new EventEmitter();
2327
+ // Framework's FunctionConfigurationData (index type) to satisfy the invariant
2328
+ // EventEmitter contract under strict mode; emitted values remain the typed config.
1675
2329
  configuration = new EventEmitter();
1676
2330
  catalogs$ = new BehaviorSubject(initialResource([]));
1677
2331
  templates$ = new BehaviorSubject(initialResource([]));
@@ -1718,6 +2372,8 @@ class GenerateDocumentConfigurationComponent {
1718
2372
  processVariables = [];
1719
2373
  expressionFunctions = [];
1720
2374
  variableSuggestions = null;
2375
+ /** Context variables for the JSONata editor's autocomplete ($doc/$pv/$case). */
2376
+ editorContextVariables = { doc: [], pv: [], case: [] };
1721
2377
  prefillDataMapping = {};
1722
2378
  validationErrors$ = new BehaviorSubject([]);
1723
2379
  destroy$ = new Subject();
@@ -2031,6 +2687,12 @@ class GenerateDocumentConfigurationComponent {
2031
2687
  .pipe(takeUntil$1(this.destroy$), catchError(() => of({ doc: [], pv: [] })))
2032
2688
  .subscribe((suggestions) => {
2033
2689
  this.variableSuggestions = suggestions;
2690
+ // `$case` is a valid (currently-empty) binding — keep it offered.
2691
+ this.editorContextVariables = {
2692
+ doc: suggestions.doc || [],
2693
+ pv: suggestions.pv || [],
2694
+ case: [],
2695
+ };
2034
2696
  this.cdr.markForCheck();
2035
2697
  });
2036
2698
  }
@@ -2123,8 +2785,8 @@ class GenerateDocumentConfigurationComponent {
2123
2785
  }
2124
2786
  });
2125
2787
  }
2126
- 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 });
2127
- 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 [clearSelectionSubject$]=\"clearTemplateId$\"\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 <!--\n The v-select / v-input here is intentionally NOT named into the v-form: v-form's\n @ContentChildren queries default to descendants:false and would skip anything nested\n in this `field-with-fx` wrapper. The value is tracked via (selectedChange)/(valueChange)\n on the component, see variantIdValue / filenameValue.\n -->\n <div *ngIf=\"variantSelectionMode === 'explicit'\" class=\"field-with-fx\">\n <v-select\n *ngIf=\"!variantIdExpressionMode\"\n [title]=\"'variantId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'variantIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.variants.data\"\n [defaultSelectionId]=\"variantIdValue\"\n [clearSelectionSubject$]=\"clearVariantId$\"\n (selectedChange)=\"onVariantIdValueChange($event)\"\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)=\"toggleVariantIdExpressionMode()\"\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 [title]=\"'filename' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'filenameTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"filenameValue\"\n (valueChange)=\"onFilenameValueChange($event)\"\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)=\"toggleFilenameExpressionMode()\"\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 });
2788
+ 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 });
2789
+ 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 [clearSelectionSubject$]=\"clearTemplateId$\"\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 <!--\n The v-select / v-input here is intentionally NOT named into the v-form: v-form's\n @ContentChildren queries default to descendants:false and would skip anything nested\n in this `field-with-fx` wrapper. The value is tracked via (selectedChange)/(valueChange)\n on the component, see variantIdValue / filenameValue.\n -->\n <div *ngIf=\"variantSelectionMode === 'explicit'\" class=\"field-with-fx\">\n <v-select\n *ngIf=\"!variantIdExpressionMode\"\n [title]=\"'variantId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'variantIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.variants.data\"\n [defaultSelectionId]=\"variantIdValue\"\n [clearSelectionSubject$]=\"clearVariantId$\"\n (selectedChange)=\"onVariantIdValueChange($event)\"\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)=\"toggleVariantIdExpressionMode()\"\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 [title]=\"'filename' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'filenameTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"filenameValue\"\n (valueChange)=\"onFilenameValueChange($event)\"\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)=\"toggleFilenameExpressionMode()\"\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 [contextVariables]=\"editorContextVariables\"\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: i3$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3$1.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: i3$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i3$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i3$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3$1.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", "contextVariables", "functions", "variablesHint"], 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 });
2128
2790
  }
2129
2791
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: GenerateDocumentConfigurationComponent, decorators: [{
2130
2792
  type: Component,
@@ -2139,8 +2801,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2139
2801
  JsonataEditorComponent,
2140
2802
  MappingBuilderComponent,
2141
2803
  MappingPreviewComponent,
2142
- ], 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 [clearSelectionSubject$]=\"clearTemplateId$\"\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 <!--\n The v-select / v-input here is intentionally NOT named into the v-form: v-form's\n @ContentChildren queries default to descendants:false and would skip anything nested\n in this `field-with-fx` wrapper. The value is tracked via (selectedChange)/(valueChange)\n on the component, see variantIdValue / filenameValue.\n -->\n <div *ngIf=\"variantSelectionMode === 'explicit'\" class=\"field-with-fx\">\n <v-select\n *ngIf=\"!variantIdExpressionMode\"\n [title]=\"'variantId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'variantIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.variants.data\"\n [defaultSelectionId]=\"variantIdValue\"\n [clearSelectionSubject$]=\"clearVariantId$\"\n (selectedChange)=\"onVariantIdValueChange($event)\"\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)=\"toggleVariantIdExpressionMode()\"\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 [title]=\"'filename' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'filenameTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"filenameValue\"\n (valueChange)=\"onFilenameValueChange($event)\"\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)=\"toggleFilenameExpressionMode()\"\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"] }]
2143
- }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$3.ProcessLinkStateService }, { type: i0.ChangeDetectorRef }], propDecorators: { save$: [{
2804
+ ], 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 [clearSelectionSubject$]=\"clearTemplateId$\"\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 <!--\n The v-select / v-input here is intentionally NOT named into the v-form: v-form's\n @ContentChildren queries default to descendants:false and would skip anything nested\n in this `field-with-fx` wrapper. The value is tracked via (selectedChange)/(valueChange)\n on the component, see variantIdValue / filenameValue.\n -->\n <div *ngIf=\"variantSelectionMode === 'explicit'\" class=\"field-with-fx\">\n <v-select\n *ngIf=\"!variantIdExpressionMode\"\n [title]=\"'variantId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'variantIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.variants.data\"\n [defaultSelectionId]=\"variantIdValue\"\n [clearSelectionSubject$]=\"clearVariantId$\"\n (selectedChange)=\"onVariantIdValueChange($event)\"\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)=\"toggleVariantIdExpressionMode()\"\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 [title]=\"'filename' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'filenameTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"filenameValue\"\n (valueChange)=\"onFilenameValueChange($event)\"\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)=\"toggleFilenameExpressionMode()\"\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 [contextVariables]=\"editorContextVariables\"\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"] }]
2805
+ }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$2.ProcessLinkStateService }, { type: i0.ChangeDetectorRef }], propDecorators: { save$: [{
2144
2806
  type: Input
2145
2807
  }], disabled$: [{
2146
2808
  type: Input
@@ -2158,12 +2820,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2158
2820
  type: Output
2159
2821
  }] } });
2160
2822
 
2823
+ /*
2824
+ * Copyright 2025 Epistola.
2825
+ *
2826
+ * Licensed under EUPL, Version 1.2 (the "License");
2827
+ * you may not use this file except in compliance with the License.
2828
+ * You may obtain a copy of the License at
2829
+ *
2830
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
2831
+ *
2832
+ * Unless required by applicable law or agreed to in writing, software
2833
+ * distributed under the License is distributed on an "AS IS" basis,
2834
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2835
+ * See the License for the specific language governing permissions and
2836
+ * limitations under the License.
2837
+ *
2838
+ * SPDX-License-Identifier: EUPL-1.2
2839
+ */
2161
2840
  class CheckJobStatusConfigurationComponent {
2162
2841
  save$;
2163
2842
  disabled$;
2164
2843
  pluginId;
2165
2844
  prefillConfiguration$;
2166
2845
  valid = new EventEmitter();
2846
+ // Typed as the framework's FunctionConfigurationData (an index type) to match the
2847
+ // FunctionConfigurationComponent contract under strict mode — EventEmitter is invariant,
2848
+ // so a narrower generic isn't assignable. Emitted values are still the typed config.
2167
2849
  configuration = new EventEmitter();
2168
2850
  saveSubscription;
2169
2851
  formValue$ = new BehaviorSubject(null);
@@ -2225,12 +2907,66 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2225
2907
  type: Output
2226
2908
  }] } });
2227
2909
 
2910
+ /*
2911
+ * Copyright 2025 Epistola.
2912
+ *
2913
+ * Licensed under EUPL, Version 1.2 (the "License");
2914
+ * you may not use this file except in compliance with the License.
2915
+ * You may obtain a copy of the License at
2916
+ *
2917
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
2918
+ *
2919
+ * Unless required by applicable law or agreed to in writing, software
2920
+ * distributed under the License is distributed on an "AS IS" basis,
2921
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2922
+ * See the License for the specific language governing permissions and
2923
+ * limitations under the License.
2924
+ *
2925
+ * SPDX-License-Identifier: EUPL-1.2
2926
+ */
2927
+ const DEFAULT_STORAGE_TARGET = 'TEMPORARY_RESOURCE';
2928
+ /** Normalize a (possibly undefined) storageTarget to a concrete target, applying the default. */
2929
+ function resolveStorageTarget(target) {
2930
+ return target === 'PROCESS_VARIABLE' ? 'PROCESS_VARIABLE' : DEFAULT_STORAGE_TARGET;
2931
+ }
2932
+ /**
2933
+ * A config is valid when the input variable and the output variable that matches the chosen
2934
+ * storage target are both set.
2935
+ */
2936
+ function isDownloadDocumentConfigValid(config) {
2937
+ if (!config?.documentVariable) {
2938
+ return false;
2939
+ }
2940
+ return resolveStorageTarget(config.storageTarget) === 'PROCESS_VARIABLE'
2941
+ ? !!config.contentVariable
2942
+ : !!config.resourceIdVariable;
2943
+ }
2944
+
2945
+ /*
2946
+ * Copyright 2025 Epistola.
2947
+ *
2948
+ * Licensed under EUPL, Version 1.2 (the "License");
2949
+ * you may not use this file except in compliance with the License.
2950
+ * You may obtain a copy of the License at
2951
+ *
2952
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
2953
+ *
2954
+ * Unless required by applicable law or agreed to in writing, software
2955
+ * distributed under the License is distributed on an "AS IS" basis,
2956
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2957
+ * See the License for the specific language governing permissions and
2958
+ * limitations under the License.
2959
+ *
2960
+ * SPDX-License-Identifier: EUPL-1.2
2961
+ */
2228
2962
  class DownloadDocumentConfigurationComponent {
2229
2963
  save$;
2230
2964
  disabled$;
2231
2965
  pluginId;
2232
2966
  prefillConfiguration$;
2233
2967
  valid = new EventEmitter();
2968
+ // Framework's FunctionConfigurationData (index type) to satisfy the invariant
2969
+ // EventEmitter contract under strict mode; emitted values remain the typed config.
2234
2970
  configuration = new EventEmitter();
2235
2971
  saveSubscription;
2236
2972
  formValue$ = new BehaviorSubject(null);
@@ -2243,11 +2979,24 @@ class DownloadDocumentConfigurationComponent {
2243
2979
  resolvedPrefill = {};
2244
2980
  prefillResolved$ = new BehaviorSubject(false);
2245
2981
  safeDisabled$;
2982
+ /**
2983
+ * Static option set for the storage target. Values match the backend
2984
+ * {@code DocumentStorageTarget} enum constants; labels are explained further via the
2985
+ * translated field title/tooltip.
2986
+ */
2987
+ storageTargetOptions = [
2988
+ { id: 'TEMPORARY_RESOURCE', text: 'Temporary resource storage' },
2989
+ { id: 'PROCESS_VARIABLE', text: 'Process variable (inline bytes)' },
2990
+ ];
2991
+ defaultStorageTarget = DEFAULT_STORAGE_TARGET;
2992
+ /** Drives which output-variable field is shown (resource id vs inline content). */
2993
+ selectedTarget$ = new BehaviorSubject(this.defaultStorageTarget);
2246
2994
  ngOnInit() {
2247
2995
  this.safeDisabled$ = this.disabled$.pipe(startWith(true), delay(0));
2248
2996
  const prefill$ = this.prefillConfiguration$ ?? of({});
2249
2997
  prefill$.pipe(take(1)).subscribe((prefill) => {
2250
2998
  this.resolvedPrefill = prefill ?? {};
2999
+ this.selectedTarget$.next(resolveStorageTarget(this.resolvedPrefill.storageTarget));
2251
3000
  this.prefillResolved$.next(true);
2252
3001
  });
2253
3002
  this.openSaveSubscription();
@@ -2257,11 +3006,14 @@ class DownloadDocumentConfigurationComponent {
2257
3006
  }
2258
3007
  formValueChange(formOutput) {
2259
3008
  const formValue = formOutput;
3009
+ if (formValue?.storageTarget) {
3010
+ this.selectedTarget$.next(formValue.storageTarget);
3011
+ }
2260
3012
  this.formValue$.next(formValue);
2261
3013
  this.handleValid(formValue);
2262
3014
  }
2263
3015
  handleValid(formValue) {
2264
- const valid = !!(formValue?.documentVariable && formValue?.contentVariable);
3016
+ const valid = isDownloadDocumentConfigValid(formValue);
2265
3017
  this.valid$.next(valid);
2266
3018
  this.valid.emit(valid);
2267
3019
  }
@@ -2277,11 +3029,11 @@ class DownloadDocumentConfigurationComponent {
2277
3029
  });
2278
3030
  }
2279
3031
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DownloadDocumentConfigurationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2280
- 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"] }] });
3032
+ 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-select\n name=\"storageTarget\"\n [title]=\"'storageTarget' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'storageTargetTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"storageTargetOptions\"\n [defaultSelectionId]=\"resolvedPrefill?.storageTarget || defaultStorageTarget\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-select>\n\n <v-input\n *ngIf=\"(selectedTarget$ | async) === 'TEMPORARY_RESOURCE'\"\n name=\"resourceIdVariable\"\n [title]=\"'resourceIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'resourceIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.resourceIdVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n *ngIf=\"(selectedTarget$ | async) === 'PROCESS_VARIABLE'\"\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"] }, { 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"] }] });
2281
3033
  }
2282
3034
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DownloadDocumentConfigurationComponent, decorators: [{
2283
3035
  type: Component,
2284
- 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" }]
3036
+ args: [{ selector: 'epistola-download-document-configuration', standalone: true, imports: [CommonModule, PluginTranslatePipeModule, FormModule, InputModule, SelectModule], 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-select\n name=\"storageTarget\"\n [title]=\"'storageTarget' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'storageTargetTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"storageTargetOptions\"\n [defaultSelectionId]=\"resolvedPrefill?.storageTarget || defaultStorageTarget\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-select>\n\n <v-input\n *ngIf=\"(selectedTarget$ | async) === 'TEMPORARY_RESOURCE'\"\n name=\"resourceIdVariable\"\n [title]=\"'resourceIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'resourceIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"resolvedPrefill?.resourceIdVariable\"\n [disabled]=\"safeDisabled$ | async\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n *ngIf=\"(selectedTarget$ | async) === 'PROCESS_VARIABLE'\"\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" }]
2285
3037
  }], propDecorators: { save$: [{
2286
3038
  type: Input
2287
3039
  }], disabled$: [{
@@ -2296,6 +3048,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2296
3048
  type: Output
2297
3049
  }] } });
2298
3050
 
3051
+ /*
3052
+ * Copyright 2025 Epistola.
3053
+ *
3054
+ * Licensed under EUPL, Version 1.2 (the "License");
3055
+ * you may not use this file except in compliance with the License.
3056
+ * You may obtain a copy of the License at
3057
+ *
3058
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
3059
+ *
3060
+ * Unless required by applicable law or agreed to in writing, software
3061
+ * distributed under the License is distributed on an "AS IS" basis,
3062
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3063
+ * See the License for the specific language governing permissions and
3064
+ * limitations under the License.
3065
+ *
3066
+ * SPDX-License-Identifier: EUPL-1.2
3067
+ */
2299
3068
  /**
2300
3069
  * Unified Formio component for the after-generation Epistola PDF UX. Reads
2301
3070
  * the PDF id and tenant id from named process variables on the caller's
@@ -2316,7 +3085,6 @@ class EpistolaDocumentComponent {
2316
3085
  epistolaPluginService;
2317
3086
  sanitizer;
2318
3087
  formIoStateService;
2319
- taskContext;
2320
3088
  cdr;
2321
3089
  value;
2322
3090
  valueChange = new EventEmitter();
@@ -2337,6 +3105,11 @@ class EpistolaDocumentComponent {
2337
3105
  tenantIdVariable = 'epistolaTenantId';
2338
3106
  /** Filename used for the download disposition. */
2339
3107
  filename = 'document.pdf';
3108
+ /**
3109
+ * Task id forwarded by the Formio wrapper from the server-prefilled form
3110
+ * ({@code epistola:taskId} value resolver), populated in every Valtimo task-open flow.
3111
+ */
3112
+ taskInstanceId;
2340
3113
  loading = false;
2341
3114
  downloading = false;
2342
3115
  error = null;
@@ -2346,18 +3119,21 @@ class EpistolaDocumentComponent {
2346
3119
  get designMode() {
2347
3120
  return !this.formIoStateService.documentId;
2348
3121
  }
2349
- constructor(epistolaPluginService, sanitizer, formIoStateService, taskContext, cdr) {
3122
+ constructor(epistolaPluginService, sanitizer, formIoStateService, cdr) {
2350
3123
  this.epistolaPluginService = epistolaPluginService;
2351
3124
  this.sanitizer = sanitizer;
2352
3125
  this.formIoStateService = formIoStateService;
2353
- this.taskContext = taskContext;
2354
3126
  this.cdr = cdr;
2355
3127
  }
2356
- ngOnInit() {
2357
- if (this.designMode) {
3128
+ ngOnChanges(changes) {
3129
+ if (this.designMode || this.display === 'button') {
2358
3130
  return;
2359
3131
  }
2360
- if (this.display !== 'button') {
3132
+ // The Formio wrapper sets taskInstanceId around attach, so it can arrive after the
3133
+ // first change — (re)load the inline document once it's available instead of leaving
3134
+ // the "Document is alleen beschikbaar binnen een taak" message until a manual refresh.
3135
+ // (For display="button" the download() click reads the task id on demand.)
3136
+ if (changes['taskInstanceId'] && this.taskInstanceId) {
2361
3137
  this.loadInline();
2362
3138
  }
2363
3139
  }
@@ -2424,7 +3200,7 @@ class EpistolaDocumentComponent {
2424
3200
  });
2425
3201
  }
2426
3202
  buildRequest(disposition) {
2427
- const taskId = this.taskContext.taskInstanceId;
3203
+ const taskId = this.taskInstanceId ?? null;
2428
3204
  const caseDocumentId = this.formIoStateService.documentId;
2429
3205
  if (!taskId || !caseDocumentId) {
2430
3206
  this.error = 'Document is alleen beschikbaar binnen een taak.';
@@ -2447,8 +3223,8 @@ class EpistolaDocumentComponent {
2447
3223
  this.previewUrl = null;
2448
3224
  }
2449
3225
  }
2450
- 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 });
2451
- 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: `
3226
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDocumentComponent, deps: [{ token: EpistolaPluginService }, { token: i2$3.DomSanitizer }, { token: i3.FormIoStateService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
3227
+ 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", taskInstanceId: "taskInstanceId" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
2452
3228
  <!-- Design-time placeholder -->
2453
3229
  <div *ngIf="designMode" class="epistola-doc-panel">
2454
3230
  <div class="doc-header">
@@ -2603,7 +3379,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2603
3379
  </div>
2604
3380
  </div>
2605
3381
  `, 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"] }]
2606
- }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$4.DomSanitizer }, { type: i3.FormIoStateService }, { type: EpistolaTaskContextService }, { type: i0.ChangeDetectorRef }], propDecorators: { value: [{
3382
+ }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$3.DomSanitizer }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }], propDecorators: { value: [{
2607
3383
  type: Input
2608
3384
  }], valueChange: [{
2609
3385
  type: Output
@@ -2619,19 +3395,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2619
3395
  type: Input
2620
3396
  }], filename: [{
2621
3397
  type: Input
3398
+ }], taskInstanceId: [{
3399
+ type: Input
2622
3400
  }] } });
2623
3401
 
3402
+ /*
3403
+ * Copyright 2025 Epistola.
3404
+ *
3405
+ * Licensed under EUPL, Version 1.2 (the "License");
3406
+ * you may not use this file except in compliance with the License.
3407
+ * You may obtain a copy of the License at
3408
+ *
3409
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
3410
+ *
3411
+ * Unless required by applicable law or agreed to in writing, software
3412
+ * distributed under the License is distributed on an "AS IS" basis,
3413
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3414
+ * See the License for the specific language governing permissions and
3415
+ * limitations under the License.
3416
+ *
3417
+ * SPDX-License-Identifier: EUPL-1.2
3418
+ */
2624
3419
  class EpistolaRetryFormComponent {
2625
3420
  epistolaPluginService;
2626
- formIoStateService;
2627
3421
  cdr;
2628
3422
  sanitizer;
2629
- taskContext;
2630
3423
  value;
2631
3424
  valueChange = new EventEmitter();
2632
3425
  disabled = false;
2633
3426
  label = 'Document Data';
2634
3427
  sourceActivityId;
3428
+ /**
3429
+ * Task id forwarded by the Formio wrapper from the server-prefilled form
3430
+ * ({@code epistola:taskId} value resolver), populated in every Valtimo task-open flow.
3431
+ */
3432
+ taskInstanceId;
2635
3433
  formDefinition;
2636
3434
  submission;
2637
3435
  loading = true;
@@ -2646,17 +3444,14 @@ class EpistolaRetryFormComponent {
2646
3444
  previewSubject = new Subject();
2647
3445
  currentBlobUrl = null;
2648
3446
  resolvedSourceActivityId;
2649
- processDefinitionKey;
2650
3447
  formOptions = {
2651
3448
  noAlerts: true,
2652
3449
  buttonSettings: { showCancel: false, showSubmit: false, showPrevious: false, showNext: false },
2653
3450
  };
2654
- constructor(epistolaPluginService, formIoStateService, cdr, sanitizer, taskContext) {
3451
+ constructor(epistolaPluginService, cdr, sanitizer) {
2655
3452
  this.epistolaPluginService = epistolaPluginService;
2656
- this.formIoStateService = formIoStateService;
2657
3453
  this.cdr = cdr;
2658
3454
  this.sanitizer = sanitizer;
2659
- this.taskContext = taskContext;
2660
3455
  // Debounce preview calls
2661
3456
  this.previewSubscription = this.previewSubject.pipe(debounceTime$1(1500)).subscribe((data) => {
2662
3457
  this.loadPreview(data);
@@ -2666,6 +3461,13 @@ class EpistolaRetryFormComponent {
2666
3461
  if (!this.loaded) {
2667
3462
  this.loaded = true;
2668
3463
  this.loadForm();
3464
+ return;
3465
+ }
3466
+ // The Formio wrapper sets taskInstanceId after attach, so it can land after the
3467
+ // first render — if the form failed to load for lack of a task id, retry once it
3468
+ // arrives instead of leaving the "only available from within a user task" message.
3469
+ if (changes['taskInstanceId'] && this.taskInstanceId && !this.formDefinition) {
3470
+ this.loadForm();
2669
3471
  }
2670
3472
  }
2671
3473
  ngOnDestroy() {
@@ -2689,11 +3491,8 @@ class EpistolaRetryFormComponent {
2689
3491
  }
2690
3492
  }
2691
3493
  loadPreview(formData) {
2692
- const documentId = this.formIoStateService.documentId;
2693
- const processInstanceId = this.formIoStateService.processInstanceId;
2694
- if (!documentId || !processInstanceId)
2695
- return;
2696
- const taskId = this.taskContext.taskInstanceId;
3494
+ // The backend derives the process instance and case document from the task.
3495
+ const taskId = this.taskInstanceId ?? null;
2697
3496
  if (!taskId) {
2698
3497
  this.previewError = 'Preview is only available from within a user task.';
2699
3498
  this.cdr.markForCheck();
@@ -2710,8 +3509,6 @@ class EpistolaRetryFormComponent {
2710
3509
  this.epistolaPluginService
2711
3510
  .previewToBlob({
2712
3511
  taskId,
2713
- documentId,
2714
- processInstanceId,
2715
3512
  sourceActivityId: this.sourceActivityId || null,
2716
3513
  overrides: formData,
2717
3514
  })
@@ -2748,15 +3545,8 @@ class EpistolaRetryFormComponent {
2748
3545
  });
2749
3546
  }
2750
3547
  loadForm() {
2751
- const processInstanceId = this.formIoStateService.processInstanceId;
2752
- const documentId = this.formIoStateService.documentId;
2753
- if (!processInstanceId) {
2754
- this.error = 'Could not determine process instance ID.';
2755
- this.loading = false;
2756
- this.cdr.markForCheck();
2757
- return;
2758
- }
2759
- const taskId = this.taskContext.taskInstanceId;
3548
+ // The backend derives the process instance and case document from the task.
3549
+ const taskId = this.taskInstanceId ?? null;
2760
3550
  if (!taskId) {
2761
3551
  this.error = 'Retry form is only available from within a user task.';
2762
3552
  this.loading = false;
@@ -2764,7 +3554,7 @@ class EpistolaRetryFormComponent {
2764
3554
  return;
2765
3555
  }
2766
3556
  this.loadSubscription = this.epistolaPluginService
2767
- .getRetryForm(taskId, processInstanceId, documentId ?? undefined, this.sourceActivityId)
3557
+ .getRetryForm(taskId, this.sourceActivityId)
2768
3558
  .subscribe({
2769
3559
  next: (form) => {
2770
3560
  this.formDefinition = form;
@@ -2787,8 +3577,8 @@ class EpistolaRetryFormComponent {
2787
3577
  },
2788
3578
  });
2789
3579
  }
2790
- 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 });
2791
- 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: `
3580
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaRetryFormComponent, deps: [{ token: EpistolaPluginService }, { token: i0.ChangeDetectorRef }, { token: i2$3.DomSanitizer }], target: i0.ɵɵFactoryTarget.Component });
3581
+ 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", taskInstanceId: "taskInstanceId" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
2792
3582
  <div *ngIf="loading" class="epistola-retry-loading">Loading form...</div>
2793
3583
  <div *ngIf="error" class="epistola-retry-error">{{ error }}</div>
2794
3584
  <div
@@ -2826,7 +3616,7 @@ class EpistolaRetryFormComponent {
2826
3616
  </div>
2827
3617
  </div>
2828
3618
  </div>
2829
- `, 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 });
3619
+ `, 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: i4.FormioComponent, selector: "formio" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2830
3620
  }
2831
3621
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaRetryFormComponent, decorators: [{
2832
3622
  type: Component,
@@ -2869,7 +3659,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2869
3659
  </div>
2870
3660
  </div>
2871
3661
  `, 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"] }]
2872
- }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }, { type: i2$4.DomSanitizer }, { type: EpistolaTaskContextService }], propDecorators: { value: [{
3662
+ }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i0.ChangeDetectorRef }, { type: i2$3.DomSanitizer }], propDecorators: { value: [{
2873
3663
  type: Input
2874
3664
  }], valueChange: [{
2875
3665
  type: Output
@@ -2879,21 +3669,74 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2879
3669
  type: Input
2880
3670
  }], sourceActivityId: [{
2881
3671
  type: Input
3672
+ }], taskInstanceId: [{
3673
+ type: Input
2882
3674
  }] } });
2883
3675
 
3676
+ /*
3677
+ * Copyright 2025 Epistola.
3678
+ *
3679
+ * Licensed under EUPL, Version 1.2 (the "License");
3680
+ * you may not use this file except in compliance with the License.
3681
+ * You may obtain a copy of the License at
3682
+ *
3683
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
3684
+ *
3685
+ * Unless required by applicable law or agreed to in writing, software
3686
+ * distributed under the License is distributed on an "AS IS" basis,
3687
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
3688
+ * See the License for the specific language governing permissions and
3689
+ * limitations under the License.
3690
+ *
3691
+ * SPDX-License-Identifier: EUPL-1.2
3692
+ */
2884
3693
  class EpistolaDocumentPreviewComponent {
2885
3694
  epistolaPluginService;
2886
3695
  sanitizer;
2887
3696
  formIoStateService;
2888
3697
  cdr;
2889
- taskContext;
2890
3698
  value;
2891
3699
  valueChange = new EventEmitter();
2892
3700
  disabled = false;
2893
3701
  label = 'Document Preview';
2894
3702
  processDefinitionKey;
2895
3703
  sourceActivityId;
3704
+ /**
3705
+ * The override mapping: a JSONata expression string over `$form`, or — for
3706
+ * not-yet-re-saved forms — the legacy `form:`-ref object.
3707
+ */
2896
3708
  overrideMapping;
3709
+ /**
3710
+ * Task id forwarded by the Formio wrapper from the server-prefilled form
3711
+ * ({@code epistola:taskId} value resolver), populated in every Valtimo task-open flow.
3712
+ */
3713
+ taskInstanceId;
3714
+ /**
3715
+ * The computed input overrides (`{ doc, pv }`) the preview renders with, pushed
3716
+ * by the Formio wrapper. Kept separate from the Formio `value`: Valtimo's custom
3717
+ * component bridge only mirrors `value` to the DOM (never to Formio's data
3718
+ * model), so Formio resets it to `emptyValue` on every redraw — which would
3719
+ * cancel the preview. This dedicated input is never touched by Formio.
3720
+ */
3721
+ inputOverrides;
3722
+ /**
3723
+ * Forces the Formio wrapper to recompute the input overrides from the live form
3724
+ * data. Set by the wrapper for override-driven previews; lets the Refresh button
3725
+ * work before the first change (e.g. on initial load with pre-filled fields).
3726
+ */
3727
+ requestOverrides;
3728
+ /**
3729
+ * Current auto-refresh state, forwarded by the wrapper (seeded from the builder's
3730
+ * `autoRefresh` option, default on). Seeds the header toggle's initial state.
3731
+ */
3732
+ autoRefresh;
3733
+ /**
3734
+ * Tells the wrapper to enable/disable auto-refresh (recompute on change/blur).
3735
+ * Set by the wrapper for override-driven previews; called by the header toggle.
3736
+ */
3737
+ setAutoRefresh;
3738
+ /** Runtime state of the header auto-refresh toggle. */
3739
+ autoRefreshEnabled = true;
2897
3740
  loading = false;
2898
3741
  error = null;
2899
3742
  previewUrl = null;
@@ -2901,31 +3744,35 @@ class EpistolaDocumentPreviewComponent {
2901
3744
  initialized = false;
2902
3745
  currentBlobUrl = null;
2903
3746
  previewSubscription;
2904
- constructor(epistolaPluginService, sanitizer, formIoStateService, cdr, taskContext) {
3747
+ constructor(epistolaPluginService, sanitizer, formIoStateService, cdr) {
2905
3748
  this.epistolaPluginService = epistolaPluginService;
2906
3749
  this.sanitizer = sanitizer;
2907
3750
  this.formIoStateService = formIoStateService;
2908
3751
  this.cdr = cdr;
2909
- this.taskContext = taskContext;
2910
3752
  }
2911
3753
  /**
2912
- * Resolve the active task id from {@link EpistolaTaskContextService}, populated
2913
- * by {@code EpistolaTaskContextInterceptor} on the canonical Valtimo task-open
2914
- * call. Returns null when used outside a task context (e.g. Formio builder).
3754
+ * The active task id, forwarded by the Formio wrapper from the server-prefilled
3755
+ * form ({@code epistola:taskId} value resolver). Null outside a task context
3756
+ * (e.g. Formio builder), in which case the component fails closed.
2915
3757
  */
2916
3758
  get currentTaskId() {
2917
- return this.taskContext.taskInstanceId;
3759
+ return this.taskInstanceId ?? null;
2918
3760
  }
2919
- get overrideMappingScopes() {
2920
- return this.overrideMapping ? Object.keys(this.overrideMapping) : [];
2921
- }
2922
- overrideMappingEntries(scope) {
2923
- const fields = this.overrideMapping?.[scope];
2924
- if (!fields || typeof fields !== 'object')
2925
- return [];
2926
- return Object.entries(fields).map(([path, field]) => ({ path, field: String(field) }));
3761
+ /**
3762
+ * The override mapping as a JSONata expression for the design-mode summary.
3763
+ * Legacy `form:`-ref objects are converted on the fly for display.
3764
+ */
3765
+ get overrideExpression() {
3766
+ const mapping = this.overrideMapping;
3767
+ if (!mapping)
3768
+ return '';
3769
+ return isLegacyOverrideMapping(mapping) ? legacyOverrideToJsonata(mapping) : String(mapping);
2927
3770
  }
2928
3771
  ngOnChanges(changes) {
3772
+ // Seed the runtime auto-refresh toggle from the wrapper-forwarded state.
3773
+ if (changes['autoRefresh']) {
3774
+ this.autoRefreshEnabled = this.autoRefresh !== false;
3775
+ }
2929
3776
  if (!this.initialized) {
2930
3777
  this.initialized = true;
2931
3778
  // Detect design mode: no runtime context (Formio builder)
@@ -2940,12 +3787,17 @@ class EpistolaDocumentPreviewComponent {
2940
3787
  this.cdr.markForCheck();
2941
3788
  return;
2942
3789
  }
2943
- this.loadPreview();
3790
+ this.triggerPreview();
2944
3791
  return;
2945
3792
  }
2946
- // React to value changes (input overrides from the Formio wrapper).
2947
- if (changes['value']) {
2948
- this.loadPreview();
3793
+ if (this.designMode)
3794
+ return;
3795
+ // React to input-override changes, and to the task id arriving late: the Formio
3796
+ // wrapper sets taskInstanceId after attach, so it can land after the first render —
3797
+ // re-run the preview once it does, instead of leaving the "only available from
3798
+ // within a user task" message until a manual refresh.
3799
+ if (changes['inputOverrides'] || changes['taskInstanceId']) {
3800
+ this.triggerPreview();
2949
3801
  }
2950
3802
  }
2951
3803
  ngOnDestroy() {
@@ -2953,31 +3805,58 @@ class EpistolaDocumentPreviewComponent {
2953
3805
  this.revokeBlobUrl();
2954
3806
  }
2955
3807
  refresh() {
3808
+ // For an override-driven preview whose value isn't ready yet (e.g. initial load
3809
+ // before the overrides have been computed), recompute from the live form data;
3810
+ // the resulting value change drives the preview. Otherwise re-fetch directly.
3811
+ if (this.requestOverrides && !shouldLoadPreview(this.overrideMapping, this.inputOverrides)) {
3812
+ this.requestOverrides();
3813
+ return;
3814
+ }
3815
+ this.triggerPreview();
3816
+ }
3817
+ /** Toggle auto-refresh for this session; flipping it on triggers an immediate refresh. */
3818
+ onToggleAutoRefresh(event) {
3819
+ const enabled = event.target.checked;
3820
+ this.autoRefreshEnabled = enabled;
3821
+ // The wrapper owns the change/blur listeners, so it must learn about the flip;
3822
+ // turning it on recomputes immediately (handled wrapper-side).
3823
+ this.setAutoRefresh?.(enabled);
3824
+ this.cdr.markForCheck();
3825
+ }
3826
+ /**
3827
+ * Load the preview only when there is enough data for it. Override-driven
3828
+ * previews (those with an override mapping) wait until the mapped form data
3829
+ * has been computed; until then they show a placeholder rather than firing a
3830
+ * request that Epistola would reject with a 400 for missing required fields.
3831
+ */
3832
+ triggerPreview() {
3833
+ if (!shouldLoadPreview(this.overrideMapping, this.inputOverrides)) {
3834
+ this.showWaitingForInput();
3835
+ return;
3836
+ }
2956
3837
  this.loadPreview();
2957
3838
  }
3839
+ showWaitingForInput() {
3840
+ this.revokeBlobUrl();
3841
+ this.previewSubscription?.unsubscribe();
3842
+ this.previewUrl = null;
3843
+ this.loading = false;
3844
+ this.error = 'Complete the form to generate a preview.';
3845
+ this.cdr.markForCheck();
3846
+ }
2958
3847
  /**
2959
3848
  * Preview using the explicitly configured process link + input overrides.
2960
3849
  * Requires a runtime task context — the backend authorizes the request against
2961
3850
  * the task's process instance and case document, so all three ids must match.
2962
3851
  */
2963
3852
  loadPreview() {
2964
- const documentId = this.formIoStateService.documentId;
2965
- if (!documentId) {
2966
- this.error = 'Could not determine document ID from context.';
2967
- this.cdr.markForCheck();
2968
- return;
2969
- }
2970
3853
  if (!this.sourceActivityId) {
2971
3854
  this.error = 'Preview is not configured: set the source activity on the form component.';
2972
3855
  this.cdr.markForCheck();
2973
3856
  return;
2974
3857
  }
2975
- const processInstanceId = this.formIoStateService.processInstanceId;
2976
- if (!processInstanceId) {
2977
- this.error = 'Preview is only available from within a running process.';
2978
- this.cdr.markForCheck();
2979
- return;
2980
- }
3858
+ // The backend derives the process instance and case document from the task, so the
3859
+ // task id is the only runtime context the request carries.
2981
3860
  const taskId = this.currentTaskId;
2982
3861
  if (!taskId) {
2983
3862
  this.error = 'Preview is only available from within a user task.';
@@ -2992,11 +3871,8 @@ class EpistolaDocumentPreviewComponent {
2992
3871
  this.previewSubscription = this.epistolaPluginService
2993
3872
  .previewToBlob({
2994
3873
  taskId,
2995
- documentId,
2996
- processDefinitionKey: this.processDefinitionKey || null,
2997
- processInstanceId,
2998
3874
  sourceActivityId: this.sourceActivityId,
2999
- inputOverrides: this.value || null,
3875
+ inputOverrides: this.inputOverrides || null,
3000
3876
  overrides: null,
3001
3877
  })
3002
3878
  .subscribe({
@@ -3039,8 +3915,8 @@ class EpistolaDocumentPreviewComponent {
3039
3915
  this.previewUrl = null;
3040
3916
  }
3041
3917
  }
3042
- 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 });
3043
- 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: `
3918
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDocumentPreviewComponent, deps: [{ token: EpistolaPluginService }, { token: i2$3.DomSanitizer }, { token: i3.FormIoStateService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
3919
+ 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", taskInstanceId: "taskInstanceId", inputOverrides: "inputOverrides", requestOverrides: "requestOverrides", autoRefresh: "autoRefresh", setAutoRefresh: "setAutoRefresh" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
3044
3920
  <!-- Design-time view: show configuration summary when no runtime context -->
3045
3921
  <div *ngIf="designMode" class="epistola-preview-panel">
3046
3922
  <div class="preview-header">
@@ -3053,16 +3929,9 @@ class EpistolaDocumentPreviewComponent {
3053
3929
  <div class="design-label">Activity</div>
3054
3930
  <div class="design-value">{{ sourceActivityId }}</div>
3055
3931
  </div>
3056
- <div class="design-section" *ngIf="overrideMapping">
3057
- <div class="design-label">Input Overrides</div>
3058
- <div *ngFor="let scope of overrideMappingScopes" class="design-mapping">
3059
- <div *ngFor="let entry of overrideMappingEntries(scope)" class="design-entry">
3060
- <span class="design-scope">{{ scope }}</span
3061
- >.{{ entry.path }}
3062
- <i class="mdi mdi-arrow-left"></i>
3063
- <span class="design-field">{{ entry.field }}</span>
3064
- </div>
3065
- </div>
3932
+ <div class="design-section" *ngIf="overrideExpression">
3933
+ <div class="design-label">Input Overrides ($form)</div>
3934
+ <pre class="design-expression">{{ overrideExpression }}</pre>
3066
3935
  </div>
3067
3936
  <div *ngIf="!sourceActivityId" class="design-unconfigured">
3068
3937
  Auto-discover mode (no process link configured)
@@ -3075,6 +3944,18 @@ class EpistolaDocumentPreviewComponent {
3075
3944
  <div class="preview-header">
3076
3945
  <span>{{ label || 'Document Preview' }}</span>
3077
3946
  <div class="preview-controls">
3947
+ <label
3948
+ *ngIf="overrideMapping"
3949
+ class="preview-autorefresh"
3950
+ title="Automatically refresh the preview as you fill in the form"
3951
+ >
3952
+ <input
3953
+ type="checkbox"
3954
+ [checked]="autoRefreshEnabled"
3955
+ (change)="onToggleAutoRefresh($event)"
3956
+ />
3957
+ Auto-refresh
3958
+ </label>
3078
3959
  <button type="button" class="preview-refresh" [disabled]="loading" (click)="refresh()">
3079
3960
  <i class="mdi mdi-refresh mr-1"></i>
3080
3961
  {{ loading ? 'Generating...' : 'Refresh' }}
@@ -3097,7 +3978,7 @@ class EpistolaDocumentPreviewComponent {
3097
3978
  </object>
3098
3979
  </div>
3099
3980
  </div>
3100
- `, 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 });
3981
+ `, 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:.75rem}.preview-autorefresh{display:flex;align-items:center;gap:.3rem;font-size:.8rem;font-weight:400;color:#495057;cursor:pointer;margin:0;white-space:nowrap}.preview-autorefresh input{cursor:pointer;margin:0}.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-expression{font-family:monospace;font-size:.8rem;color:#212529;background:#eef0f2;border-radius:4px;padding:.5rem;margin:.25rem 0 0;white-space:pre-wrap;word-break:break-word}.design-unconfigured{color:#6c757d;font-style:italic;font-size:.85rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3101
3982
  }
3102
3983
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDocumentPreviewComponent, decorators: [{
3103
3984
  type: Component,
@@ -3114,16 +3995,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3114
3995
  <div class="design-label">Activity</div>
3115
3996
  <div class="design-value">{{ sourceActivityId }}</div>
3116
3997
  </div>
3117
- <div class="design-section" *ngIf="overrideMapping">
3118
- <div class="design-label">Input Overrides</div>
3119
- <div *ngFor="let scope of overrideMappingScopes" class="design-mapping">
3120
- <div *ngFor="let entry of overrideMappingEntries(scope)" class="design-entry">
3121
- <span class="design-scope">{{ scope }}</span
3122
- >.{{ entry.path }}
3123
- <i class="mdi mdi-arrow-left"></i>
3124
- <span class="design-field">{{ entry.field }}</span>
3125
- </div>
3126
- </div>
3998
+ <div class="design-section" *ngIf="overrideExpression">
3999
+ <div class="design-label">Input Overrides ($form)</div>
4000
+ <pre class="design-expression">{{ overrideExpression }}</pre>
3127
4001
  </div>
3128
4002
  <div *ngIf="!sourceActivityId" class="design-unconfigured">
3129
4003
  Auto-discover mode (no process link configured)
@@ -3136,6 +4010,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3136
4010
  <div class="preview-header">
3137
4011
  <span>{{ label || 'Document Preview' }}</span>
3138
4012
  <div class="preview-controls">
4013
+ <label
4014
+ *ngIf="overrideMapping"
4015
+ class="preview-autorefresh"
4016
+ title="Automatically refresh the preview as you fill in the form"
4017
+ >
4018
+ <input
4019
+ type="checkbox"
4020
+ [checked]="autoRefreshEnabled"
4021
+ (change)="onToggleAutoRefresh($event)"
4022
+ />
4023
+ Auto-refresh
4024
+ </label>
3139
4025
  <button type="button" class="preview-refresh" [disabled]="loading" (click)="refresh()">
3140
4026
  <i class="mdi mdi-refresh mr-1"></i>
3141
4027
  {{ loading ? 'Generating...' : 'Refresh' }}
@@ -3158,8 +4044,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3158
4044
  </object>
3159
4045
  </div>
3160
4046
  </div>
3161
- `, 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"] }]
3162
- }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$4.DomSanitizer }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }, { type: EpistolaTaskContextService }], propDecorators: { value: [{
4047
+ `, 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:.75rem}.preview-autorefresh{display:flex;align-items:center;gap:.3rem;font-size:.8rem;font-weight:400;color:#495057;cursor:pointer;margin:0;white-space:nowrap}.preview-autorefresh input{cursor:pointer;margin:0}.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-expression{font-family:monospace;font-size:.8rem;color:#212529;background:#eef0f2;border-radius:4px;padding:.5rem;margin:.25rem 0 0;white-space:pre-wrap;word-break:break-word}.design-unconfigured{color:#6c757d;font-style:italic;font-size:.85rem}\n"] }]
4048
+ }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$3.DomSanitizer }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }], propDecorators: { value: [{
3163
4049
  type: Input
3164
4050
  }], valueChange: [{
3165
4051
  type: Output
@@ -3173,8 +4059,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3173
4059
  type: Input
3174
4060
  }], overrideMapping: [{
3175
4061
  type: Input
4062
+ }], taskInstanceId: [{
4063
+ type: Input
4064
+ }], inputOverrides: [{
4065
+ type: Input
4066
+ }], requestOverrides: [{
4067
+ type: Input
4068
+ }], autoRefresh: [{
4069
+ type: Input
4070
+ }], setAutoRefresh: [{
4071
+ type: Input
3176
4072
  }] } });
3177
4073
 
4074
+ /*
4075
+ * Copyright 2025 Epistola.
4076
+ *
4077
+ * Licensed under EUPL, Version 1.2 (the "License");
4078
+ * you may not use this file except in compliance with the License.
4079
+ * You may obtain a copy of the License at
4080
+ *
4081
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
4082
+ *
4083
+ * Unless required by applicable law or agreed to in writing, software
4084
+ * distributed under the License is distributed on an "AS IS" basis,
4085
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4086
+ * See the License for the specific language governing permissions and
4087
+ * limitations under the License.
4088
+ *
4089
+ * SPDX-License-Identifier: EUPL-1.2
4090
+ */
3178
4091
  class EpistolaAdminPageComponent {
3179
4092
  adminService;
3180
4093
  route;
@@ -3182,18 +4095,28 @@ class EpistolaAdminPageComponent {
3182
4095
  cards = [];
3183
4096
  selectedCard = null;
3184
4097
  activeTab = 'actions';
4098
+ // NOTE: the 'forms' tab is TEMPORARY (remove in 1.0.0) — see the carrier-repair block below.
3185
4099
  overviewTab = 'configurations';
3186
4100
  loading = false;
3187
4101
  pluginVersion = null;
3188
4102
  changelog = null;
3189
4103
  changelogLoading = false;
3190
- validationViolations = [];
4104
+ validationReport = null;
3191
4105
  reconcilingExecutionIds = new Set();
3192
4106
  reconcileFeedback = null;
3193
4107
  catalogs = [];
3194
4108
  catalogsLoading = false;
3195
4109
  redeployingSlugs = new Set();
3196
4110
  catalogFeedback = null;
4111
+ // TEMPORARY (removed in 1.0.0): task-id carrier detection + repair.
4112
+ formIssues = null;
4113
+ formIssuesLoading = false;
4114
+ repairingFormIds = new Set();
4115
+ repairingAll = false;
4116
+ formFeedback = null;
4117
+ // TEMPORARY: forms still using the legacy override-mapping object format.
4118
+ legacyOverrideForms = null;
4119
+ legacyOverrideLoading = false;
3197
4120
  connectionStatuses = [];
3198
4121
  usageEntries = [];
3199
4122
  pendingJobs = [];
@@ -3206,6 +4129,22 @@ class EpistolaAdminPageComponent {
3206
4129
  this.route = route;
3207
4130
  this.router = router;
3208
4131
  }
4132
+ /** Violations from the latest report (empty when healthy or not yet loaded). */
4133
+ get validationViolations() {
4134
+ return this.validationReport?.violations ?? [];
4135
+ }
4136
+ /** Scan cadence in whole minutes, for the "refreshes every N min" note. */
4137
+ get refreshIntervalMinutes() {
4138
+ return Math.round((this.validationReport?.refreshIntervalMs ?? 600000) / 60000);
4139
+ }
4140
+ /** Combined "forms needing attention" count for the tab badge (carrier + legacy override). */
4141
+ get formsAttentionCount() {
4142
+ return (this.formIssues?.length ?? 0) + (this.legacyOverrideForms?.length ?? 0);
4143
+ }
4144
+ /** Whether the forms tab has loaded at least one of its two scans. */
4145
+ get formsScanLoaded() {
4146
+ return this.formIssues !== null || this.legacyOverrideForms !== null;
4147
+ }
3209
4148
  ngOnInit() {
3210
4149
  this.deepLinkConfigId = this.route.snapshot.queryParamMap.get('configurationId');
3211
4150
  const tab = this.route.snapshot.queryParamMap.get('tab');
@@ -3232,11 +4171,96 @@ class EpistolaAdminPageComponent {
3232
4171
  this.activeTab = tab;
3233
4172
  this.updateUrl(this.selectedCard?.configurationId ?? null, tab);
3234
4173
  }
4174
+ // 'forms' is TEMPORARY (remove in 1.0.0).
3235
4175
  setOverviewTab(tab) {
3236
4176
  this.overviewTab = tab;
3237
4177
  if (tab === 'changelog' && this.changelog === null && !this.changelogLoading) {
3238
4178
  this.loadChangelog();
3239
4179
  }
4180
+ if (tab === 'forms' && this.formIssues === null && !this.formIssuesLoading) {
4181
+ this.loadFormIssues();
4182
+ }
4183
+ if (tab === 'forms' && this.legacyOverrideForms === null && !this.legacyOverrideLoading) {
4184
+ this.loadLegacyOverrideForms();
4185
+ }
4186
+ }
4187
+ // ---- TEMPORARY: legacy override-mapping format detection ----
4188
+ loadLegacyOverrideForms() {
4189
+ this.legacyOverrideLoading = true;
4190
+ this.adminService.getLegacyOverrideForms().subscribe({
4191
+ next: (forms) => {
4192
+ this.legacyOverrideForms = forms;
4193
+ this.legacyOverrideLoading = false;
4194
+ },
4195
+ error: () => {
4196
+ this.legacyOverrideForms = [];
4197
+ this.legacyOverrideLoading = false;
4198
+ },
4199
+ });
4200
+ }
4201
+ // ---- TEMPORARY (removed in 1.0.0): task-id carrier detection + repair ----
4202
+ loadFormIssues() {
4203
+ this.formIssuesLoading = true;
4204
+ this.formFeedback = null;
4205
+ this.adminService.getFormCarrierIssues().subscribe({
4206
+ next: (issues) => {
4207
+ this.formIssues = issues;
4208
+ this.formIssuesLoading = false;
4209
+ },
4210
+ error: () => {
4211
+ this.formIssues = [];
4212
+ this.formIssuesLoading = false;
4213
+ },
4214
+ });
4215
+ }
4216
+ isRepairingForm(issue) {
4217
+ return this.repairingFormIds.has(issue.formId);
4218
+ }
4219
+ repairForm(issue) {
4220
+ if (this.repairingFormIds.has(issue.formId)) {
4221
+ return;
4222
+ }
4223
+ this.repairingFormIds.add(issue.formId);
4224
+ this.formFeedback = null;
4225
+ this.adminService.repairFormCarrier(issue.formId).subscribe({
4226
+ next: (result) => {
4227
+ this.repairingFormIds.delete(issue.formId);
4228
+ this.formFeedback = {
4229
+ formId: issue.formId,
4230
+ type: 'success',
4231
+ message: `OK — ${result.componentsPatched} component(s) patched`,
4232
+ };
4233
+ this.loadFormIssues();
4234
+ },
4235
+ error: (err) => {
4236
+ this.repairingFormIds.delete(issue.formId);
4237
+ const message = err?.error?.errorMessage ?? err?.error?.message ?? err?.message ?? 'unknown error';
4238
+ this.formFeedback = { formId: issue.formId, type: 'error', message };
4239
+ },
4240
+ });
4241
+ }
4242
+ repairAllForms() {
4243
+ if (this.repairingAll) {
4244
+ return;
4245
+ }
4246
+ this.repairingAll = true;
4247
+ this.formFeedback = null;
4248
+ this.adminService.repairAllFormCarriers().subscribe({
4249
+ next: (summary) => {
4250
+ this.repairingAll = false;
4251
+ this.formFeedback = {
4252
+ formId: 'all',
4253
+ type: summary.failed > 0 ? 'error' : 'success',
4254
+ message: `Repaired ${summary.formsRepaired} form(s), ${summary.componentsPatched} component(s)${summary.failed > 0 ? `, ${summary.failed} failed` : ''}`,
4255
+ };
4256
+ this.loadFormIssues();
4257
+ },
4258
+ error: (err) => {
4259
+ this.repairingAll = false;
4260
+ const message = err?.error?.message ?? err?.message ?? 'unknown error';
4261
+ this.formFeedback = { formId: 'all', type: 'error', message };
4262
+ },
4263
+ });
3240
4264
  }
3241
4265
  loadChangelog() {
3242
4266
  this.changelogLoading = true;
@@ -3425,14 +4449,14 @@ class EpistolaAdminPageComponent {
3425
4449
  this.tryBuildCards();
3426
4450
  },
3427
4451
  });
3428
- // Validation violations are independent of cards — load alongside but don't
3429
- // gate the loading flag on them.
3430
- this.adminService.getValidationViolations().subscribe({
3431
- next: (violations) => {
3432
- this.validationViolations = violations;
4452
+ // Validation report is independent of cards — load alongside but don't
4453
+ // gate the loading flag on it.
4454
+ this.adminService.getValidationReport().subscribe({
4455
+ next: (report) => {
4456
+ this.validationReport = report;
3433
4457
  },
3434
4458
  error: () => {
3435
- this.validationViolations = [];
4459
+ this.validationReport = null;
3436
4460
  },
3437
4461
  });
3438
4462
  }
@@ -3476,14 +4500,31 @@ class EpistolaAdminPageComponent {
3476
4500
  },
3477
4501
  });
3478
4502
  }
3479
- 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 });
3480
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaAdminPageComponent, isStandalone: true, selector: "epistola-admin-page", ngImport: i0, template: "<div class=\"epistola-admin\">\n <!-- Overview: tabs (no configuration selected) -->\n <ng-container *ngIf=\"!selectedCard\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"d-flex align-items-center\">\n <h5 class=\"mb-0\">{{ 'epistolaAdminOverview' | pluginTranslate: 'epistola' | async }}</h5>\n <span *ngIf=\"pluginVersion\" class=\"version-badge ms-2\">v{{ pluginVersion }}</span>\n </div>\n <button class=\"btn btn-outline-primary btn-sm\" (click)=\"refresh()\" [disabled]=\"loading\">\n {{ 'epistolaAdminRefresh' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <ng-template #configurationsHeading>\n {{ 'epistolaAdminConfigurations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ cards.length }}</cds-tag>\n </ng-template>\n\n <ng-template #validationsHeading>\n {{ 'epistolaAdminValidations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" [type]=\"validationViolations.length > 0 ? 'red' : 'gray'\" class=\"ms-1\">\n {{ validationViolations.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #changelogHeading>\n {{ 'epistolaAdminChangelog' | pluginTranslate: 'epistola' | async }}\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"configurationsHeading\"\n [active]=\"overviewTab === 'configurations'\"\n (selected)=\"setOverviewTab('configurations')\"\n >\n <div *ngIf=\"loading\" class=\"text-muted mt-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length === 0\" class=\"text-muted mt-3\">\n {{ 'epistolaAdminNoConfigurations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length > 0\" class=\"card-grid\">\n <div\n *ngFor=\"let card of cards\"\n class=\"config-card\"\n [class.config-card--ok]=\"card.reachable && card.problemCount === 0\"\n [class.config-card--warning]=\"card.reachable && card.problemCount > 0\"\n [class.config-card--error]=\"!card.reachable\"\n (click)=\"selectConfiguration(card)\"\n >\n <div class=\"config-card__header\">\n <span\n class=\"status-dot\"\n [class.status-dot--ok]=\"card.reachable\"\n [class.status-dot--error]=\"!card.reachable\"\n >\n </span>\n <h5 class=\"config-card__title\">{{ card.configurationTitle }}</h5>\n </div>\n\n <div class=\"config-card__body\">\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async\n }}</span>\n <code class=\"config-card__value\">{{ card.tenantId }}</code>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" [type]=\"card.reachable ? 'green' : 'red'\">\n {{\n card.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n </div>\n <div *ngIf=\"card.serverVersion\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.serverVersion }}</span>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.usageCount }}</span>\n </div>\n <div *ngIf=\"card.pendingJobs.length > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">{{ card.pendingJobs.length }}</cds-tag>\n </div>\n <div *ngIf=\"card.problemCount > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"red\">{{ card.problemCount }}</cds-tag>\n </div>\n </div>\n\n <div class=\"config-card__footer\">\n <span class=\"config-card__latency\">{{ card.latencyMs }} ms</span>\n </div>\n </div>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"validationsHeading\"\n [active]=\"overviewTab === 'validations'\"\n (selected)=\"setOverviewTab('validations')\"\n >\n <div *ngIf=\"validationViolations.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoValidations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"validationViolations.length > 0\" class=\"mt-3\">\n <p class=\"text-muted mb-3\">\n {{ 'epistolaAdminValidationWarningBody' | pluginTranslate: 'epistola' | async }}\n </p>\n <table class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminValidationCode' | pluginTranslate: 'epistola' | async }}</th>\n <th>\n {{ 'epistolaAdminValidationMessage' | pluginTranslate: 'epistola' | async }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let v of validationViolations\">\n <td>\n {{ v.processDefinitionName || v.processDefinitionKey }}\n <div *ngIf=\"v.processDefinitionName\" class=\"text-muted small\">\n <code>{{ v.processDefinitionKey }}</code>\n </div>\n </td>\n <td>\n <code>{{ v.activityId }}</code>\n </td>\n <td>\n <cds-tag size=\"sm\" type=\"red\">{{ v.code }}</cds-tag>\n </td>\n <td>{{ v.message }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"changelogHeading\"\n [active]=\"overviewTab === 'changelog'\"\n (selected)=\"setOverviewTab('changelog')\"\n >\n <div class=\"d-flex align-items-center mt-3 mb-3\">\n <span class=\"text-muted me-2\">{{\n 'epistolaAdminRunningVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">v{{ pluginVersion || '\u2014' }}</cds-tag>\n </div>\n\n <div *ngIf=\"changelogLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoChangelog' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length > 0\"\n class=\"epistola-changelog\"\n >\n <section *ngFor=\"let release of changelog\" class=\"changelog-release\">\n <div class=\"changelog-release__header\">\n <cds-tag size=\"sm\" type=\"purple\">{{ release.version }}</cds-tag>\n <span *ngIf=\"release.date\" class=\"text-muted ms-2\">{{ release.date }}</span>\n </div>\n <div *ngFor=\"let section of release.sections\" class=\"changelog-section\">\n <h6 class=\"changelog-section__title\">{{ section.title }}</h6>\n <ul class=\"changelog-section__items\">\n <li *ngFor=\"let item of section.items\">{{ item }}</li>\n </ul>\n </div>\n </section>\n </div>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n\n <!-- Detail view: selected configuration -->\n <ng-container *ngIf=\"selectedCard\">\n <div class=\"detail-header mb-3\">\n <button class=\"btn btn-link btn-sm p-0\" (click)=\"backToOverview()\">\n &larr; {{ 'epistolaAdminBackToOverview' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div class=\"detail-summary mb-4\">\n <h4>\n <span\n class=\"status-dot me-2\"\n [class.status-dot--ok]=\"selectedCard.reachable\"\n [class.status-dot--error]=\"!selectedCard.reachable\"\n >\n </span>\n {{ selectedCard.configurationTitle }}\n </h4>\n\n <table class=\"table table-sm detail-info-table\">\n <tbody>\n <tr>\n <th>{{ 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <code>{{ selectedCard.tenantId }}</code>\n </td>\n </tr>\n <tr>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <cds-tag size=\"sm\" [type]=\"selectedCard.reachable ? 'green' : 'red'\">\n {{\n selectedCard.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n <span class=\"text-muted ms-2\">{{ selectedCard.latencyMs }} ms</span>\n </td>\n </tr>\n <tr *ngIf=\"selectedCard.serverVersion\">\n <th>{{ 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async }}</th>\n <td>{{ selectedCard.serverVersion }}</td>\n </tr>\n <tr *ngIf=\"selectedCard.errorMessage\">\n <th>{{ 'epistolaAdminError' | pluginTranslate: 'epistola' | async }}</th>\n <td class=\"text-danger\">{{ selectedCard.errorMessage }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Tabs -->\n <ng-template #actionsHeading>\n {{ 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ selectedCard.usageEntries.length }}</cds-tag>\n </ng-template>\n\n <ng-template #pendingHeading>\n {{ 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n size=\"sm\"\n [type]=\"selectedCard.pendingJobs.length > 0 ? 'blue' : 'gray'\"\n class=\"ms-1\"\n >\n {{ selectedCard.pendingJobs.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #catalogsHeading>\n {{ 'epistolaAdminCatalogs' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ catalogs.length }}</cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"actionsHeading\"\n [active]=\"activeTab === 'actions'\"\n (selected)=\"setActiveTab('actions')\"\n >\n <div *ngIf=\"selectedCard.usageEntries.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoUsageForConfig' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.usageEntries.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCase' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminAction' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let entry of selectedCard.usageEntries\"\n [class.table-warning]=\"entry.problems.length > 0\"\n >\n <td>{{ entry.caseDefinitionKey || '-' }}</td>\n <td>\n <a\n *ngIf=\"entry.caseDefinitionKey && entry.caseDefinitionVersionTag\"\n [routerLink]=\"[\n '/case-management',\n 'case',\n entry.caseDefinitionKey,\n 'version',\n entry.caseDefinitionVersionTag,\n 'processes',\n entry.processDefinitionKey,\n ]\"\n class=\"usage-link\"\n >\n {{ entry.processDefinitionName }}\n </a>\n <span *ngIf=\"!entry.caseDefinitionKey || !entry.caseDefinitionVersionTag\">\n {{ entry.processDefinitionName }}\n </span>\n </td>\n <td>{{ entry.activityName }}</td>\n <td>\n <code>{{ entry.actionKey }}</code>\n </td>\n <td>\n <cds-tag *ngIf=\"entry.problems.length === 0\" size=\"sm\" type=\"green\">OK</cds-tag>\n <cds-tag\n *ngFor=\"let problem of entry.problems\"\n size=\"sm\"\n type=\"red\"\n class=\"d-block mb-1\"\n >\n {{ problem }}\n </cds-tag>\n </td>\n <td>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"exportProcessLink(entry)\"\n [title]=\"'epistolaAdminExport' | pluginTranslate: 'epistola' | async\"\n >\n &#x2913;\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"pendingHeading\"\n [active]=\"activeTab === 'pending'\"\n (selected)=\"setActiveTab('pending')\"\n >\n <div *ngIf=\"selectedCard.pendingJobs.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoPendingJobs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.pendingJobs.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminRequestId' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let job of selectedCard.pendingJobs\">\n <td>{{ job.processDefinitionName }}</td>\n <td>{{ job.activityName }}</td>\n <td>\n <code>{{ job.requestId }}</code>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"reconcilePending(job)\"\n [disabled]=\"isReconciling(job)\"\n [title]=\"'epistolaAdminReconcileTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isReconciling(job)\">\n {{ 'epistolaAdminReconcile' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isReconciling(job)\">\n {{ 'epistolaAdminReconciling' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"reconcileFeedback && reconcileFeedback.executionId === job.executionId\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"reconcileFeedback.type === 'success'\"\n [class.text-warning]=\"reconcileFeedback.type === 'pending'\"\n [class.text-danger]=\"reconcileFeedback.type === 'error'\"\n >\n {{ reconcileFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"catalogsHeading\"\n [active]=\"activeTab === 'catalogs'\"\n (selected)=\"setActiveTab('catalogs')\"\n >\n <p class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminCatalogsIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"catalogsLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!catalogsLoading && catalogs.length === 0\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminNoCatalogs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"!catalogsLoading && catalogs.length > 0\" class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCatalogSlug' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogVersion' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogStatus' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let catalog of catalogs\">\n <td>\n <code>{{ catalog.slug }}</code>\n </td>\n <td>{{ catalog.version }}</td>\n <td>\n <cds-tag *ngIf=\"catalog.status === 'IN_EPISTOLA'\" size=\"sm\" type=\"green\">\n {{ 'epistolaAdminCatalogInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'NOT_IN_EPISTOLA'\" size=\"sm\" type=\"red\">\n {{ 'epistolaAdminCatalogNotInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'UNKNOWN'\" size=\"sm\" type=\"gray\">\n {{ 'epistolaAdminCatalogStatusUnknown' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"redeployCatalog(catalog)\"\n [disabled]=\"isRedeploying(catalog)\"\n [title]=\"'epistolaAdminRedeployTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploy' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploying' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"catalogFeedback && catalogFeedback.slug === catalog.slug\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"catalogFeedback.type === 'success'\"\n [class.text-danger]=\"catalogFeedback.type === 'error'\"\n >\n {{ catalogFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n</div>\n", styles: [".epistola-admin{padding:1.5rem}.epistola-admin .version-badge{font-size:.75rem;font-weight:500;padding:.2em .6em;border-radius:4px;background-color:#e8e8e8;color:#525252}.epistola-admin .badge{font-size:.85em;padding:.35em .65em}.epistola-admin code{font-size:.9em;color:#525252}.epistola-admin table th{font-weight:600;white-space:nowrap}.epistola-admin .status-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:.5rem;flex-shrink:0;background-color:#adb5bd}.epistola-admin .status-dot--ok{background-color:#198754}.epistola-admin .status-dot--error{background-color:#dc3545}.epistola-admin .card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.epistola-admin .config-card{border:1px solid #dee2e6;border-radius:8px;padding:1.25rem;cursor:pointer;transition:box-shadow .15s ease,border-color .15s ease;background:#fff}.epistola-admin .config-card:hover{box-shadow:0 2px 8px #0000001a}.epistola-admin .config-card--ok{border-left:4px solid #198754}.epistola-admin .config-card--warning{border-left:4px solid #ffc107}.epistola-admin .config-card--error{border-left:4px solid #dc3545}.epistola-admin .config-card__header{display:flex;align-items:center;margin-bottom:1rem}.epistola-admin .config-card__title{margin:0;font-size:1.05rem;font-weight:600;color:#161616}.epistola-admin .config-card__body{display:flex;flex-direction:column;gap:.5rem}.epistola-admin .config-card__field{display:flex;justify-content:space-between;align-items:center}.epistola-admin .config-card__label{font-size:.875rem;color:#6c757d}.epistola-admin .config-card__value{font-size:.875rem;color:#161616}.epistola-admin .config-card__footer{margin-top:1rem;padding-top:.75rem;border-top:1px solid #f0f0f0;text-align:right}.epistola-admin .config-card__latency{font-size:.8rem;color:#adb5bd}.epistola-admin .usage-link{color:#0f62fe;text-decoration:none}.epistola-admin .usage-link:hover{text-decoration:underline}.epistola-admin .detail-info-table{max-width:500px}.epistola-admin .detail-info-table th{width:140px}.epistola-admin .detail-summary{padding:1rem 0;border-bottom:1px solid #dee2e6}.epistola-admin .epistola-changelog{max-height:60vh;overflow:auto;padding:.5rem 1rem;border:1px solid #dee2e6;border-radius:4px}.epistola-admin .epistola-changelog .changelog-release{padding:.75rem 0}.epistola-admin .epistola-changelog .changelog-release+.changelog-release{border-top:1px solid #eee}.epistola-admin .epistola-changelog .changelog-release__header{display:flex;align-items:center;margin-bottom:.5rem}.epistola-admin .epistola-changelog .changelog-section{margin:.25rem 0 .75rem}.epistola-admin .epistola-changelog .changelog-section__title{font-size:.8125rem;font-weight:600;text-transform:uppercase;letter-spacing:.02em;color:#6f6f6f;margin-bottom:.25rem}.epistola-admin .epistola-changelog .changelog-section__items{margin:0;padding-left:1.25rem;font-size:.8125rem;line-height:1.5}.epistola-admin .epistola-changelog .changelog-section__items li{margin-bottom:.25rem;word-break:break-word}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2$5.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: TabsModule }, { kind: "component", type: i5$1.Tabs, selector: "cds-tabs, ibm-tabs", inputs: ["position", "cacheActive", "followFocus", "isNavigation", "ariaLabel", "ariaLabelledby", "type", "theme", "skeleton"] }, { kind: "component", type: i5$1.Tab, selector: "cds-tab, ibm-tab", inputs: ["heading", "title", "context", "active", "disabled", "tabIndex", "id", "cacheActive", "tabContent", "templateContext"], outputs: ["selected"] }, { kind: "ngmodule", type: TagModule }, { kind: "component", type: i6.Tag, selector: "cds-tag, ibm-tag", inputs: ["type", "size", "class", "skeleton"] }] });
4503
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminPageComponent, deps: [{ token: EpistolaAdminService }, { token: i2$4.ActivatedRoute }, { token: i2$4.Router }], target: i0.ɵɵFactoryTarget.Component });
4504
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaAdminPageComponent, isStandalone: true, selector: "epistola-admin-page", ngImport: i0, template: "<div class=\"epistola-admin\">\n <!-- Overview: tabs (no configuration selected) -->\n <ng-container *ngIf=\"!selectedCard\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"d-flex align-items-center\">\n <h5 class=\"mb-0\">{{ 'epistolaAdminOverview' | pluginTranslate: 'epistola' | async }}</h5>\n <span *ngIf=\"pluginVersion\" class=\"version-badge ms-2\">v{{ pluginVersion }}</span>\n </div>\n <button class=\"btn btn-outline-primary btn-sm\" (click)=\"refresh()\" [disabled]=\"loading\">\n {{ 'epistolaAdminRefresh' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <ng-template #configurationsHeading>\n {{ 'epistolaAdminConfigurations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ cards.length }}</cds-tag>\n </ng-template>\n\n <ng-template #validationsHeading>\n {{ 'epistolaAdminValidations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" [type]=\"validationViolations.length > 0 ? 'red' : 'gray'\" class=\"ms-1\">\n {{ validationViolations.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #changelogHeading>\n {{ 'epistolaAdminChangelog' | pluginTranslate: 'epistola' | async }}\n </ng-template>\n\n <!-- TEMPORARY (removed in 1.0.0): forms missing the task-id carrier -->\n <ng-template #formsHeading>\n {{ 'epistolaAdminForms' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n *ngIf=\"formsScanLoaded\"\n size=\"sm\"\n [type]=\"formsAttentionCount > 0 ? 'red' : 'gray'\"\n class=\"ms-1\"\n >{{ formsAttentionCount }}</cds-tag\n >\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 class=\"text-muted small mt-3\">\n <div>\n {{ 'epistolaAdminValidationLastChecked' | pluginTranslate: 'epistola' | async }}:\n <ng-container *ngIf=\"validationReport?.lastCheckedAt; else notYetRun\">\n {{ validationReport?.lastCheckedAt | date: 'medium' }}\n </ng-container>\n <ng-template #notYetRun>\n {{ 'epistolaAdminValidationNotYetRun' | pluginTranslate: 'epistola' | async }}\n </ng-template>\n \u00B7 {{ 'epistolaAdminValidationAutoRefresh' | pluginTranslate: 'epistola' | async }}\n {{ refreshIntervalMinutes }} min.\n </div>\n <div>\n {{ 'epistolaAdminValidationLatestVersionNote' | pluginTranslate: 'epistola' | async }}\n </div>\n </div>\n\n <div *ngIf=\"validationViolations.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoValidations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"validationViolations.length > 0\" class=\"mt-3\">\n <p class=\"text-muted mb-3\">\n {{ 'epistolaAdminValidationWarningBody' | pluginTranslate: 'epistola' | async }}\n </p>\n <table class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminValidationCode' | pluginTranslate: 'epistola' | async }}</th>\n <th>\n {{ 'epistolaAdminValidationMessage' | pluginTranslate: 'epistola' | async }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let v of validationViolations\">\n <td>\n {{ v.processDefinitionName || v.processDefinitionKey }}\n <div *ngIf=\"v.processDefinitionName\" class=\"text-muted small\">\n <code>{{ v.processDefinitionKey }}</code>\n </div>\n </td>\n <td>\n <code>{{ v.activityId }}</code>\n </td>\n <td>\n <cds-tag size=\"sm\" type=\"red\">{{ v.code }}</cds-tag>\n </td>\n <td>{{ v.message }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"changelogHeading\"\n [active]=\"overviewTab === 'changelog'\"\n (selected)=\"setOverviewTab('changelog')\"\n >\n <div class=\"d-flex align-items-center mt-3 mb-3\">\n <span class=\"text-muted me-2\">{{\n 'epistolaAdminRunningVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">v{{ pluginVersion || '\u2014' }}</cds-tag>\n </div>\n\n <div *ngIf=\"changelogLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoChangelog' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length > 0\"\n class=\"epistola-changelog\"\n >\n <section *ngFor=\"let release of changelog\" class=\"changelog-release\">\n <div class=\"changelog-release__header\">\n <cds-tag size=\"sm\" type=\"purple\">{{ release.version }}</cds-tag>\n <span *ngIf=\"release.date\" class=\"text-muted ms-2\">{{ release.date }}</span>\n </div>\n <div *ngFor=\"let section of release.sections\" class=\"changelog-section\">\n <h6 class=\"changelog-section__title\">{{ section.title }}</h6>\n <ul class=\"changelog-section__items\">\n <li *ngFor=\"let item of section.items\">{{ item }}</li>\n </ul>\n </div>\n </section>\n </div>\n </cds-tab>\n\n <!-- TEMPORARY (removed in 1.0.0): forms missing the task-id carrier -->\n <cds-tab\n [heading]=\"formsHeading\"\n [active]=\"overviewTab === 'forms'\"\n (selected)=\"setOverviewTab('forms')\"\n >\n <p class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminFormsIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"formIssuesLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!formIssuesLoading && formIssues && formIssues.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoFormIssues' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!formIssuesLoading && formIssues && formIssues.length > 0\">\n <div class=\"mb-2\">\n <button\n class=\"btn btn-sm btn-primary\"\n (click)=\"repairAllForms()\"\n [disabled]=\"repairingAll\"\n >\n {{\n (repairingAll ? 'epistolaAdminRepairing' : 'epistolaAdminRepairAll')\n | pluginTranslate: 'epistola'\n | async\n }}\n </button>\n <span\n *ngIf=\"formFeedback && formFeedback.formId === 'all'\"\n class=\"small ms-2\"\n [class.text-success]=\"formFeedback.type === 'success'\"\n [class.text-danger]=\"formFeedback.type === 'error'\"\n >{{ formFeedback.message }}</span\n >\n </div>\n\n <table class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminFormName' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminFormMissing' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let issue of formIssues\">\n <td>\n <code>{{ issue.name }}</code>\n <cds-tag\n *ngIf=\"issue.readOnly\"\n size=\"sm\"\n type=\"warm-gray\"\n class=\"ms-1\"\n [title]=\"'epistolaAdminFormReadOnlyHint' | pluginTranslate: 'epistola' | async\"\n >{{\n 'epistolaAdminFormReadOnly' | pluginTranslate: 'epistola' | async\n }}</cds-tag\n >\n </td>\n <td>{{ issue.missingComponents }}</td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"repairForm(issue)\"\n [disabled]=\"isRepairingForm(issue)\"\n [title]=\"'epistolaAdminRepairTooltip' | pluginTranslate: 'epistola' | async\"\n >\n {{\n (isRepairingForm(issue) ? 'epistolaAdminRepairing' : 'epistolaAdminRepair')\n | pluginTranslate: 'epistola'\n | async\n }}\n </button>\n <div\n *ngIf=\"formFeedback && formFeedback.formId === issue.formId\"\n class=\"small mt-1\"\n [class.text-success]=\"formFeedback.type === 'success'\"\n [class.text-danger]=\"formFeedback.type === 'error'\"\n >\n {{ formFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- TEMPORARY: forms still using the legacy override-mapping object format -->\n <hr class=\"my-4\" />\n <h5 class=\"mb-1\">\n {{ 'epistolaAdminLegacyOverrideTitle' | pluginTranslate: 'epistola' | async }}\n </h5>\n <p class=\"text-muted mb-3\">\n {{ 'epistolaAdminLegacyOverrideIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"legacyOverrideLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!legacyOverrideLoading && legacyOverrideForms && legacyOverrideForms.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoLegacyOverride' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table\n *ngIf=\"!legacyOverrideLoading && legacyOverrideForms && legacyOverrideForms.length > 0\"\n class=\"table table-striped\"\n >\n <thead>\n <tr>\n <th>{{ 'epistolaAdminFormName' | pluginTranslate: 'epistola' | async }}</th>\n <th>\n {{ 'epistolaAdminLegacyOverrideComponents' | pluginTranslate: 'epistola' | async }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let form of legacyOverrideForms\">\n <td>\n <code>{{ form.name }}</code>\n <cds-tag\n *ngIf=\"form.readOnly\"\n size=\"sm\"\n type=\"warm-gray\"\n class=\"ms-1\"\n [title]=\"'epistolaAdminFormReadOnlyHint' | pluginTranslate: 'epistola' | async\"\n >{{ 'epistolaAdminFormReadOnly' | pluginTranslate: 'epistola' | async }}</cds-tag\n >\n </td>\n <td>{{ form.legacyComponents }}</td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n\n <!-- Detail view: selected configuration -->\n <ng-container *ngIf=\"selectedCard\">\n <div class=\"detail-header mb-3\">\n <button class=\"btn btn-link btn-sm p-0\" (click)=\"backToOverview()\">\n &larr; {{ 'epistolaAdminBackToOverview' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div class=\"detail-summary mb-4\">\n <h4>\n <span\n class=\"status-dot me-2\"\n [class.status-dot--ok]=\"selectedCard.reachable\"\n [class.status-dot--error]=\"!selectedCard.reachable\"\n >\n </span>\n {{ selectedCard.configurationTitle }}\n </h4>\n\n <table class=\"table table-sm detail-info-table\">\n <tbody>\n <tr>\n <th>{{ 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <code>{{ selectedCard.tenantId }}</code>\n </td>\n </tr>\n <tr>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <cds-tag size=\"sm\" [type]=\"selectedCard.reachable ? 'green' : 'red'\">\n {{\n selectedCard.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n <span class=\"text-muted ms-2\">{{ selectedCard.latencyMs }} ms</span>\n </td>\n </tr>\n <tr *ngIf=\"selectedCard.serverVersion\">\n <th>{{ 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async }}</th>\n <td>{{ selectedCard.serverVersion }}</td>\n </tr>\n <tr *ngIf=\"selectedCard.errorMessage\">\n <th>{{ 'epistolaAdminError' | pluginTranslate: 'epistola' | async }}</th>\n <td class=\"text-danger\">{{ selectedCard.errorMessage }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Tabs -->\n <ng-template #actionsHeading>\n {{ 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ selectedCard.usageEntries.length }}</cds-tag>\n </ng-template>\n\n <ng-template #pendingHeading>\n {{ 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n size=\"sm\"\n [type]=\"selectedCard.pendingJobs.length > 0 ? 'blue' : 'gray'\"\n class=\"ms-1\"\n >\n {{ selectedCard.pendingJobs.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #catalogsHeading>\n {{ 'epistolaAdminCatalogs' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ catalogs.length }}</cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"actionsHeading\"\n [active]=\"activeTab === 'actions'\"\n (selected)=\"setActiveTab('actions')\"\n >\n <div *ngIf=\"selectedCard.usageEntries.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoUsageForConfig' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.usageEntries.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCase' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminAction' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let entry of selectedCard.usageEntries\"\n [class.table-warning]=\"entry.problems.length > 0\"\n >\n <td>{{ entry.caseDefinitionKey || '-' }}</td>\n <td>\n <a\n *ngIf=\"entry.caseDefinitionKey && entry.caseDefinitionVersionTag\"\n [routerLink]=\"[\n '/case-management',\n 'case',\n entry.caseDefinitionKey,\n 'version',\n entry.caseDefinitionVersionTag,\n 'processes',\n entry.processDefinitionKey,\n ]\"\n class=\"usage-link\"\n >\n {{ entry.processDefinitionName }}\n </a>\n <span *ngIf=\"!entry.caseDefinitionKey || !entry.caseDefinitionVersionTag\">\n {{ entry.processDefinitionName }}\n </span>\n </td>\n <td>{{ entry.activityName }}</td>\n <td>\n <code>{{ entry.actionKey }}</code>\n </td>\n <td>\n <cds-tag *ngIf=\"entry.problems.length === 0\" size=\"sm\" type=\"green\">OK</cds-tag>\n <cds-tag\n *ngFor=\"let problem of entry.problems\"\n size=\"sm\"\n type=\"red\"\n class=\"d-block mb-1\"\n >\n {{ problem }}\n </cds-tag>\n </td>\n <td>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"exportProcessLink(entry)\"\n [title]=\"'epistolaAdminExport' | pluginTranslate: 'epistola' | async\"\n >\n &#x2913;\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"pendingHeading\"\n [active]=\"activeTab === 'pending'\"\n (selected)=\"setActiveTab('pending')\"\n >\n <div *ngIf=\"selectedCard.pendingJobs.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoPendingJobs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.pendingJobs.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminStatus' | 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 <cds-tag\n *ngIf=\"job.status === 'UNWIRED'; else waitingTag\"\n size=\"sm\"\n type=\"red\"\n [title]=\"'epistolaAdminUnwiredTooltip' | pluginTranslate: 'epistola' | async\"\n >\n {{ 'epistolaAdminStatusUnwired' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <ng-template #waitingTag>\n <cds-tag size=\"sm\" type=\"blue\">\n {{ 'epistolaAdminStatusWaiting' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n </ng-template>\n </td>\n <td>\n <code *ngIf=\"job.status !== 'UNWIRED'\">{{ job.requestId }}</code>\n <span *ngIf=\"job.status === 'UNWIRED'\" class=\"text-muted\">\u2014</span>\n </td>\n <td class=\"text-end\">\n <button\n *ngIf=\"job.status !== 'UNWIRED'\"\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 <span\n *ngIf=\"job.status === 'UNWIRED'\"\n class=\"text-muted small\"\n [title]=\"'epistolaAdminUnwiredTooltip' | pluginTranslate: 'epistola' | async\"\n >\n {{ 'epistolaAdminUnwiredHint' | pluginTranslate: 'epistola' | async }}\n </span>\n <div\n *ngIf=\"reconcileFeedback && reconcileFeedback.executionId === job.executionId\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"reconcileFeedback.type === 'success'\"\n [class.text-warning]=\"reconcileFeedback.type === 'pending'\"\n [class.text-danger]=\"reconcileFeedback.type === 'error'\"\n >\n {{ reconcileFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"catalogsHeading\"\n [active]=\"activeTab === 'catalogs'\"\n (selected)=\"setActiveTab('catalogs')\"\n >\n <p class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminCatalogsIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"catalogsLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!catalogsLoading && catalogs.length === 0\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminNoCatalogs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"!catalogsLoading && catalogs.length > 0\" class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCatalogSlug' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogVersion' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogStatus' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let catalog of catalogs\">\n <td>\n <code>{{ catalog.slug }}</code>\n </td>\n <td>{{ catalog.version }}</td>\n <td>\n <cds-tag *ngIf=\"catalog.status === 'IN_EPISTOLA'\" size=\"sm\" type=\"green\">\n {{ 'epistolaAdminCatalogInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'NOT_IN_EPISTOLA'\" size=\"sm\" type=\"red\">\n {{ 'epistolaAdminCatalogNotInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'UNKNOWN'\" size=\"sm\" type=\"gray\">\n {{ 'epistolaAdminCatalogStatusUnknown' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"redeployCatalog(catalog)\"\n [disabled]=\"isRedeploying(catalog)\"\n [title]=\"'epistolaAdminRedeployTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploy' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploying' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"catalogFeedback && catalogFeedback.slug === catalog.slug\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"catalogFeedback.type === 'success'\"\n [class.text-danger]=\"catalogFeedback.type === 'error'\"\n >\n {{ catalogFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n</div>\n", styles: [".epistola-admin{padding:1.5rem}.epistola-admin .version-badge{font-size:.75rem;font-weight:500;padding:.2em .6em;border-radius:4px;background-color:#e8e8e8;color:#525252}.epistola-admin .badge{font-size:.85em;padding:.35em .65em}.epistola-admin code{font-size:.9em;color:#525252}.epistola-admin table th{font-weight:600;white-space:nowrap}.epistola-admin .status-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:.5rem;flex-shrink:0;background-color:#adb5bd}.epistola-admin .status-dot--ok{background-color:#198754}.epistola-admin .status-dot--error{background-color:#dc3545}.epistola-admin .card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.epistola-admin .config-card{border:1px solid #dee2e6;border-radius:8px;padding:1.25rem;cursor:pointer;transition:box-shadow .15s ease,border-color .15s ease;background:#fff}.epistola-admin .config-card:hover{box-shadow:0 2px 8px #0000001a}.epistola-admin .config-card--ok{border-left:4px solid #198754}.epistola-admin .config-card--warning{border-left:4px solid #ffc107}.epistola-admin .config-card--error{border-left:4px solid #dc3545}.epistola-admin .config-card__header{display:flex;align-items:center;margin-bottom:1rem}.epistola-admin .config-card__title{margin:0;font-size:1.05rem;font-weight:600;color:#161616}.epistola-admin .config-card__body{display:flex;flex-direction:column;gap:.5rem}.epistola-admin .config-card__field{display:flex;justify-content:space-between;align-items:center}.epistola-admin .config-card__label{font-size:.875rem;color:#6c757d}.epistola-admin .config-card__value{font-size:.875rem;color:#161616}.epistola-admin .config-card__footer{margin-top:1rem;padding-top:.75rem;border-top:1px solid #f0f0f0;text-align:right}.epistola-admin .config-card__latency{font-size:.8rem;color:#adb5bd}.epistola-admin .usage-link{color:#0f62fe;text-decoration:none}.epistola-admin .usage-link:hover{text-decoration:underline}.epistola-admin .detail-info-table{max-width:500px}.epistola-admin .detail-info-table th{width:140px}.epistola-admin .detail-summary{padding:1rem 0;border-bottom:1px solid #dee2e6}.epistola-admin .epistola-changelog{max-height:60vh;overflow:auto;padding:.5rem 1rem;border:1px solid #dee2e6;border-radius:4px}.epistola-admin .epistola-changelog .changelog-release{padding:.75rem 0}.epistola-admin .epistola-changelog .changelog-release+.changelog-release{border-top:1px solid #eee}.epistola-admin .epistola-changelog .changelog-release__header{display:flex;align-items:center;margin-bottom:.5rem}.epistola-admin .epistola-changelog .changelog-section{margin:.25rem 0 .75rem}.epistola-admin .epistola-changelog .changelog-section__title{font-size:.8125rem;font-weight:600;text-transform:uppercase;letter-spacing:.02em;color:#6f6f6f;margin-bottom:.25rem}.epistola-admin .epistola-changelog .changelog-section__items{margin:0;padding-left:1.25rem;font-size:.8125rem;line-height:1.5}.epistola-admin .epistola-changelog .changelog-section__items li{margin-bottom:.25rem;word-break:break-word}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.DatePipe, name: "date" }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2$4.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: TabsModule }, { kind: "component", type: i5.Tabs, selector: "cds-tabs, ibm-tabs", inputs: ["position", "cacheActive", "followFocus", "isNavigation", "ariaLabel", "ariaLabelledby", "type", "theme", "skeleton"] }, { kind: "component", type: i5.Tab, selector: "cds-tab, ibm-tab", inputs: ["heading", "title", "context", "active", "disabled", "tabIndex", "id", "cacheActive", "tabContent", "templateContext"], outputs: ["selected"] }, { kind: "ngmodule", type: TagModule }, { kind: "component", type: i6.Tag, selector: "cds-tag, ibm-tag", inputs: ["type", "size", "class", "skeleton"] }] });
3481
4505
  }
3482
4506
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminPageComponent, decorators: [{
3483
4507
  type: Component,
3484
- args: [{ selector: 'epistola-admin-page', standalone: true, imports: [CommonModule, RouterModule, PluginTranslatePipeModule, TabsModule, TagModule], template: "<div class=\"epistola-admin\">\n <!-- Overview: tabs (no configuration selected) -->\n <ng-container *ngIf=\"!selectedCard\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"d-flex align-items-center\">\n <h5 class=\"mb-0\">{{ 'epistolaAdminOverview' | pluginTranslate: 'epistola' | async }}</h5>\n <span *ngIf=\"pluginVersion\" class=\"version-badge ms-2\">v{{ pluginVersion }}</span>\n </div>\n <button class=\"btn btn-outline-primary btn-sm\" (click)=\"refresh()\" [disabled]=\"loading\">\n {{ 'epistolaAdminRefresh' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <ng-template #configurationsHeading>\n {{ 'epistolaAdminConfigurations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ cards.length }}</cds-tag>\n </ng-template>\n\n <ng-template #validationsHeading>\n {{ 'epistolaAdminValidations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" [type]=\"validationViolations.length > 0 ? 'red' : 'gray'\" class=\"ms-1\">\n {{ validationViolations.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #changelogHeading>\n {{ 'epistolaAdminChangelog' | pluginTranslate: 'epistola' | async }}\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"configurationsHeading\"\n [active]=\"overviewTab === 'configurations'\"\n (selected)=\"setOverviewTab('configurations')\"\n >\n <div *ngIf=\"loading\" class=\"text-muted mt-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length === 0\" class=\"text-muted mt-3\">\n {{ 'epistolaAdminNoConfigurations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length > 0\" class=\"card-grid\">\n <div\n *ngFor=\"let card of cards\"\n class=\"config-card\"\n [class.config-card--ok]=\"card.reachable && card.problemCount === 0\"\n [class.config-card--warning]=\"card.reachable && card.problemCount > 0\"\n [class.config-card--error]=\"!card.reachable\"\n (click)=\"selectConfiguration(card)\"\n >\n <div class=\"config-card__header\">\n <span\n class=\"status-dot\"\n [class.status-dot--ok]=\"card.reachable\"\n [class.status-dot--error]=\"!card.reachable\"\n >\n </span>\n <h5 class=\"config-card__title\">{{ card.configurationTitle }}</h5>\n </div>\n\n <div class=\"config-card__body\">\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async\n }}</span>\n <code class=\"config-card__value\">{{ card.tenantId }}</code>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" [type]=\"card.reachable ? 'green' : 'red'\">\n {{\n card.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n </div>\n <div *ngIf=\"card.serverVersion\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.serverVersion }}</span>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.usageCount }}</span>\n </div>\n <div *ngIf=\"card.pendingJobs.length > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">{{ card.pendingJobs.length }}</cds-tag>\n </div>\n <div *ngIf=\"card.problemCount > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"red\">{{ card.problemCount }}</cds-tag>\n </div>\n </div>\n\n <div class=\"config-card__footer\">\n <span class=\"config-card__latency\">{{ card.latencyMs }} ms</span>\n </div>\n </div>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"validationsHeading\"\n [active]=\"overviewTab === 'validations'\"\n (selected)=\"setOverviewTab('validations')\"\n >\n <div *ngIf=\"validationViolations.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoValidations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"validationViolations.length > 0\" class=\"mt-3\">\n <p class=\"text-muted mb-3\">\n {{ 'epistolaAdminValidationWarningBody' | pluginTranslate: 'epistola' | async }}\n </p>\n <table class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminValidationCode' | pluginTranslate: 'epistola' | async }}</th>\n <th>\n {{ 'epistolaAdminValidationMessage' | pluginTranslate: 'epistola' | async }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let v of validationViolations\">\n <td>\n {{ v.processDefinitionName || v.processDefinitionKey }}\n <div *ngIf=\"v.processDefinitionName\" class=\"text-muted small\">\n <code>{{ v.processDefinitionKey }}</code>\n </div>\n </td>\n <td>\n <code>{{ v.activityId }}</code>\n </td>\n <td>\n <cds-tag size=\"sm\" type=\"red\">{{ v.code }}</cds-tag>\n </td>\n <td>{{ v.message }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"changelogHeading\"\n [active]=\"overviewTab === 'changelog'\"\n (selected)=\"setOverviewTab('changelog')\"\n >\n <div class=\"d-flex align-items-center mt-3 mb-3\">\n <span class=\"text-muted me-2\">{{\n 'epistolaAdminRunningVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">v{{ pluginVersion || '\u2014' }}</cds-tag>\n </div>\n\n <div *ngIf=\"changelogLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoChangelog' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length > 0\"\n class=\"epistola-changelog\"\n >\n <section *ngFor=\"let release of changelog\" class=\"changelog-release\">\n <div class=\"changelog-release__header\">\n <cds-tag size=\"sm\" type=\"purple\">{{ release.version }}</cds-tag>\n <span *ngIf=\"release.date\" class=\"text-muted ms-2\">{{ release.date }}</span>\n </div>\n <div *ngFor=\"let section of release.sections\" class=\"changelog-section\">\n <h6 class=\"changelog-section__title\">{{ section.title }}</h6>\n <ul class=\"changelog-section__items\">\n <li *ngFor=\"let item of section.items\">{{ item }}</li>\n </ul>\n </div>\n </section>\n </div>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n\n <!-- Detail view: selected configuration -->\n <ng-container *ngIf=\"selectedCard\">\n <div class=\"detail-header mb-3\">\n <button class=\"btn btn-link btn-sm p-0\" (click)=\"backToOverview()\">\n &larr; {{ 'epistolaAdminBackToOverview' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div class=\"detail-summary mb-4\">\n <h4>\n <span\n class=\"status-dot me-2\"\n [class.status-dot--ok]=\"selectedCard.reachable\"\n [class.status-dot--error]=\"!selectedCard.reachable\"\n >\n </span>\n {{ selectedCard.configurationTitle }}\n </h4>\n\n <table class=\"table table-sm detail-info-table\">\n <tbody>\n <tr>\n <th>{{ 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <code>{{ selectedCard.tenantId }}</code>\n </td>\n </tr>\n <tr>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <cds-tag size=\"sm\" [type]=\"selectedCard.reachable ? 'green' : 'red'\">\n {{\n selectedCard.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n <span class=\"text-muted ms-2\">{{ selectedCard.latencyMs }} ms</span>\n </td>\n </tr>\n <tr *ngIf=\"selectedCard.serverVersion\">\n <th>{{ 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async }}</th>\n <td>{{ selectedCard.serverVersion }}</td>\n </tr>\n <tr *ngIf=\"selectedCard.errorMessage\">\n <th>{{ 'epistolaAdminError' | pluginTranslate: 'epistola' | async }}</th>\n <td class=\"text-danger\">{{ selectedCard.errorMessage }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Tabs -->\n <ng-template #actionsHeading>\n {{ 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ selectedCard.usageEntries.length }}</cds-tag>\n </ng-template>\n\n <ng-template #pendingHeading>\n {{ 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n size=\"sm\"\n [type]=\"selectedCard.pendingJobs.length > 0 ? 'blue' : 'gray'\"\n class=\"ms-1\"\n >\n {{ selectedCard.pendingJobs.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #catalogsHeading>\n {{ 'epistolaAdminCatalogs' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ catalogs.length }}</cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"actionsHeading\"\n [active]=\"activeTab === 'actions'\"\n (selected)=\"setActiveTab('actions')\"\n >\n <div *ngIf=\"selectedCard.usageEntries.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoUsageForConfig' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.usageEntries.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCase' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminAction' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let entry of selectedCard.usageEntries\"\n [class.table-warning]=\"entry.problems.length > 0\"\n >\n <td>{{ entry.caseDefinitionKey || '-' }}</td>\n <td>\n <a\n *ngIf=\"entry.caseDefinitionKey && entry.caseDefinitionVersionTag\"\n [routerLink]=\"[\n '/case-management',\n 'case',\n entry.caseDefinitionKey,\n 'version',\n entry.caseDefinitionVersionTag,\n 'processes',\n entry.processDefinitionKey,\n ]\"\n class=\"usage-link\"\n >\n {{ entry.processDefinitionName }}\n </a>\n <span *ngIf=\"!entry.caseDefinitionKey || !entry.caseDefinitionVersionTag\">\n {{ entry.processDefinitionName }}\n </span>\n </td>\n <td>{{ entry.activityName }}</td>\n <td>\n <code>{{ entry.actionKey }}</code>\n </td>\n <td>\n <cds-tag *ngIf=\"entry.problems.length === 0\" size=\"sm\" type=\"green\">OK</cds-tag>\n <cds-tag\n *ngFor=\"let problem of entry.problems\"\n size=\"sm\"\n type=\"red\"\n class=\"d-block mb-1\"\n >\n {{ problem }}\n </cds-tag>\n </td>\n <td>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"exportProcessLink(entry)\"\n [title]=\"'epistolaAdminExport' | pluginTranslate: 'epistola' | async\"\n >\n &#x2913;\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"pendingHeading\"\n [active]=\"activeTab === 'pending'\"\n (selected)=\"setActiveTab('pending')\"\n >\n <div *ngIf=\"selectedCard.pendingJobs.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoPendingJobs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.pendingJobs.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminRequestId' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let job of selectedCard.pendingJobs\">\n <td>{{ job.processDefinitionName }}</td>\n <td>{{ job.activityName }}</td>\n <td>\n <code>{{ job.requestId }}</code>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"reconcilePending(job)\"\n [disabled]=\"isReconciling(job)\"\n [title]=\"'epistolaAdminReconcileTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isReconciling(job)\">\n {{ 'epistolaAdminReconcile' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isReconciling(job)\">\n {{ 'epistolaAdminReconciling' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"reconcileFeedback && reconcileFeedback.executionId === job.executionId\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"reconcileFeedback.type === 'success'\"\n [class.text-warning]=\"reconcileFeedback.type === 'pending'\"\n [class.text-danger]=\"reconcileFeedback.type === 'error'\"\n >\n {{ reconcileFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"catalogsHeading\"\n [active]=\"activeTab === 'catalogs'\"\n (selected)=\"setActiveTab('catalogs')\"\n >\n <p class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminCatalogsIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"catalogsLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!catalogsLoading && catalogs.length === 0\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminNoCatalogs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"!catalogsLoading && catalogs.length > 0\" class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCatalogSlug' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogVersion' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogStatus' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let catalog of catalogs\">\n <td>\n <code>{{ catalog.slug }}</code>\n </td>\n <td>{{ catalog.version }}</td>\n <td>\n <cds-tag *ngIf=\"catalog.status === 'IN_EPISTOLA'\" size=\"sm\" type=\"green\">\n {{ 'epistolaAdminCatalogInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'NOT_IN_EPISTOLA'\" size=\"sm\" type=\"red\">\n {{ 'epistolaAdminCatalogNotInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'UNKNOWN'\" size=\"sm\" type=\"gray\">\n {{ 'epistolaAdminCatalogStatusUnknown' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"redeployCatalog(catalog)\"\n [disabled]=\"isRedeploying(catalog)\"\n [title]=\"'epistolaAdminRedeployTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploy' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploying' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"catalogFeedback && catalogFeedback.slug === catalog.slug\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"catalogFeedback.type === 'success'\"\n [class.text-danger]=\"catalogFeedback.type === 'error'\"\n >\n {{ catalogFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n</div>\n", styles: [".epistola-admin{padding:1.5rem}.epistola-admin .version-badge{font-size:.75rem;font-weight:500;padding:.2em .6em;border-radius:4px;background-color:#e8e8e8;color:#525252}.epistola-admin .badge{font-size:.85em;padding:.35em .65em}.epistola-admin code{font-size:.9em;color:#525252}.epistola-admin table th{font-weight:600;white-space:nowrap}.epistola-admin .status-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:.5rem;flex-shrink:0;background-color:#adb5bd}.epistola-admin .status-dot--ok{background-color:#198754}.epistola-admin .status-dot--error{background-color:#dc3545}.epistola-admin .card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.epistola-admin .config-card{border:1px solid #dee2e6;border-radius:8px;padding:1.25rem;cursor:pointer;transition:box-shadow .15s ease,border-color .15s ease;background:#fff}.epistola-admin .config-card:hover{box-shadow:0 2px 8px #0000001a}.epistola-admin .config-card--ok{border-left:4px solid #198754}.epistola-admin .config-card--warning{border-left:4px solid #ffc107}.epistola-admin .config-card--error{border-left:4px solid #dc3545}.epistola-admin .config-card__header{display:flex;align-items:center;margin-bottom:1rem}.epistola-admin .config-card__title{margin:0;font-size:1.05rem;font-weight:600;color:#161616}.epistola-admin .config-card__body{display:flex;flex-direction:column;gap:.5rem}.epistola-admin .config-card__field{display:flex;justify-content:space-between;align-items:center}.epistola-admin .config-card__label{font-size:.875rem;color:#6c757d}.epistola-admin .config-card__value{font-size:.875rem;color:#161616}.epistola-admin .config-card__footer{margin-top:1rem;padding-top:.75rem;border-top:1px solid #f0f0f0;text-align:right}.epistola-admin .config-card__latency{font-size:.8rem;color:#adb5bd}.epistola-admin .usage-link{color:#0f62fe;text-decoration:none}.epistola-admin .usage-link:hover{text-decoration:underline}.epistola-admin .detail-info-table{max-width:500px}.epistola-admin .detail-info-table th{width:140px}.epistola-admin .detail-summary{padding:1rem 0;border-bottom:1px solid #dee2e6}.epistola-admin .epistola-changelog{max-height:60vh;overflow:auto;padding:.5rem 1rem;border:1px solid #dee2e6;border-radius:4px}.epistola-admin .epistola-changelog .changelog-release{padding:.75rem 0}.epistola-admin .epistola-changelog .changelog-release+.changelog-release{border-top:1px solid #eee}.epistola-admin .epistola-changelog .changelog-release__header{display:flex;align-items:center;margin-bottom:.5rem}.epistola-admin .epistola-changelog .changelog-section{margin:.25rem 0 .75rem}.epistola-admin .epistola-changelog .changelog-section__title{font-size:.8125rem;font-weight:600;text-transform:uppercase;letter-spacing:.02em;color:#6f6f6f;margin-bottom:.25rem}.epistola-admin .epistola-changelog .changelog-section__items{margin:0;padding-left:1.25rem;font-size:.8125rem;line-height:1.5}.epistola-admin .epistola-changelog .changelog-section__items li{margin-bottom:.25rem;word-break:break-word}\n"] }]
3485
- }], ctorParameters: () => [{ type: EpistolaAdminService }, { type: i2$5.ActivatedRoute }, { type: i2$5.Router }] });
4508
+ args: [{ selector: 'epistola-admin-page', standalone: true, imports: [CommonModule, RouterModule, PluginTranslatePipeModule, TabsModule, TagModule], template: "<div class=\"epistola-admin\">\n <!-- Overview: tabs (no configuration selected) -->\n <ng-container *ngIf=\"!selectedCard\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"d-flex align-items-center\">\n <h5 class=\"mb-0\">{{ 'epistolaAdminOverview' | pluginTranslate: 'epistola' | async }}</h5>\n <span *ngIf=\"pluginVersion\" class=\"version-badge ms-2\">v{{ pluginVersion }}</span>\n </div>\n <button class=\"btn btn-outline-primary btn-sm\" (click)=\"refresh()\" [disabled]=\"loading\">\n {{ 'epistolaAdminRefresh' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <ng-template #configurationsHeading>\n {{ 'epistolaAdminConfigurations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ cards.length }}</cds-tag>\n </ng-template>\n\n <ng-template #validationsHeading>\n {{ 'epistolaAdminValidations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" [type]=\"validationViolations.length > 0 ? 'red' : 'gray'\" class=\"ms-1\">\n {{ validationViolations.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #changelogHeading>\n {{ 'epistolaAdminChangelog' | pluginTranslate: 'epistola' | async }}\n </ng-template>\n\n <!-- TEMPORARY (removed in 1.0.0): forms missing the task-id carrier -->\n <ng-template #formsHeading>\n {{ 'epistolaAdminForms' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n *ngIf=\"formsScanLoaded\"\n size=\"sm\"\n [type]=\"formsAttentionCount > 0 ? 'red' : 'gray'\"\n class=\"ms-1\"\n >{{ formsAttentionCount }}</cds-tag\n >\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 class=\"text-muted small mt-3\">\n <div>\n {{ 'epistolaAdminValidationLastChecked' | pluginTranslate: 'epistola' | async }}:\n <ng-container *ngIf=\"validationReport?.lastCheckedAt; else notYetRun\">\n {{ validationReport?.lastCheckedAt | date: 'medium' }}\n </ng-container>\n <ng-template #notYetRun>\n {{ 'epistolaAdminValidationNotYetRun' | pluginTranslate: 'epistola' | async }}\n </ng-template>\n \u00B7 {{ 'epistolaAdminValidationAutoRefresh' | pluginTranslate: 'epistola' | async }}\n {{ refreshIntervalMinutes }} min.\n </div>\n <div>\n {{ 'epistolaAdminValidationLatestVersionNote' | pluginTranslate: 'epistola' | async }}\n </div>\n </div>\n\n <div *ngIf=\"validationViolations.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoValidations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"validationViolations.length > 0\" class=\"mt-3\">\n <p class=\"text-muted mb-3\">\n {{ 'epistolaAdminValidationWarningBody' | pluginTranslate: 'epistola' | async }}\n </p>\n <table class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminValidationCode' | pluginTranslate: 'epistola' | async }}</th>\n <th>\n {{ 'epistolaAdminValidationMessage' | pluginTranslate: 'epistola' | async }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let v of validationViolations\">\n <td>\n {{ v.processDefinitionName || v.processDefinitionKey }}\n <div *ngIf=\"v.processDefinitionName\" class=\"text-muted small\">\n <code>{{ v.processDefinitionKey }}</code>\n </div>\n </td>\n <td>\n <code>{{ v.activityId }}</code>\n </td>\n <td>\n <cds-tag size=\"sm\" type=\"red\">{{ v.code }}</cds-tag>\n </td>\n <td>{{ v.message }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"changelogHeading\"\n [active]=\"overviewTab === 'changelog'\"\n (selected)=\"setOverviewTab('changelog')\"\n >\n <div class=\"d-flex align-items-center mt-3 mb-3\">\n <span class=\"text-muted me-2\">{{\n 'epistolaAdminRunningVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">v{{ pluginVersion || '\u2014' }}</cds-tag>\n </div>\n\n <div *ngIf=\"changelogLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoChangelog' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length > 0\"\n class=\"epistola-changelog\"\n >\n <section *ngFor=\"let release of changelog\" class=\"changelog-release\">\n <div class=\"changelog-release__header\">\n <cds-tag size=\"sm\" type=\"purple\">{{ release.version }}</cds-tag>\n <span *ngIf=\"release.date\" class=\"text-muted ms-2\">{{ release.date }}</span>\n </div>\n <div *ngFor=\"let section of release.sections\" class=\"changelog-section\">\n <h6 class=\"changelog-section__title\">{{ section.title }}</h6>\n <ul class=\"changelog-section__items\">\n <li *ngFor=\"let item of section.items\">{{ item }}</li>\n </ul>\n </div>\n </section>\n </div>\n </cds-tab>\n\n <!-- TEMPORARY (removed in 1.0.0): forms missing the task-id carrier -->\n <cds-tab\n [heading]=\"formsHeading\"\n [active]=\"overviewTab === 'forms'\"\n (selected)=\"setOverviewTab('forms')\"\n >\n <p class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminFormsIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"formIssuesLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!formIssuesLoading && formIssues && formIssues.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoFormIssues' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!formIssuesLoading && formIssues && formIssues.length > 0\">\n <div class=\"mb-2\">\n <button\n class=\"btn btn-sm btn-primary\"\n (click)=\"repairAllForms()\"\n [disabled]=\"repairingAll\"\n >\n {{\n (repairingAll ? 'epistolaAdminRepairing' : 'epistolaAdminRepairAll')\n | pluginTranslate: 'epistola'\n | async\n }}\n </button>\n <span\n *ngIf=\"formFeedback && formFeedback.formId === 'all'\"\n class=\"small ms-2\"\n [class.text-success]=\"formFeedback.type === 'success'\"\n [class.text-danger]=\"formFeedback.type === 'error'\"\n >{{ formFeedback.message }}</span\n >\n </div>\n\n <table class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminFormName' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminFormMissing' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let issue of formIssues\">\n <td>\n <code>{{ issue.name }}</code>\n <cds-tag\n *ngIf=\"issue.readOnly\"\n size=\"sm\"\n type=\"warm-gray\"\n class=\"ms-1\"\n [title]=\"'epistolaAdminFormReadOnlyHint' | pluginTranslate: 'epistola' | async\"\n >{{\n 'epistolaAdminFormReadOnly' | pluginTranslate: 'epistola' | async\n }}</cds-tag\n >\n </td>\n <td>{{ issue.missingComponents }}</td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"repairForm(issue)\"\n [disabled]=\"isRepairingForm(issue)\"\n [title]=\"'epistolaAdminRepairTooltip' | pluginTranslate: 'epistola' | async\"\n >\n {{\n (isRepairingForm(issue) ? 'epistolaAdminRepairing' : 'epistolaAdminRepair')\n | pluginTranslate: 'epistola'\n | async\n }}\n </button>\n <div\n *ngIf=\"formFeedback && formFeedback.formId === issue.formId\"\n class=\"small mt-1\"\n [class.text-success]=\"formFeedback.type === 'success'\"\n [class.text-danger]=\"formFeedback.type === 'error'\"\n >\n {{ formFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- TEMPORARY: forms still using the legacy override-mapping object format -->\n <hr class=\"my-4\" />\n <h5 class=\"mb-1\">\n {{ 'epistolaAdminLegacyOverrideTitle' | pluginTranslate: 'epistola' | async }}\n </h5>\n <p class=\"text-muted mb-3\">\n {{ 'epistolaAdminLegacyOverrideIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"legacyOverrideLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!legacyOverrideLoading && legacyOverrideForms && legacyOverrideForms.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoLegacyOverride' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table\n *ngIf=\"!legacyOverrideLoading && legacyOverrideForms && legacyOverrideForms.length > 0\"\n class=\"table table-striped\"\n >\n <thead>\n <tr>\n <th>{{ 'epistolaAdminFormName' | pluginTranslate: 'epistola' | async }}</th>\n <th>\n {{ 'epistolaAdminLegacyOverrideComponents' | pluginTranslate: 'epistola' | async }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let form of legacyOverrideForms\">\n <td>\n <code>{{ form.name }}</code>\n <cds-tag\n *ngIf=\"form.readOnly\"\n size=\"sm\"\n type=\"warm-gray\"\n class=\"ms-1\"\n [title]=\"'epistolaAdminFormReadOnlyHint' | pluginTranslate: 'epistola' | async\"\n >{{ 'epistolaAdminFormReadOnly' | pluginTranslate: 'epistola' | async }}</cds-tag\n >\n </td>\n <td>{{ form.legacyComponents }}</td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n\n <!-- Detail view: selected configuration -->\n <ng-container *ngIf=\"selectedCard\">\n <div class=\"detail-header mb-3\">\n <button class=\"btn btn-link btn-sm p-0\" (click)=\"backToOverview()\">\n &larr; {{ 'epistolaAdminBackToOverview' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div class=\"detail-summary mb-4\">\n <h4>\n <span\n class=\"status-dot me-2\"\n [class.status-dot--ok]=\"selectedCard.reachable\"\n [class.status-dot--error]=\"!selectedCard.reachable\"\n >\n </span>\n {{ selectedCard.configurationTitle }}\n </h4>\n\n <table class=\"table table-sm detail-info-table\">\n <tbody>\n <tr>\n <th>{{ 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <code>{{ selectedCard.tenantId }}</code>\n </td>\n </tr>\n <tr>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <cds-tag size=\"sm\" [type]=\"selectedCard.reachable ? 'green' : 'red'\">\n {{\n selectedCard.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n <span class=\"text-muted ms-2\">{{ selectedCard.latencyMs }} ms</span>\n </td>\n </tr>\n <tr *ngIf=\"selectedCard.serverVersion\">\n <th>{{ 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async }}</th>\n <td>{{ selectedCard.serverVersion }}</td>\n </tr>\n <tr *ngIf=\"selectedCard.errorMessage\">\n <th>{{ 'epistolaAdminError' | pluginTranslate: 'epistola' | async }}</th>\n <td class=\"text-danger\">{{ selectedCard.errorMessage }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Tabs -->\n <ng-template #actionsHeading>\n {{ 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ selectedCard.usageEntries.length }}</cds-tag>\n </ng-template>\n\n <ng-template #pendingHeading>\n {{ 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n size=\"sm\"\n [type]=\"selectedCard.pendingJobs.length > 0 ? 'blue' : 'gray'\"\n class=\"ms-1\"\n >\n {{ selectedCard.pendingJobs.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #catalogsHeading>\n {{ 'epistolaAdminCatalogs' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ catalogs.length }}</cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"actionsHeading\"\n [active]=\"activeTab === 'actions'\"\n (selected)=\"setActiveTab('actions')\"\n >\n <div *ngIf=\"selectedCard.usageEntries.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoUsageForConfig' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.usageEntries.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCase' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminAction' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let entry of selectedCard.usageEntries\"\n [class.table-warning]=\"entry.problems.length > 0\"\n >\n <td>{{ entry.caseDefinitionKey || '-' }}</td>\n <td>\n <a\n *ngIf=\"entry.caseDefinitionKey && entry.caseDefinitionVersionTag\"\n [routerLink]=\"[\n '/case-management',\n 'case',\n entry.caseDefinitionKey,\n 'version',\n entry.caseDefinitionVersionTag,\n 'processes',\n entry.processDefinitionKey,\n ]\"\n class=\"usage-link\"\n >\n {{ entry.processDefinitionName }}\n </a>\n <span *ngIf=\"!entry.caseDefinitionKey || !entry.caseDefinitionVersionTag\">\n {{ entry.processDefinitionName }}\n </span>\n </td>\n <td>{{ entry.activityName }}</td>\n <td>\n <code>{{ entry.actionKey }}</code>\n </td>\n <td>\n <cds-tag *ngIf=\"entry.problems.length === 0\" size=\"sm\" type=\"green\">OK</cds-tag>\n <cds-tag\n *ngFor=\"let problem of entry.problems\"\n size=\"sm\"\n type=\"red\"\n class=\"d-block mb-1\"\n >\n {{ problem }}\n </cds-tag>\n </td>\n <td>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"exportProcessLink(entry)\"\n [title]=\"'epistolaAdminExport' | pluginTranslate: 'epistola' | async\"\n >\n &#x2913;\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"pendingHeading\"\n [active]=\"activeTab === 'pending'\"\n (selected)=\"setActiveTab('pending')\"\n >\n <div *ngIf=\"selectedCard.pendingJobs.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoPendingJobs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.pendingJobs.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminStatus' | 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 <cds-tag\n *ngIf=\"job.status === 'UNWIRED'; else waitingTag\"\n size=\"sm\"\n type=\"red\"\n [title]=\"'epistolaAdminUnwiredTooltip' | pluginTranslate: 'epistola' | async\"\n >\n {{ 'epistolaAdminStatusUnwired' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <ng-template #waitingTag>\n <cds-tag size=\"sm\" type=\"blue\">\n {{ 'epistolaAdminStatusWaiting' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n </ng-template>\n </td>\n <td>\n <code *ngIf=\"job.status !== 'UNWIRED'\">{{ job.requestId }}</code>\n <span *ngIf=\"job.status === 'UNWIRED'\" class=\"text-muted\">\u2014</span>\n </td>\n <td class=\"text-end\">\n <button\n *ngIf=\"job.status !== 'UNWIRED'\"\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 <span\n *ngIf=\"job.status === 'UNWIRED'\"\n class=\"text-muted small\"\n [title]=\"'epistolaAdminUnwiredTooltip' | pluginTranslate: 'epistola' | async\"\n >\n {{ 'epistolaAdminUnwiredHint' | pluginTranslate: 'epistola' | async }}\n </span>\n <div\n *ngIf=\"reconcileFeedback && reconcileFeedback.executionId === job.executionId\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"reconcileFeedback.type === 'success'\"\n [class.text-warning]=\"reconcileFeedback.type === 'pending'\"\n [class.text-danger]=\"reconcileFeedback.type === 'error'\"\n >\n {{ reconcileFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"catalogsHeading\"\n [active]=\"activeTab === 'catalogs'\"\n (selected)=\"setActiveTab('catalogs')\"\n >\n <p class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminCatalogsIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"catalogsLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!catalogsLoading && catalogs.length === 0\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminNoCatalogs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"!catalogsLoading && catalogs.length > 0\" class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCatalogSlug' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogVersion' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogStatus' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let catalog of catalogs\">\n <td>\n <code>{{ catalog.slug }}</code>\n </td>\n <td>{{ catalog.version }}</td>\n <td>\n <cds-tag *ngIf=\"catalog.status === 'IN_EPISTOLA'\" size=\"sm\" type=\"green\">\n {{ 'epistolaAdminCatalogInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'NOT_IN_EPISTOLA'\" size=\"sm\" type=\"red\">\n {{ 'epistolaAdminCatalogNotInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'UNKNOWN'\" size=\"sm\" type=\"gray\">\n {{ 'epistolaAdminCatalogStatusUnknown' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n (click)=\"redeployCatalog(catalog)\"\n [disabled]=\"isRedeploying(catalog)\"\n [title]=\"'epistolaAdminRedeployTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploy' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploying' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"catalogFeedback && catalogFeedback.slug === catalog.slug\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"catalogFeedback.type === 'success'\"\n [class.text-danger]=\"catalogFeedback.type === 'error'\"\n >\n {{ catalogFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n</div>\n", styles: [".epistola-admin{padding:1.5rem}.epistola-admin .version-badge{font-size:.75rem;font-weight:500;padding:.2em .6em;border-radius:4px;background-color:#e8e8e8;color:#525252}.epistola-admin .badge{font-size:.85em;padding:.35em .65em}.epistola-admin code{font-size:.9em;color:#525252}.epistola-admin table th{font-weight:600;white-space:nowrap}.epistola-admin .status-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:.5rem;flex-shrink:0;background-color:#adb5bd}.epistola-admin .status-dot--ok{background-color:#198754}.epistola-admin .status-dot--error{background-color:#dc3545}.epistola-admin .card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.epistola-admin .config-card{border:1px solid #dee2e6;border-radius:8px;padding:1.25rem;cursor:pointer;transition:box-shadow .15s ease,border-color .15s ease;background:#fff}.epistola-admin .config-card:hover{box-shadow:0 2px 8px #0000001a}.epistola-admin .config-card--ok{border-left:4px solid #198754}.epistola-admin .config-card--warning{border-left:4px solid #ffc107}.epistola-admin .config-card--error{border-left:4px solid #dc3545}.epistola-admin .config-card__header{display:flex;align-items:center;margin-bottom:1rem}.epistola-admin .config-card__title{margin:0;font-size:1.05rem;font-weight:600;color:#161616}.epistola-admin .config-card__body{display:flex;flex-direction:column;gap:.5rem}.epistola-admin .config-card__field{display:flex;justify-content:space-between;align-items:center}.epistola-admin .config-card__label{font-size:.875rem;color:#6c757d}.epistola-admin .config-card__value{font-size:.875rem;color:#161616}.epistola-admin .config-card__footer{margin-top:1rem;padding-top:.75rem;border-top:1px solid #f0f0f0;text-align:right}.epistola-admin .config-card__latency{font-size:.8rem;color:#adb5bd}.epistola-admin .usage-link{color:#0f62fe;text-decoration:none}.epistola-admin .usage-link:hover{text-decoration:underline}.epistola-admin .detail-info-table{max-width:500px}.epistola-admin .detail-info-table th{width:140px}.epistola-admin .detail-summary{padding:1rem 0;border-bottom:1px solid #dee2e6}.epistola-admin .epistola-changelog{max-height:60vh;overflow:auto;padding:.5rem 1rem;border:1px solid #dee2e6;border-radius:4px}.epistola-admin .epistola-changelog .changelog-release{padding:.75rem 0}.epistola-admin .epistola-changelog .changelog-release+.changelog-release{border-top:1px solid #eee}.epistola-admin .epistola-changelog .changelog-release__header{display:flex;align-items:center;margin-bottom:.5rem}.epistola-admin .epistola-changelog .changelog-section{margin:.25rem 0 .75rem}.epistola-admin .epistola-changelog .changelog-section__title{font-size:.8125rem;font-weight:600;text-transform:uppercase;letter-spacing:.02em;color:#6f6f6f;margin-bottom:.25rem}.epistola-admin .epistola-changelog .changelog-section__items{margin:0;padding-left:1.25rem;font-size:.8125rem;line-height:1.5}.epistola-admin .epistola-changelog .changelog-section__items li{margin-bottom:.25rem;word-break:break-word}\n"] }]
4509
+ }], ctorParameters: () => [{ type: EpistolaAdminService }, { type: i2$4.ActivatedRoute }, { type: i2$4.Router }] });
3486
4510
 
4511
+ /*
4512
+ * Copyright 2025 Epistola.
4513
+ *
4514
+ * Licensed under EUPL, Version 1.2 (the "License");
4515
+ * you may not use this file except in compliance with the License.
4516
+ * You may obtain a copy of the License at
4517
+ *
4518
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
4519
+ *
4520
+ * Unless required by applicable law or agreed to in writing, software
4521
+ * distributed under the License is distributed on an "AS IS" basis,
4522
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4523
+ * See the License for the specific language governing permissions and
4524
+ * limitations under the License.
4525
+ *
4526
+ * SPDX-License-Identifier: EUPL-1.2
4527
+ */
3487
4528
  function isRuntimeWindow(value) {
3488
4529
  return typeof value === 'object' && value !== null;
3489
4530
  }
@@ -3513,12 +4554,46 @@ function isEpistolaEnabled() {
3513
4554
  return flag !== false && flag !== 'false';
3514
4555
  }
3515
4556
 
4557
+ /*
4558
+ * Copyright 2025 Epistola.
4559
+ *
4560
+ * Licensed under EUPL, Version 1.2 (the "License");
4561
+ * you may not use this file except in compliance with the License.
4562
+ * You may obtain a copy of the License at
4563
+ *
4564
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
4565
+ *
4566
+ * Unless required by applicable law or agreed to in writing, software
4567
+ * distributed under the License is distributed on an "AS IS" basis,
4568
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4569
+ * See the License for the specific language governing permissions and
4570
+ * limitations under the License.
4571
+ *
4572
+ * SPDX-License-Identifier: EUPL-1.2
4573
+ */
3516
4574
  const epistolaEnabledGuard = () => {
3517
4575
  if (isEpistolaEnabled())
3518
4576
  return true;
3519
4577
  return inject(Router).parseUrl('/');
3520
4578
  };
3521
4579
 
4580
+ /*
4581
+ * Copyright 2025 Epistola.
4582
+ *
4583
+ * Licensed under EUPL, Version 1.2 (the "License");
4584
+ * you may not use this file except in compliance with the License.
4585
+ * You may obtain a copy of the License at
4586
+ *
4587
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
4588
+ *
4589
+ * Unless required by applicable law or agreed to in writing, software
4590
+ * distributed under the License is distributed on an "AS IS" basis,
4591
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4592
+ * See the License for the specific language governing permissions and
4593
+ * limitations under the License.
4594
+ *
4595
+ * SPDX-License-Identifier: EUPL-1.2
4596
+ */
3522
4597
  const routes = [
3523
4598
  {
3524
4599
  path: 'epistola',
@@ -3529,7 +4604,7 @@ const routes = [
3529
4604
  ];
3530
4605
  class EpistolaAdminRoutingModule {
3531
4606
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
3532
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, imports: [i2$5.RouterModule], exports: [RouterModule] });
4607
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, imports: [i2$4.RouterModule], exports: [RouterModule] });
3533
4608
  static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, imports: [RouterModule.forChild(routes), RouterModule] });
3534
4609
  }
3535
4610
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, decorators: [{
@@ -3540,6 +4615,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3540
4615
  }]
3541
4616
  }] });
3542
4617
 
4618
+ /*
4619
+ * Copyright 2025 Epistola.
4620
+ *
4621
+ * Licensed under EUPL, Version 1.2 (the "License");
4622
+ * you may not use this file except in compliance with the License.
4623
+ * You may obtain a copy of the License at
4624
+ *
4625
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
4626
+ *
4627
+ * Unless required by applicable law or agreed to in writing, software
4628
+ * distributed under the License is distributed on an "AS IS" basis,
4629
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4630
+ * See the License for the specific language governing permissions and
4631
+ * limitations under the License.
4632
+ *
4633
+ * SPDX-License-Identifier: EUPL-1.2
4634
+ */
3543
4635
  const EPISTOLA_DOCUMENT_OPTIONS = {
3544
4636
  type: 'epistola-document',
3545
4637
  selector: 'epistola-document-element',
@@ -3547,15 +4639,146 @@ const EPISTOLA_DOCUMENT_OPTIONS = {
3547
4639
  group: 'basic',
3548
4640
  icon: 'file-pdf-o',
3549
4641
  emptyValue: null,
3550
- fieldOptions: ['label', 'display', 'documentVariable', 'tenantIdVariable', 'filename'],
4642
+ // tenantIdVariable is intentionally absent: it is not author-configurable in the builder
4643
+ // (the tenant is process-wide, always the default), and Valtimo copies every fieldOptions
4644
+ // key onto the element unconditionally — listing it would overwrite the component's
4645
+ // `epistolaTenantId` @Input() default with `undefined` and break the download.
4646
+ fieldOptions: ['label', 'display', 'documentVariable', 'filename'],
4647
+ // Embed the hidden task-id carrier so dropping this component is enough — no separate
4648
+ // field for the author to add. Valtimo prefills it server-side via the epistola: resolver.
4649
+ schema: { components: [PREFILLED_TASK_ID_CARRIER] },
4650
+ // Minimal edit form: only the five properties this component actually reads (its
4651
+ // @Input()s / fieldOptions). A flat `components` array (no `tabs` wrapper) replaces the
4652
+ // inherited stock text-field dialog (Display/Data/Validation/API/Conditional/Logic/Layout)
4653
+ // entirely. Keys must match the @Input() names verbatim so the fieldOptions copy works.
4654
+ editForm: () => ({
4655
+ components: [
4656
+ {
4657
+ type: 'textfield',
4658
+ key: 'label',
4659
+ label: 'Label',
4660
+ weight: 10,
4661
+ defaultValue: 'Document',
4662
+ },
4663
+ {
4664
+ type: 'select',
4665
+ key: 'display',
4666
+ label: 'Display',
4667
+ weight: 20,
4668
+ defaultValue: 'both',
4669
+ dataSrc: 'values',
4670
+ data: {
4671
+ values: [
4672
+ { label: 'Inline preview', value: 'inline' },
4673
+ { label: 'Download button', value: 'button' },
4674
+ { label: 'Both', value: 'both' },
4675
+ ],
4676
+ },
4677
+ },
4678
+ {
4679
+ type: 'textfield',
4680
+ key: 'documentVariable',
4681
+ label: 'Document variable',
4682
+ weight: 30,
4683
+ defaultValue: 'epistolaResult',
4684
+ tooltip: 'Name of the process variable holding the Epistola result (PDF id).',
4685
+ },
4686
+ {
4687
+ type: 'textfield',
4688
+ key: 'filename',
4689
+ label: 'Filename',
4690
+ weight: 40,
4691
+ defaultValue: 'document.pdf',
4692
+ tooltip: 'Filename used for the download (Content-Disposition).',
4693
+ },
4694
+ ],
4695
+ }),
3551
4696
  };
3552
4697
  function registerEpistolaDocumentComponent(injector) {
3553
4698
  if (customElements.get(EPISTOLA_DOCUMENT_OPTIONS.selector)) {
3554
4699
  return;
3555
4700
  }
3556
4701
  registerCustomFormioComponent(EPISTOLA_DOCUMENT_OPTIONS, EpistolaDocumentComponent, injector);
4702
+ // Extend the base class to forward the server-prefilled task id (epistola: value
4703
+ // resolver) to the Angular element, so the download authorizes against the exact task in
4704
+ // every Valtimo task-open flow.
4705
+ const Formio = window.Formio;
4706
+ const BaseComponent = Formio?.Components?.components?.[EPISTOLA_DOCUMENT_OPTIONS.type];
4707
+ if (!BaseComponent) {
4708
+ return;
4709
+ }
4710
+ class EpistolaDocumentWithTaskContext extends BaseComponent {
4711
+ attach(element) {
4712
+ const result = super.attach(element);
4713
+ if (this._customAngularElement) {
4714
+ const prefilledTaskId = readPrefilledTaskId(this.root);
4715
+ if (prefilledTaskId) {
4716
+ this._customAngularElement['taskInstanceId'] = prefilledTaskId;
4717
+ }
4718
+ }
4719
+ return result;
4720
+ }
4721
+ }
4722
+ Formio.Components.setComponent(EPISTOLA_DOCUMENT_OPTIONS.type, EpistolaDocumentWithTaskContext);
3557
4723
  }
3558
4724
 
4725
+ /*
4726
+ * Copyright 2025 Epistola.
4727
+ *
4728
+ * Licensed under EUPL, Version 1.2 (the "License");
4729
+ * you may not use this file except in compliance with the License.
4730
+ * You may obtain a copy of the License at
4731
+ *
4732
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
4733
+ *
4734
+ * Unless required by applicable law or agreed to in writing, software
4735
+ * distributed under the License is distributed on an "AS IS" basis,
4736
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4737
+ * See the License for the specific language governing permissions and
4738
+ * limitations under the License.
4739
+ *
4740
+ * SPDX-License-Identifier: EUPL-1.2
4741
+ */
4742
+ /**
4743
+ * Hides a registered custom Formio component from the builder's component palette,
4744
+ * while keeping it fully usable inside other components' `editForm`s and at runtime.
4745
+ *
4746
+ * Formio's `WebformBuilder` only adds a component to the palette when
4747
+ * `component.builderInfo && component.builderInfo.schema` is truthy. Overriding the
4748
+ * registered class's static `builderInfo` getter to `false` therefore removes it from
4749
+ * the palette. Runtime instantiation and editForm usage don't consult `builderInfo`,
4750
+ * so they are unaffected.
4751
+ *
4752
+ * Call this AFTER the component is registered (and after any `setComponent` re-registration),
4753
+ * so it targets the final class in `Formio.Components.components[type]`.
4754
+ */
4755
+ function hideFormioComponentFromBuilder(type) {
4756
+ const registered = window.Formio?.Components?.components?.[type];
4757
+ if (registered) {
4758
+ Object.defineProperty(registered, 'builderInfo', {
4759
+ get: () => false,
4760
+ configurable: true,
4761
+ });
4762
+ }
4763
+ }
4764
+
4765
+ /*
4766
+ * Copyright 2025 Epistola.
4767
+ *
4768
+ * Licensed under EUPL, Version 1.2 (the "License");
4769
+ * you may not use this file except in compliance with the License.
4770
+ * You may obtain a copy of the License at
4771
+ *
4772
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
4773
+ *
4774
+ * Unless required by applicable law or agreed to in writing, software
4775
+ * distributed under the License is distributed on an "AS IS" basis,
4776
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4777
+ * See the License for the specific language governing permissions and
4778
+ * limitations under the License.
4779
+ *
4780
+ * SPDX-License-Identifier: EUPL-1.2
4781
+ */
3559
4782
  const EPISTOLA_RETRY_FORM_OPTIONS = {
3560
4783
  type: 'epistola-retry-form',
3561
4784
  selector: 'epistola-retry-form-element',
@@ -3564,13 +4787,60 @@ const EPISTOLA_RETRY_FORM_OPTIONS = {
3564
4787
  icon: 'refresh',
3565
4788
  emptyValue: null,
3566
4789
  fieldOptions: ['sourceActivityId', 'label'], // sourceActivityId is optional (set via BPMN input parameter)
4790
+ // Embed the hidden task-id carrier so dropping this component is enough — no separate
4791
+ // field for the author to add. Valtimo prefills it server-side via the epistola: resolver.
4792
+ schema: { components: [PREFILLED_TASK_ID_CARRIER] },
3567
4793
  };
3568
4794
  function registerEpistolaRetryFormComponent(injector) {
3569
- if (!customElements.get(EPISTOLA_RETRY_FORM_OPTIONS.selector)) {
3570
- registerCustomFormioComponent(EPISTOLA_RETRY_FORM_OPTIONS, EpistolaRetryFormComponent, injector);
4795
+ if (customElements.get(EPISTOLA_RETRY_FORM_OPTIONS.selector)) {
4796
+ return;
4797
+ }
4798
+ registerCustomFormioComponent(EPISTOLA_RETRY_FORM_OPTIONS, EpistolaRetryFormComponent, injector);
4799
+ // Extend the base class to forward the server-prefilled task id (epistola: value
4800
+ // resolver) to the Angular element, so the retry form authorizes against the exact task in
4801
+ // every Valtimo task-open flow.
4802
+ const Formio = window.Formio;
4803
+ const BaseComponent = Formio?.Components?.components?.[EPISTOLA_RETRY_FORM_OPTIONS.type];
4804
+ if (!BaseComponent) {
4805
+ return;
3571
4806
  }
4807
+ class EpistolaRetryFormWithTaskContext extends BaseComponent {
4808
+ attach(element) {
4809
+ const result = super.attach(element);
4810
+ if (this._customAngularElement) {
4811
+ const prefilledTaskId = readPrefilledTaskId(this.root);
4812
+ if (prefilledTaskId) {
4813
+ this._customAngularElement['taskInstanceId'] = prefilledTaskId;
4814
+ }
4815
+ }
4816
+ return result;
4817
+ }
4818
+ }
4819
+ Formio.Components.setComponent(EPISTOLA_RETRY_FORM_OPTIONS.type, EpistolaRetryFormWithTaskContext);
4820
+ // Part of the plugin's auto-deployed retry form, not a drop-anywhere component — hide it
4821
+ // from the builder palette. It still renders wherever it's already present in a form.
4822
+ hideFormioComponentFromBuilder(EPISTOLA_RETRY_FORM_OPTIONS.type);
3572
4823
  }
3573
4824
 
4825
+ /*
4826
+ * Copyright 2025 Epistola.
4827
+ *
4828
+ * Licensed under EUPL, Version 1.2 (the "License");
4829
+ * you may not use this file except in compliance with the License.
4830
+ * You may obtain a copy of the License at
4831
+ *
4832
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
4833
+ *
4834
+ * Unless required by applicable law or agreed to in writing, software
4835
+ * distributed under the License is distributed on an "AS IS" basis,
4836
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
4837
+ * See the License for the specific language governing permissions and
4838
+ * limitations under the License.
4839
+ *
4840
+ * SPDX-License-Identifier: EUPL-1.2
4841
+ */
4842
+ /** Default debounce for the auto-refresh, in milliseconds. */
4843
+ const DEFAULT_REFRESH_DEBOUNCE_MS = 1500;
3574
4844
  const EPISTOLA_DOCUMENT_PREVIEW_OPTIONS = {
3575
4845
  type: 'epistola-document-preview',
3576
4846
  selector: 'epistola-document-preview-element',
@@ -3579,6 +4849,9 @@ const EPISTOLA_DOCUMENT_PREVIEW_OPTIONS = {
3579
4849
  icon: 'file-pdf-o',
3580
4850
  emptyValue: null,
3581
4851
  fieldOptions: ['label', 'processDefinitionKey', 'sourceActivityId', 'overrideMapping'],
4852
+ // Embed the hidden task-id carrier so dropping this component is enough — no separate
4853
+ // field for the author to add. Valtimo prefills it server-side via the epistola: resolver.
4854
+ schema: { components: [PREFILLED_TASK_ID_CARRIER] },
3582
4855
  editForm: () => ({
3583
4856
  components: [
3584
4857
  {
@@ -3594,6 +4867,23 @@ const EPISTOLA_DOCUMENT_PREVIEW_OPTIONS = {
3594
4867
  label: 'Input Overrides',
3595
4868
  weight: 20,
3596
4869
  },
4870
+ {
4871
+ type: 'checkbox',
4872
+ key: 'autoRefresh',
4873
+ label: 'Auto-refresh preview as the form is filled in',
4874
+ tooltip: 'When on, the preview refreshes automatically while the form is edited — debounced, and only when a field loses focus, not on every keystroke. Turn off to refresh only with the Refresh button.',
4875
+ defaultValue: true,
4876
+ weight: 30,
4877
+ },
4878
+ {
4879
+ type: 'number',
4880
+ key: 'refreshDebounceMs',
4881
+ label: 'Auto-refresh debounce (ms)',
4882
+ tooltip: 'How long to wait after the last change before refreshing. Higher values feel calmer; lower values feel more responsive.',
4883
+ defaultValue: DEFAULT_REFRESH_DEBOUNCE_MS,
4884
+ weight: 40,
4885
+ conditional: { show: true, when: 'autoRefresh', eq: 'true' },
4886
+ },
3597
4887
  ],
3598
4888
  }),
3599
4889
  };
@@ -3614,7 +4904,39 @@ function registerEpistolaDocumentPreviewComponent(injector) {
3614
4904
  class PreviewWithOverrides extends BasePreviewComponent {
3615
4905
  _debounceTimer = null;
3616
4906
  _changeListenerAttached = false;
4907
+ _changeHandler = null;
4908
+ _blurHandler = null;
4909
+ _blurTarget = null;
4910
+ _destroyed = false;
4911
+ _debounceMs = DEFAULT_REFRESH_DEBOUNCE_MS;
4912
+ // Serialized form of the last value pushed, so we skip re-rendering the preview
4913
+ // when a change recomputes to the same overrides (e.g. typing in a field that
4914
+ // isn't part of the mapping). undefined = nothing pushed yet.
4915
+ _lastPushedJson = undefined;
4916
+ // Whether the last compute produced usable overrides. Drives the initial-paint
4917
+ // retry below: it stops once the form data is present.
4918
+ _hasUsableValue = false;
4919
+ // Timers for the initial-paint retry — Valtimo can prefill form data
4920
+ // asynchronously after the component mounts, sometimes without a change event.
4921
+ _initialPaintTimers = [];
4922
+ // Whether auto-refresh (recompute on change/blur) is currently active. Seeded
4923
+ // once from the builder's autoRefresh option, then toggled at runtime by the
4924
+ // end-user via the preview header. Persisted across redraws (only seeded once).
4925
+ _autoRefreshEnabled = true;
4926
+ _autoRefreshInitialized = false;
3617
4927
  attach(element) {
4928
+ // Formio detaches and re-attaches components on every redraw — not only at
4929
+ // teardown — so a re-attach means the component is alive again. Clear the
4930
+ // destroyed flag here; it only stays set when detach() is the final call
4931
+ // (genuine teardown, e.g. task completion), which is what suppresses the
4932
+ // post-submit preview.
4933
+ this._destroyed = false;
4934
+ // Seed the runtime auto-refresh state from the builder option, once. Re-attach
4935
+ // (redraw) must not clobber a choice the end-user made via the header toggle.
4936
+ if (!this._autoRefreshInitialized) {
4937
+ this._autoRefreshEnabled = this.component?.autoRefresh !== false;
4938
+ this._autoRefreshInitialized = true;
4939
+ }
3618
4940
  // Bidirectional sync between processLinkSelection object and separate properties.
3619
4941
  // The editForm uses processLinkSelection (single field), while the component
3620
4942
  // config and Angular inputs use processDefinitionKey + sourceActivityId.
@@ -3634,78 +4956,436 @@ function registerEpistolaDocumentPreviewComponent(injector) {
3634
4956
  this._customAngularElement['processDefinitionKey'] =
3635
4957
  this.component.processDefinitionKey || '';
3636
4958
  this._customAngularElement['sourceActivityId'] = this.component.sourceActivityId || '';
4959
+ // Forward the server-prefilled task id (epistola: value resolver) so the
4960
+ // component authorizes against the exact task in every Valtimo task-open flow.
4961
+ const prefilledTaskId = readPrefilledTaskId(this.root);
4962
+ if (prefilledTaskId) {
4963
+ this._customAngularElement['taskInstanceId'] = prefilledTaskId;
4964
+ }
4965
+ if (this.component?.overrideMapping) {
4966
+ // Let the component's Refresh button force a recompute from the live form
4967
+ // data, so it works before the first change (e.g. on initial load with
4968
+ // pre-filled fields) rather than reading a not-yet-populated value.
4969
+ this._customAngularElement['requestOverrides'] = () => this._computeAndSetOverrides(true);
4970
+ // Reflect the current auto-refresh state to the header toggle (current
4971
+ // state, not the builder default — so a redraw keeps the user's choice),
4972
+ // and let the toggle flip it. Turning it on does an immediate refresh.
4973
+ this._customAngularElement['autoRefresh'] = this._autoRefreshEnabled;
4974
+ this._customAngularElement['setAutoRefresh'] = (enabled) => {
4975
+ this._autoRefreshEnabled = enabled;
4976
+ if (enabled) {
4977
+ this._computeAndSetOverrides(true);
4978
+ }
4979
+ };
4980
+ }
3637
4981
  }
3638
- // Listen to form changes and compute input overrides from the mapping
4982
+ // Compute input overrides from the mapping and wire up the live listeners.
3639
4983
  if (this.root && this.component?.overrideMapping && !this._changeListenerAttached) {
3640
4984
  this._changeListenerAttached = true;
3641
- this.root.on('change', () => {
3642
- this._computeAndSetOverrides();
3643
- });
3644
- // Compute initial value
3645
- this._computeAndSetOverrides();
4985
+ this._debounceMs = this._resolveDebounceMs();
4986
+ // Compute the initial overrides immediately (no debounce) so a pre-filled
4987
+ // form paints its preview without the debounce delay. This runs regardless
4988
+ // of the auto-refresh state, so the preview still shows once on open.
4989
+ this._computeAndSetOverrides(true);
4990
+ // Valtimo can prefill the form data asynchronously after the component
4991
+ // mounts, sometimes without a change event we can hook — so the single
4992
+ // compute above may see empty data. Re-attempt a few times over ~2s until
4993
+ // usable overrides appear, so a pre-filled form previews itself without a
4994
+ // manual edit or Refresh click. Each attempt is skipped once the data is in.
4995
+ this._initialPaintTimers = [400, 1000, 2000].map((ms) => setTimeout(() => {
4996
+ if (!this._hasUsableValue && !this._destroyed) {
4997
+ void this._runCompute();
4998
+ }
4999
+ }, ms));
5000
+ // Always wire the change + blur listeners; the runtime auto-refresh toggle
5001
+ // (_autoRefreshEnabled) gates whether they actually recompute, so the
5002
+ // end-user can switch it on/off live without re-attaching anything.
5003
+ //
5004
+ // Debounced recompute on any form change — collapses bursts of edits and,
5005
+ // together with the dedup, only re-renders when the mapped data changes.
5006
+ this._changeHandler = () => {
5007
+ if (this._autoRefreshEnabled)
5008
+ this._computeAndSetOverrides();
5009
+ };
5010
+ this.root.on('change', this._changeHandler);
5011
+ // Flush immediately when a field loses focus. `focusout` bubbles (unlike
5012
+ // `blur`), so one listener on the form root catches every input — and it
5013
+ // fires on blur rather than on each keystroke, which keeps the refresh from
5014
+ // feeling hectic.
5015
+ const formEl = this.root?.element;
5016
+ if (formEl?.addEventListener) {
5017
+ this._blurHandler = () => {
5018
+ if (this._autoRefreshEnabled)
5019
+ this._computeAndSetOverrides(true);
5020
+ };
5021
+ formEl.addEventListener('focusout', this._blurHandler);
5022
+ this._blurTarget = formEl;
5023
+ }
3646
5024
  }
3647
5025
  return result;
3648
5026
  }
3649
- _computeAndSetOverrides() {
5027
+ // Tear down the change listener and any pending debounce so a preview is never
5028
+ // fired after the form is unmounted (e.g. on task completion). Without this the
5029
+ // 1.5s debounce can outlive submit and POST /preview with reset/incomplete data,
5030
+ // which Epistola rejects with a 400.
5031
+ detach() {
5032
+ this._destroyed = true;
3650
5033
  if (this._debounceTimer) {
3651
5034
  clearTimeout(this._debounceTimer);
5035
+ this._debounceTimer = null;
3652
5036
  }
3653
- this._debounceTimer = setTimeout(() => {
3654
- const mapping = this.component?.overrideMapping;
3655
- const formData = this.root?.data;
3656
- if (mapping && formData) {
3657
- const overrides = computeInputOverrides(mapping, formData);
3658
- if (Object.keys(overrides).length > 0) {
3659
- this.setValue(overrides);
3660
- }
3661
- }
3662
- }, 1500);
5037
+ this._initialPaintTimers.forEach((t) => clearTimeout(t));
5038
+ this._initialPaintTimers = [];
5039
+ this._hasUsableValue = false;
5040
+ if (this._changeHandler && this.root?.off) {
5041
+ this.root.off('change', this._changeHandler);
5042
+ this._changeHandler = null;
5043
+ }
5044
+ if (this._blurHandler && this._blurTarget?.removeEventListener) {
5045
+ this._blurTarget.removeEventListener('focusout', this._blurHandler);
5046
+ this._blurHandler = null;
5047
+ this._blurTarget = null;
5048
+ }
5049
+ this._changeListenerAttached = false;
5050
+ this._lastPushedJson = undefined;
5051
+ return super.detach();
5052
+ }
5053
+ _computeAndSetOverrides(immediate = false) {
5054
+ if (this._debounceTimer) {
5055
+ clearTimeout(this._debounceTimer);
5056
+ }
5057
+ this._debounceTimer = setTimeout(() => void this._runCompute(), immediate ? 0 : this._debounceMs);
5058
+ }
5059
+ // Compute the input overrides from the live form data and push them to the
5060
+ // component (deduped). Separated from the debounce scheduling so the
5061
+ // initial-paint retry can invoke it directly without another timer hop.
5062
+ async _runCompute() {
5063
+ // Skip if the form is being/has been submitted or the component is gone —
5064
+ // those previews would run with incomplete/reset data and 400 from Epistola.
5065
+ if (this._destroyed || this.root?.submitting || this.root?.submitted) {
5066
+ return;
5067
+ }
5068
+ const mapping = this.component?.overrideMapping;
5069
+ const formData = this.root?.data;
5070
+ if (!mapping || !formData) {
5071
+ return;
5072
+ }
5073
+ // computeInputOverrides evaluates a JSONata expression (async). Re-check
5074
+ // the submit/teardown guards after the await — they can flip while the
5075
+ // promise is in flight.
5076
+ const overrides = await computeInputOverrides(mapping, formData);
5077
+ if (this._destroyed || this.root?.submitting || this.root?.submitted) {
5078
+ return;
5079
+ }
5080
+ // Push null when there's nothing usable yet so the component reverts to
5081
+ // its "complete the form" placeholder instead of keeping a stale preview.
5082
+ const next = Object.keys(overrides).length > 0 ? overrides : null;
5083
+ // Track whether the data is in yet (stops the initial-paint retry).
5084
+ this._hasUsableValue = next !== null;
5085
+ // Dedup: only push (and re-render) when the computed overrides actually
5086
+ // changed since the last push. A change/blur that doesn't affect the
5087
+ // mapped data recomputes to the same value and is dropped here.
5088
+ const nextJson = JSON.stringify(next);
5089
+ if (nextJson === this._lastPushedJson) {
5090
+ return;
5091
+ }
5092
+ this._lastPushedJson = nextJson;
5093
+ this._pushOverrides(next);
5094
+ }
5095
+ // Push the computed overrides to the Angular component via a dedicated input.
5096
+ // NOT Formio's setValue: Valtimo's bridge only mirrors `value` to the DOM and
5097
+ // never to Formio's data model, so Formio resets it to emptyValue on the next
5098
+ // redraw — which would cancel the preview. A plain element property is left
5099
+ // untouched by Formio and so sticks.
5100
+ _pushOverrides(value) {
5101
+ if (this._customAngularElement) {
5102
+ this._customAngularElement['inputOverrides'] = value;
5103
+ }
5104
+ }
5105
+ /**
5106
+ * Resolve the configured auto-refresh debounce (ms), falling back to the
5107
+ * default for missing or non-numeric/negative values.
5108
+ */
5109
+ _resolveDebounceMs() {
5110
+ const configured = Number(this.component?.refreshDebounceMs);
5111
+ return Number.isFinite(configured) && configured >= 0
5112
+ ? configured
5113
+ : DEFAULT_REFRESH_DEBOUNCE_MS;
3663
5114
  }
3664
5115
  }
3665
5116
  // Re-register with the extended class
3666
5117
  Formio.Components.setComponent(EPISTOLA_DOCUMENT_PREVIEW_OPTIONS.type, PreviewWithOverrides);
3667
5118
  }
3668
5119
 
3669
- const FORM_REF_PREFIX = 'form:';
5120
+ /*
5121
+ * Copyright 2025 Epistola.
5122
+ *
5123
+ * Licensed under EUPL, Version 1.2 (the "License");
5124
+ * you may not use this file except in compliance with the License.
5125
+ * You may obtain a copy of the License at
5126
+ *
5127
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
5128
+ *
5129
+ * Unless required by applicable law or agreed to in writing, software
5130
+ * distributed under the License is distributed on an "AS IS" basis,
5131
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5132
+ * See the License for the specific language governing permissions and
5133
+ * limitations under the License.
5134
+ *
5135
+ * SPDX-License-Identifier: EUPL-1.2
5136
+ */
5137
+ const jsonata = _jsonata.default || _jsonata;
5138
+ const SCOPES = ['doc', 'pv', 'case'];
5139
+ function isScope(value) {
5140
+ return typeof value === 'string' && SCOPES.includes(value);
5141
+ }
5142
+ /**
5143
+ * Statically extract every `$doc`/`$pv`/`$case` path referenced anywhere in a JSONata
5144
+ * expression. Used to surface — informationally — which inputs a template's data mapping
5145
+ * consumes, so the override-builder author sees what is worth overriding during preview.
5146
+ *
5147
+ * This is a best-effort static read: paths built dynamically (`$lookup`, custom functions,
5148
+ * computed keys) can't be resolved and simply won't appear. Treat the result as suggestions,
5149
+ * never as validation — an empty result (e.g. on a parse error) means "nothing to suggest".
5150
+ *
5151
+ * Generalizes the variable-path primitive in `utils/jsonata-converter.ts` (`classifyValue`)
5152
+ * to recurse over the whole AST rather than only top-level object values.
5153
+ */
5154
+ function extractReferencedPaths(expression) {
5155
+ if (!expression || !expression.trim()) {
5156
+ return [];
5157
+ }
5158
+ let ast;
5159
+ try {
5160
+ ast = jsonata(expression).ast();
5161
+ }
5162
+ catch {
5163
+ return [];
5164
+ }
5165
+ const seen = new Map();
5166
+ walk(ast, seen);
5167
+ return [...seen.values()].sort((a, b) => a.scope.localeCompare(b.scope) || a.path.localeCompare(b.path));
5168
+ }
5169
+ function record(node, seen) {
5170
+ // Two shapes carry a scope reference:
5171
+ // - a bare `variable` node (`$doc`), where the scope is `node.value` and the path is empty;
5172
+ // - a `path` node whose first step is the `$<scope>` variable, followed by the property
5173
+ // names: `$doc.aanvrager.naam` → steps [doc, aanvrager, naam].
5174
+ const scope = node?.type === 'variable' ? node.value : node?.steps?.[0]?.value;
5175
+ if (!isScope(scope)) {
5176
+ return;
5177
+ }
5178
+ const path = (node.steps ?? [])
5179
+ .slice(1)
5180
+ .map((step) => step?.value)
5181
+ .filter((segment) => typeof segment === 'string')
5182
+ .join('.');
5183
+ const key = `${scope}.${path}`;
5184
+ if (!seen.has(key)) {
5185
+ seen.set(key, { scope, path });
5186
+ }
5187
+ }
5188
+ /** Recursively walk every child node, recording any `$doc`/`$pv`/`$case` path reference. */
5189
+ function walk(node, seen) {
5190
+ if (!node || typeof node !== 'object') {
5191
+ return;
5192
+ }
5193
+ // A bare scope variable with no property access, e.g. `$spread($doc)`.
5194
+ if (node.type === 'variable') {
5195
+ if (isScope(node.value)) {
5196
+ record(node, seen);
5197
+ }
5198
+ return;
5199
+ }
5200
+ // A path rooted at a scope variable, e.g. `$doc.aanvrager.naam`. Record the whole path,
5201
+ // then walk only the filters/predicates attached to its steps — not the variable/name
5202
+ // steps themselves, which would otherwise re-record the leading scope with an empty path.
5203
+ if (node.type === 'path' &&
5204
+ node.steps?.[0]?.type === 'variable' &&
5205
+ isScope(node.steps[0].value)) {
5206
+ record(node, seen);
5207
+ for (const step of node.steps) {
5208
+ walkValue(step?.predicate, seen);
5209
+ walkValue(step?.stages, seen);
5210
+ walkValue(step?.group, seen);
5211
+ }
5212
+ return;
5213
+ }
5214
+ // Generic recursion into every structural child the dashjoin AST uses. Object literals
5215
+ // carry their entries as `lhs` = array of [keyNode, valueNode] pairs.
5216
+ for (const child of [
5217
+ node.lhs,
5218
+ node.rhs,
5219
+ node.condition,
5220
+ node.then,
5221
+ node.else,
5222
+ node.procedure,
5223
+ node.group,
5224
+ node.pattern,
5225
+ node.update,
5226
+ node.delete,
5227
+ ]) {
5228
+ walkValue(child, seen);
5229
+ }
5230
+ for (const list of [
5231
+ node.steps,
5232
+ node.arguments,
5233
+ node.stages,
5234
+ node.expressions,
5235
+ node.terms,
5236
+ node.predicate,
5237
+ ]) {
5238
+ walkValue(list, seen);
5239
+ }
5240
+ }
5241
+ /** Walk a value that may be a node, an array of nodes, or an array of [key, value] pairs. */
5242
+ function walkValue(value, seen) {
5243
+ if (Array.isArray(value)) {
5244
+ for (const item of value) {
5245
+ walkValue(item, seen);
5246
+ }
5247
+ }
5248
+ else {
5249
+ walk(value, seen);
5250
+ }
5251
+ }
5252
+
5253
+ /*
5254
+ * Copyright 2025 Epistola.
5255
+ *
5256
+ * Licensed under EUPL, Version 1.2 (the "License");
5257
+ * you may not use this file except in compliance with the License.
5258
+ * You may obtain a copy of the License at
5259
+ *
5260
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
5261
+ *
5262
+ * Unless required by applicable law or agreed to in writing, software
5263
+ * distributed under the License is distributed on an "AS IS" basis,
5264
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5265
+ * See the License for the specific language governing permissions and
5266
+ * limitations under the License.
5267
+ *
5268
+ * SPDX-License-Identifier: EUPL-1.2
5269
+ */
3670
5270
  class EpistolaOverrideBuilderComponent {
3671
5271
  cdr;
5272
+ pluginService;
3672
5273
  value;
3673
5274
  valueChange = new EventEmitter();
3674
5275
  disabled = false;
3675
5276
  label = 'Input Overrides';
3676
5277
  availableFields = [];
5278
+ /**
5279
+ * Identify the selected generate-document process link, forwarded from the
5280
+ * preview component's editForm. Used to fetch the link's data mapping and
5281
+ * surface which `$doc`/`$pv` paths it consumes — purely informational guidance.
5282
+ */
5283
+ processDefinitionKey = '';
5284
+ sourceActivityId = '';
3677
5285
  rows = [];
3678
5286
  advancedMode = false;
3679
- jsonText = '';
3680
- jsonError = null;
5287
+ /** True when the current expression can't be represented by the simple table. */
5288
+ simpleUnavailable = false;
5289
+ expression = '';
5290
+ /** `$doc`/`$pv`/`$case` paths the selected template's data mapping references. */
5291
+ referencedPaths = [];
5292
+ exampleExpression = '{ "doc": { "naam": $form.voornaam & \' \' & $form.achternaam } }';
3681
5293
  initialized = false;
3682
- constructor(cdr) {
5294
+ destroy$ = new Subject();
5295
+ /** Link last fetched, so we refetch only when the selected process link changes. */
5296
+ lastFetchedLinkKey = null;
5297
+ constructor(cdr, pluginService) {
3683
5298
  this.cdr = cdr;
5299
+ this.pluginService = pluginService;
5300
+ }
5301
+ get formFieldKeys() {
5302
+ return this.availableFields.map((f) => f.key);
3684
5303
  }
3685
- ngOnChanges() {
3686
- if (!this.initialized && this.value) {
5304
+ get hasReferencedPaths() {
5305
+ return this.referencedPaths.length > 0;
5306
+ }
5307
+ /** Referenced paths for a scope, excluding whole-scope refs (empty path) that aren't completions. */
5308
+ referencedPathsForScope(scope) {
5309
+ return this.referencedPaths.filter((p) => p.scope === scope && p.path).map((p) => p.path);
5310
+ }
5311
+ /** Autocomplete context for the advanced editor: form fields plus the mapping's referenced paths. */
5312
+ get editorContextVariables() {
5313
+ return {
5314
+ form: this.formFieldKeys,
5315
+ doc: this.referencedPathsForScope('doc'),
5316
+ pv: this.referencedPathsForScope('pv'),
5317
+ case: this.referencedPathsForScope('case'),
5318
+ };
5319
+ }
5320
+ /** Render a referenced path as a `$scope.path` reference (or `$scope` for a whole-scope ref). */
5321
+ formatReferencedPath(ref) {
5322
+ return ref.path ? `$${ref.scope}.${ref.path}` : `$${ref.scope}`;
5323
+ }
5324
+ ngOnChanges(_changes) {
5325
+ if (!this.initialized && this.value != null) {
3687
5326
  this.initialized = true;
3688
- this.rows = this.mappingToRows(this.value);
3689
- this.jsonText = JSON.stringify(this.value, null, 2);
5327
+ // Migrate a legacy object value to JSONata once, and persist it upward so
5328
+ // the form is saved in the new format. Everything below works on a string.
5329
+ if (isLegacyOverrideMapping(this.value)) {
5330
+ this.expression = legacyOverrideToJsonata(this.value);
5331
+ this.value = this.expression || null;
5332
+ this.valueChange.emit(this.value);
5333
+ }
5334
+ else {
5335
+ this.expression = String(this.value);
5336
+ }
5337
+ this.loadFromExpression(this.expression);
3690
5338
  }
5339
+ this.refreshReferencedPaths();
3691
5340
  this.cdr.markForCheck();
3692
5341
  }
5342
+ ngOnDestroy() {
5343
+ this.destroy$.next();
5344
+ this.destroy$.complete();
5345
+ }
5346
+ /**
5347
+ * Fetch the selected process link's data mapping and extract the `$doc`/`$pv`/`$case`
5348
+ * paths it references, so the author sees what this template consumes. Refetches only
5349
+ * when the selected link changes; clears when no link is selected. Best-effort and
5350
+ * non-blocking — a failed fetch simply shows no suggestions.
5351
+ */
5352
+ refreshReferencedPaths() {
5353
+ const linkKey = this.processDefinitionKey && this.sourceActivityId
5354
+ ? `${this.processDefinitionKey}::${this.sourceActivityId}`
5355
+ : null;
5356
+ if (!linkKey) {
5357
+ if (this.lastFetchedLinkKey !== null) {
5358
+ this.lastFetchedLinkKey = null;
5359
+ this.referencedPaths = [];
5360
+ }
5361
+ return;
5362
+ }
5363
+ if (linkKey === this.lastFetchedLinkKey) {
5364
+ return;
5365
+ }
5366
+ this.lastFetchedLinkKey = linkKey;
5367
+ this.pluginService
5368
+ .getProcessLinkMapping(this.processDefinitionKey, this.sourceActivityId)
5369
+ .pipe(takeUntil$1(this.destroy$), catchError(() => of({ dataMapping: '' })))
5370
+ .subscribe((mapping) => {
5371
+ this.referencedPaths = extractReferencedPaths(mapping.dataMapping);
5372
+ this.cdr.markForCheck();
5373
+ });
5374
+ }
3693
5375
  toggleMode() {
3694
- this.advancedMode = !this.advancedMode;
3695
5376
  if (this.advancedMode) {
3696
- const mapping = this.rowsToMapping();
3697
- this.jsonText = Object.keys(mapping).length > 0 ? JSON.stringify(mapping, null, 2) : '';
3698
- this.jsonError = null;
5377
+ // Advanced -> simple: only possible when the expression round-trips.
5378
+ const parsed = parseOverrideJsonata(this.expression);
5379
+ if (parsed === null) {
5380
+ this.simpleUnavailable = true;
5381
+ return;
5382
+ }
5383
+ this.rows = parsed;
5384
+ this.simpleUnavailable = false;
5385
+ this.advancedMode = false;
3699
5386
  }
3700
5387
  else {
3701
- try {
3702
- const parsed = this.jsonText.trim() ? JSON.parse(this.jsonText) : {};
3703
- this.rows = this.mappingToRows(parsed);
3704
- this.jsonError = null;
3705
- }
3706
- catch {
3707
- // Keep current rows if JSON is invalid
3708
- }
5388
+ this.advancedMode = true;
3709
5389
  }
3710
5390
  }
3711
5391
  addRow() {
@@ -3715,65 +5395,105 @@ class EpistolaOverrideBuilderComponent {
3715
5395
  this.rows.splice(index, 1);
3716
5396
  this.emitChange();
3717
5397
  }
5398
+ /** Simple-table change: serialize rows back to a JSONata expression. */
3718
5399
  emitChange() {
3719
- const mapping = this.rowsToMapping();
3720
- this.value = Object.keys(mapping).length > 0 ? mapping : null;
3721
- this.valueChange.emit(this.value);
3722
- }
3723
- onJsonChange(text) {
3724
- this.jsonText = text;
3725
- if (!text.trim()) {
3726
- this.jsonError = null;
3727
- this.value = null;
3728
- this.valueChange.emit(null);
3729
- return;
3730
- }
3731
- try {
3732
- const parsed = JSON.parse(text);
3733
- this.jsonError = null;
3734
- this.value = parsed;
3735
- this.valueChange.emit(parsed);
5400
+ this.expression = serializeOverrideRows(this.rows);
5401
+ this.emit(this.expression);
5402
+ }
5403
+ /** Advanced-editor change. */
5404
+ onExpressionChange(expr) {
5405
+ this.expression = expr;
5406
+ this.simpleUnavailable = parseOverrideJsonata(expr) === null;
5407
+ this.emit(expr);
5408
+ }
5409
+ loadFromExpression(expression) {
5410
+ const parsed = parseOverrideJsonata(expression);
5411
+ if (parsed === null) {
5412
+ // Richer than the simple table can show — start in advanced mode.
5413
+ this.simpleUnavailable = true;
5414
+ this.advancedMode = true;
5415
+ this.rows = [];
3736
5416
  }
3737
- catch (e) {
3738
- this.jsonError = 'Invalid JSON';
5417
+ else {
5418
+ this.simpleUnavailable = false;
5419
+ this.rows = parsed;
3739
5420
  }
3740
5421
  }
3741
- rowsToMapping() {
3742
- const mapping = {};
3743
- for (const row of this.rows) {
3744
- if (row.inputPath && row.formFieldKey) {
3745
- if (!mapping[row.scope]) {
3746
- mapping[row.scope] = {};
3747
- }
3748
- mapping[row.scope][row.inputPath] = FORM_REF_PREFIX + row.formFieldKey;
3749
- }
3750
- }
3751
- return mapping;
3752
- }
3753
- mappingToRows(mapping) {
3754
- const rows = [];
3755
- for (const [scope, fields] of Object.entries(mapping)) {
3756
- if (scope === 'doc' || scope === 'pv') {
3757
- for (const [path, ref] of Object.entries(fields)) {
3758
- const formFieldKey = String(ref).startsWith(FORM_REF_PREFIX)
3759
- ? String(ref).substring(FORM_REF_PREFIX.length)
3760
- : String(ref);
3761
- rows.push({ scope, inputPath: path, formFieldKey });
3762
- }
3763
- }
3764
- }
3765
- return rows;
5422
+ emit(expression) {
5423
+ const next = expression && expression.trim() ? expression : null;
5424
+ this.value = next;
5425
+ this.valueChange.emit(next);
3766
5426
  }
3767
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaOverrideBuilderComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
3768
- 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: `
5427
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaOverrideBuilderComponent, deps: [{ token: i0.ChangeDetectorRef }, { token: EpistolaPluginService }], target: i0.ɵɵFactoryTarget.Component });
5428
+ 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", processDefinitionKey: "processDefinitionKey", sourceActivityId: "sourceActivityId" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
3769
5429
  <div class="override-builder">
3770
5430
  <div class="builder-header">
3771
5431
  <span class="builder-label">{{ label || 'Input Overrides' }}</span>
3772
- <button type="button" class="mode-toggle" (click)="toggleMode()">
5432
+ <button
5433
+ type="button"
5434
+ class="mode-toggle"
5435
+ [disabled]="simpleUnavailable && !advancedMode"
5436
+ (click)="toggleMode()"
5437
+ >
3773
5438
  {{ advancedMode ? 'Simple' : 'Advanced' }}
3774
5439
  </button>
3775
5440
  </div>
3776
5441
 
5442
+ <!-- Inline guidance for the author -->
5443
+ <p class="builder-intro">
5444
+ Make the preview reflect what the user is typing — <em>before</em> they submit — by feeding
5445
+ live form values into the document inputs.
5446
+ </p>
5447
+ <details class="builder-help">
5448
+ <summary>When should I map a field?</summary>
5449
+ <ul>
5450
+ <li>
5451
+ <strong>Map</strong> a field when its value ends up in the generated document — i.e. the
5452
+ template's data mapping reads that <code>doc</code>/<code>pv</code> path. The preview
5453
+ then updates live as the field is filled in.
5454
+ </li>
5455
+ <li>
5456
+ <strong>Don't map</strong> fields that don't affect the document, or values that are
5457
+ already saved on the case/process before this task — those are read from the real data
5458
+ automatically.
5459
+ </li>
5460
+ <li>
5461
+ Overriding a path the template never reads has <strong>no effect</strong> on the
5462
+ preview.
5463
+ </li>
5464
+ </ul>
5465
+ <p class="builder-help__how">
5466
+ <strong>How it works:</strong> <code>$form</code> holds the current form values; the
5467
+ mapping returns a <code>{{ '{' }} doc, pv {{ '}' }}</code> overlay used
5468
+ <strong>only for the preview</strong>. The actual document is always generated from the
5469
+ real saved data after the form is submitted.
5470
+ </p>
5471
+ </details>
5472
+
5473
+ <!-- Variables the selected template's mapping consumes (read-only guidance) -->
5474
+ <details *ngIf="hasReferencedPaths" class="used-by-template">
5475
+ <summary class="used-by-template__label">
5476
+ Used by this template ({{ referencedPaths.length }})
5477
+ </summary>
5478
+ <p class="used-by-template__hint">
5479
+ This template's data mapping reads these inputs — the paths worth overriding for the
5480
+ preview.
5481
+ </p>
5482
+ <ul class="used-by-template__list">
5483
+ <li *ngFor="let ref of referencedPaths">
5484
+ <code>{{ formatReferencedPath(ref) }}</code>
5485
+ </li>
5486
+ </ul>
5487
+ </details>
5488
+
5489
+ <!-- Per-scope autocomplete options for the Input Path column -->
5490
+ <datalist id="epistola-override-paths-doc">
5491
+ <option *ngFor="let p of referencedPathsForScope('doc')" [value]="p"></option>
5492
+ </datalist>
5493
+ <datalist id="epistola-override-paths-pv">
5494
+ <option *ngFor="let p of referencedPathsForScope('pv')" [value]="p"></option>
5495
+ </datalist>
5496
+
3777
5497
  <!-- Simple mode: table -->
3778
5498
  <div *ngIf="!advancedMode" class="builder-table">
3779
5499
  <div *ngIf="rows.length > 0" class="table-header">
@@ -3792,6 +5512,7 @@ class EpistolaOverrideBuilderComponent {
3792
5512
  type="text"
3793
5513
  [(ngModel)]="row.inputPath"
3794
5514
  (ngModelChange)="emitChange()"
5515
+ [attr.list]="'epistola-override-paths-' + row.scope"
3795
5516
  placeholder="e.g. beslissing.tekst"
3796
5517
  />
3797
5518
  <!-- Dropdown when form fields are available, text input as fallback -->
@@ -3823,31 +5544,96 @@ class EpistolaOverrideBuilderComponent {
3823
5544
  </button>
3824
5545
  </div>
3825
5546
 
3826
- <!-- Advanced mode: JSON editor -->
5547
+ <!-- Advanced mode: JSONata editor over $form -->
3827
5548
  <div *ngIf="advancedMode" class="builder-advanced">
3828
- <textarea
3829
- class="json-editor"
3830
- [ngModel]="jsonText"
3831
- (ngModelChange)="onJsonChange($event)"
3832
- placeholder='{ "pv": { "motivation": "form:pv:motivation" } }'
3833
- rows="6"
3834
- ></textarea>
3835
- <div *ngIf="jsonError" class="json-error">{{ jsonError }}</div>
5549
+ <div *ngIf="simpleUnavailable" class="advanced-note">
5550
+ This expression is too rich for the simple table — edit it here.
5551
+ </div>
5552
+ <epistola-jsonata-editor
5553
+ [expression]="expression"
5554
+ [contextVariables]="editorContextVariables"
5555
+ variablesHint="$form"
5556
+ (expressionChange)="onExpressionChange($event)"
5557
+ ></epistola-jsonata-editor>
5558
+ <div class="advanced-hint">
5559
+ Map form fields onto a <code>{{ '{' }} doc, pv {{ '}' }}</code> overlay, e.g.
5560
+ <code>{{ exampleExpression }}</code>
5561
+ </div>
3836
5562
  </div>
3837
5563
  </div>
3838
- `, 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 });
5564
+ `, 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}.builder-intro{font-size:.78rem;color:#495057;margin:0 0 .4rem;line-height:1.4}.builder-help{margin:0 0 .6rem;font-size:.76rem;color:#6c757d}.builder-help>summary{cursor:pointer;color:#0d6efd;font-size:.76rem;-webkit-user-select:none;user-select:none}.builder-help ul{margin:.35rem 0;padding-left:1.1rem;line-height:1.45}.builder-help li{margin-bottom:.2rem}.builder-help__how{margin:.35rem 0 0;line-height:1.45}.builder-help code{background:#eef0f2;border-radius:3px;padding:0 .2rem}.used-by-template{border:1px solid #d6e4ff;background:#f0f6ff;border-radius:4px;padding:.5rem .6rem;margin:0 0 .6rem}.used-by-template__label{font-weight:600;font-size:.78rem;color:#495057;cursor:pointer;-webkit-user-select:none;user-select:none}.used-by-template[open] .used-by-template__label{margin-bottom:.1rem}.used-by-template__hint{margin:.2rem 0 .4rem;font-size:.74rem;color:#6c757d;line-height:1.4}.used-by-template__list{margin:0;padding-left:1.1rem;display:flex;flex-wrap:wrap;gap:.15rem 1rem;list-style:none}.used-by-template__list code{background:#e2ecff;border-radius:3px;padding:0 .25rem;font-size:.76rem;color:#0d4a9c}.mode-toggle{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.15rem .5rem;font-size:.75rem;cursor:pointer}.mode-toggle:hover:not(:disabled){background:#e9ecef}.mode-toggle:disabled{opacity:.5;cursor:not-allowed}.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}.advanced-note{color:#b54708;font-size:.75rem;margin-bottom:.4rem}.advanced-hint{color:#6c757d;font-size:.72rem;margin-top:.35rem;line-height:1.4}.advanced-hint code{background:#eef0f2;border-radius:3px;padding:0 .2rem;font-size:.95em}\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: i3$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3$1.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: i3$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i3$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: JsonataEditorComponent, selector: "epistola-jsonata-editor", inputs: ["expression", "disabled", "contextVariables", "functions", "variablesHint"], outputs: ["expressionChange", "validChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3839
5565
  }
3840
5566
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaOverrideBuilderComponent, decorators: [{
3841
5567
  type: Component,
3842
- args: [{ standalone: true, imports: [CommonModule, FormsModule], selector: 'epistola-override-builder-component', changeDetection: ChangeDetectionStrategy.OnPush, template: `
5568
+ args: [{ standalone: true, imports: [CommonModule, FormsModule, JsonataEditorComponent], selector: 'epistola-override-builder-component', changeDetection: ChangeDetectionStrategy.OnPush, template: `
3843
5569
  <div class="override-builder">
3844
5570
  <div class="builder-header">
3845
5571
  <span class="builder-label">{{ label || 'Input Overrides' }}</span>
3846
- <button type="button" class="mode-toggle" (click)="toggleMode()">
5572
+ <button
5573
+ type="button"
5574
+ class="mode-toggle"
5575
+ [disabled]="simpleUnavailable && !advancedMode"
5576
+ (click)="toggleMode()"
5577
+ >
3847
5578
  {{ advancedMode ? 'Simple' : 'Advanced' }}
3848
5579
  </button>
3849
5580
  </div>
3850
5581
 
5582
+ <!-- Inline guidance for the author -->
5583
+ <p class="builder-intro">
5584
+ Make the preview reflect what the user is typing — <em>before</em> they submit — by feeding
5585
+ live form values into the document inputs.
5586
+ </p>
5587
+ <details class="builder-help">
5588
+ <summary>When should I map a field?</summary>
5589
+ <ul>
5590
+ <li>
5591
+ <strong>Map</strong> a field when its value ends up in the generated document — i.e. the
5592
+ template's data mapping reads that <code>doc</code>/<code>pv</code> path. The preview
5593
+ then updates live as the field is filled in.
5594
+ </li>
5595
+ <li>
5596
+ <strong>Don't map</strong> fields that don't affect the document, or values that are
5597
+ already saved on the case/process before this task — those are read from the real data
5598
+ automatically.
5599
+ </li>
5600
+ <li>
5601
+ Overriding a path the template never reads has <strong>no effect</strong> on the
5602
+ preview.
5603
+ </li>
5604
+ </ul>
5605
+ <p class="builder-help__how">
5606
+ <strong>How it works:</strong> <code>$form</code> holds the current form values; the
5607
+ mapping returns a <code>{{ '{' }} doc, pv {{ '}' }}</code> overlay used
5608
+ <strong>only for the preview</strong>. The actual document is always generated from the
5609
+ real saved data after the form is submitted.
5610
+ </p>
5611
+ </details>
5612
+
5613
+ <!-- Variables the selected template's mapping consumes (read-only guidance) -->
5614
+ <details *ngIf="hasReferencedPaths" class="used-by-template">
5615
+ <summary class="used-by-template__label">
5616
+ Used by this template ({{ referencedPaths.length }})
5617
+ </summary>
5618
+ <p class="used-by-template__hint">
5619
+ This template's data mapping reads these inputs — the paths worth overriding for the
5620
+ preview.
5621
+ </p>
5622
+ <ul class="used-by-template__list">
5623
+ <li *ngFor="let ref of referencedPaths">
5624
+ <code>{{ formatReferencedPath(ref) }}</code>
5625
+ </li>
5626
+ </ul>
5627
+ </details>
5628
+
5629
+ <!-- Per-scope autocomplete options for the Input Path column -->
5630
+ <datalist id="epistola-override-paths-doc">
5631
+ <option *ngFor="let p of referencedPathsForScope('doc')" [value]="p"></option>
5632
+ </datalist>
5633
+ <datalist id="epistola-override-paths-pv">
5634
+ <option *ngFor="let p of referencedPathsForScope('pv')" [value]="p"></option>
5635
+ </datalist>
5636
+
3851
5637
  <!-- Simple mode: table -->
3852
5638
  <div *ngIf="!advancedMode" class="builder-table">
3853
5639
  <div *ngIf="rows.length > 0" class="table-header">
@@ -3866,6 +5652,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3866
5652
  type="text"
3867
5653
  [(ngModel)]="row.inputPath"
3868
5654
  (ngModelChange)="emitChange()"
5655
+ [attr.list]="'epistola-override-paths-' + row.scope"
3869
5656
  placeholder="e.g. beslissing.tekst"
3870
5657
  />
3871
5658
  <!-- Dropdown when form fields are available, text input as fallback -->
@@ -3897,20 +5684,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3897
5684
  </button>
3898
5685
  </div>
3899
5686
 
3900
- <!-- Advanced mode: JSON editor -->
5687
+ <!-- Advanced mode: JSONata editor over $form -->
3901
5688
  <div *ngIf="advancedMode" class="builder-advanced">
3902
- <textarea
3903
- class="json-editor"
3904
- [ngModel]="jsonText"
3905
- (ngModelChange)="onJsonChange($event)"
3906
- placeholder='{ "pv": { "motivation": "form:pv:motivation" } }'
3907
- rows="6"
3908
- ></textarea>
3909
- <div *ngIf="jsonError" class="json-error">{{ jsonError }}</div>
5689
+ <div *ngIf="simpleUnavailable" class="advanced-note">
5690
+ This expression is too rich for the simple table — edit it here.
5691
+ </div>
5692
+ <epistola-jsonata-editor
5693
+ [expression]="expression"
5694
+ [contextVariables]="editorContextVariables"
5695
+ variablesHint="$form"
5696
+ (expressionChange)="onExpressionChange($event)"
5697
+ ></epistola-jsonata-editor>
5698
+ <div class="advanced-hint">
5699
+ Map form fields onto a <code>{{ '{' }} doc, pv {{ '}' }}</code> overlay, e.g.
5700
+ <code>{{ exampleExpression }}</code>
5701
+ </div>
3910
5702
  </div>
3911
5703
  </div>
3912
- `, 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"] }]
3913
- }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { value: [{
5704
+ `, 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}.builder-intro{font-size:.78rem;color:#495057;margin:0 0 .4rem;line-height:1.4}.builder-help{margin:0 0 .6rem;font-size:.76rem;color:#6c757d}.builder-help>summary{cursor:pointer;color:#0d6efd;font-size:.76rem;-webkit-user-select:none;user-select:none}.builder-help ul{margin:.35rem 0;padding-left:1.1rem;line-height:1.45}.builder-help li{margin-bottom:.2rem}.builder-help__how{margin:.35rem 0 0;line-height:1.45}.builder-help code{background:#eef0f2;border-radius:3px;padding:0 .2rem}.used-by-template{border:1px solid #d6e4ff;background:#f0f6ff;border-radius:4px;padding:.5rem .6rem;margin:0 0 .6rem}.used-by-template__label{font-weight:600;font-size:.78rem;color:#495057;cursor:pointer;-webkit-user-select:none;user-select:none}.used-by-template[open] .used-by-template__label{margin-bottom:.1rem}.used-by-template__hint{margin:.2rem 0 .4rem;font-size:.74rem;color:#6c757d;line-height:1.4}.used-by-template__list{margin:0;padding-left:1.1rem;display:flex;flex-wrap:wrap;gap:.15rem 1rem;list-style:none}.used-by-template__list code{background:#e2ecff;border-radius:3px;padding:0 .25rem;font-size:.76rem;color:#0d4a9c}.mode-toggle{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.15rem .5rem;font-size:.75rem;cursor:pointer}.mode-toggle:hover:not(:disabled){background:#e9ecef}.mode-toggle:disabled{opacity:.5;cursor:not-allowed}.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}.advanced-note{color:#b54708;font-size:.75rem;margin-bottom:.4rem}.advanced-hint{color:#6c757d;font-size:.72rem;margin-top:.35rem;line-height:1.4}.advanced-hint code{background:#eef0f2;border-radius:3px;padding:0 .2rem;font-size:.95em}\n"] }]
5705
+ }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }, { type: EpistolaPluginService }], propDecorators: { value: [{
3914
5706
  type: Input
3915
5707
  }], valueChange: [{
3916
5708
  type: Output
@@ -3920,8 +5712,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3920
5712
  type: Input
3921
5713
  }], availableFields: [{
3922
5714
  type: Input
5715
+ }], processDefinitionKey: [{
5716
+ type: Input
5717
+ }], sourceActivityId: [{
5718
+ type: Input
3923
5719
  }] } });
3924
5720
 
5721
+ /*
5722
+ * Copyright 2025 Epistola.
5723
+ *
5724
+ * Licensed under EUPL, Version 1.2 (the "License");
5725
+ * you may not use this file except in compliance with the License.
5726
+ * You may obtain a copy of the License at
5727
+ *
5728
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
5729
+ *
5730
+ * Unless required by applicable law or agreed to in writing, software
5731
+ * distributed under the License is distributed on an "AS IS" basis,
5732
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5733
+ * See the License for the specific language governing permissions and
5734
+ * limitations under the License.
5735
+ *
5736
+ * SPDX-License-Identifier: EUPL-1.2
5737
+ */
3925
5738
  const EPISTOLA_OVERRIDE_BUILDER_OPTIONS = {
3926
5739
  type: 'epistola-override-builder',
3927
5740
  selector: 'epistola-override-builder-element',
@@ -3929,7 +5742,7 @@ const EPISTOLA_OVERRIDE_BUILDER_OPTIONS = {
3929
5742
  group: 'basic',
3930
5743
  icon: 'list',
3931
5744
  emptyValue: null,
3932
- fieldOptions: ['label', 'availableFields'],
5745
+ fieldOptions: ['label', 'availableFields', 'processDefinitionKey', 'sourceActivityId'],
3933
5746
  };
3934
5747
  /**
3935
5748
  * Recursively collect input field keys and labels from a Formio component tree.
@@ -3967,12 +5780,45 @@ function registerEpistolaOverrideBuilderComponent(injector) {
3967
5780
  const BaseComponent = Formio.Components.components[EPISTOLA_OVERRIDE_BUILDER_OPTIONS.type];
3968
5781
  if (!BaseComponent)
3969
5782
  return;
3970
- // Extend the base class to pass available form fields to the Angular component
5783
+ // Extend the base class to pass available form fields and the selected process link
5784
+ // to the Angular component.
3971
5785
  class OverrideBuilderWithFields extends BaseComponent {
5786
+ _selectionChangeHandler = null;
3972
5787
  attach(element) {
3973
- // Set form fields on the component BEFORE super.attach() reads fieldOptions
5788
+ // Set inputs on the component BEFORE super.attach() reads fieldOptions.
3974
5789
  this.component.availableFields = this._extractFormFields();
3975
- return super.attach(element);
5790
+ this._applyProcessLinkSelection();
5791
+ const result = super.attach(element);
5792
+ // The override builder lives in the preview component's editForm alongside the
5793
+ // process-link selector (key `processLinkSelection`). When the author changes the
5794
+ // selected link, push the new identity straight to the Angular element so it can
5795
+ // refetch the link's data mapping — Formio doesn't always redraw this widget on a
5796
+ // sibling change. Mirrors the listener lifecycle in epistola-document-preview.formio.ts.
5797
+ if (this.root?.on && !this._selectionChangeHandler) {
5798
+ this._selectionChangeHandler = () => {
5799
+ const selection = this.root?.data?.processLinkSelection;
5800
+ if (this._customAngularElement) {
5801
+ this._customAngularElement['processDefinitionKey'] =
5802
+ selection?.processDefinitionKey || '';
5803
+ this._customAngularElement['sourceActivityId'] = selection?.sourceActivityId || '';
5804
+ }
5805
+ };
5806
+ this.root.on('change', this._selectionChangeHandler);
5807
+ }
5808
+ return result;
5809
+ }
5810
+ detach() {
5811
+ if (this._selectionChangeHandler && this.root?.off) {
5812
+ this.root.off('change', this._selectionChangeHandler);
5813
+ this._selectionChangeHandler = null;
5814
+ }
5815
+ return super.detach();
5816
+ }
5817
+ _applyProcessLinkSelection() {
5818
+ // The editForm dialog stores the selected link under `processLinkSelection`.
5819
+ const selection = this.root?.data?.processLinkSelection;
5820
+ this.component.processDefinitionKey = selection?.processDefinitionKey || '';
5821
+ this.component.sourceActivityId = selection?.sourceActivityId || '';
3976
5822
  }
3977
5823
  _extractFormFields() {
3978
5824
  // The Formio builder passes the main form schema as options.editForm
@@ -3986,8 +5832,27 @@ function registerEpistolaOverrideBuilderComponent(injector) {
3986
5832
  }
3987
5833
  // Re-register with the extended class
3988
5834
  Formio.Components.setComponent(EPISTOLA_OVERRIDE_BUILDER_OPTIONS.type, OverrideBuilderWithFields);
5835
+ // Internal editForm widget — not a standalone form field. Hide it from the builder palette.
5836
+ hideFormioComponentFromBuilder(EPISTOLA_OVERRIDE_BUILDER_OPTIONS.type);
3989
5837
  }
3990
5838
 
5839
+ /*
5840
+ * Copyright 2025 Epistola.
5841
+ *
5842
+ * Licensed under EUPL, Version 1.2 (the "License");
5843
+ * you may not use this file except in compliance with the License.
5844
+ * You may obtain a copy of the License at
5845
+ *
5846
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
5847
+ *
5848
+ * Unless required by applicable law or agreed to in writing, software
5849
+ * distributed under the License is distributed on an "AS IS" basis,
5850
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5851
+ * See the License for the specific language governing permissions and
5852
+ * limitations under the License.
5853
+ *
5854
+ * SPDX-License-Identifier: EUPL-1.2
5855
+ */
3991
5856
  /**
3992
5857
  * The plugin action definition key the backend serializes for generate-document
3993
5858
  * process links. It carries the `epistola-` prefix — see `EPISTOLA_ACTION_KEYS`
@@ -4000,6 +5865,23 @@ function filterGenerateDocumentEntries(entries) {
4000
5865
  return entries.filter((e) => e.actionKey === GENERATE_DOCUMENT_ACTION_KEY);
4001
5866
  }
4002
5867
 
5868
+ /*
5869
+ * Copyright 2025 Epistola.
5870
+ *
5871
+ * Licensed under EUPL, Version 1.2 (the "License");
5872
+ * you may not use this file except in compliance with the License.
5873
+ * You may obtain a copy of the License at
5874
+ *
5875
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
5876
+ *
5877
+ * Unless required by applicable law or agreed to in writing, software
5878
+ * distributed under the License is distributed on an "AS IS" basis,
5879
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5880
+ * See the License for the specific language governing permissions and
5881
+ * limitations under the License.
5882
+ *
5883
+ * SPDX-License-Identifier: EUPL-1.2
5884
+ */
4003
5885
  class EpistolaProcessLinkSelectorComponent {
4004
5886
  adminService;
4005
5887
  cdr;
@@ -4082,7 +5964,7 @@ class EpistolaProcessLinkSelectorComponent {
4082
5964
  </select>
4083
5965
  <div *ngIf="error" class="selector-error">{{ error }}</div>
4084
5966
  </div>
4085
- `, 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 });
5967
+ `, 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: i3$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i3$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4086
5968
  }
4087
5969
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaProcessLinkSelectorComponent, decorators: [{
4088
5970
  type: Component,
@@ -4113,6 +5995,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
4113
5995
  type: Input
4114
5996
  }] } });
4115
5997
 
5998
+ /*
5999
+ * Copyright 2025 Epistola.
6000
+ *
6001
+ * Licensed under EUPL, Version 1.2 (the "License");
6002
+ * you may not use this file except in compliance with the License.
6003
+ * You may obtain a copy of the License at
6004
+ *
6005
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
6006
+ *
6007
+ * Unless required by applicable law or agreed to in writing, software
6008
+ * distributed under the License is distributed on an "AS IS" basis,
6009
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6010
+ * See the License for the specific language governing permissions and
6011
+ * limitations under the License.
6012
+ *
6013
+ * SPDX-License-Identifier: EUPL-1.2
6014
+ */
4116
6015
  const EPISTOLA_PROCESS_LINK_SELECTOR_OPTIONS = {
4117
6016
  type: 'epistola-process-link-selector',
4118
6017
  selector: 'epistola-process-link-selector-element',
@@ -4125,9 +6024,28 @@ const EPISTOLA_PROCESS_LINK_SELECTOR_OPTIONS = {
4125
6024
  function registerEpistolaProcessLinkSelectorComponent(injector) {
4126
6025
  if (!customElements.get(EPISTOLA_PROCESS_LINK_SELECTOR_OPTIONS.selector)) {
4127
6026
  registerCustomFormioComponent(EPISTOLA_PROCESS_LINK_SELECTOR_OPTIONS, EpistolaProcessLinkSelectorComponent, injector);
6027
+ // Internal editForm widget — not a standalone form field. Hide it from the builder palette.
6028
+ hideFormioComponentFromBuilder(EPISTOLA_PROCESS_LINK_SELECTOR_OPTIONS.type);
4128
6029
  }
4129
6030
  }
4130
6031
 
6032
+ /*
6033
+ * Copyright 2025 Epistola.
6034
+ *
6035
+ * Licensed under EUPL, Version 1.2 (the "License");
6036
+ * you may not use this file except in compliance with the License.
6037
+ * You may obtain a copy of the License at
6038
+ *
6039
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
6040
+ *
6041
+ * Unless required by applicable law or agreed to in writing, software
6042
+ * distributed under the License is distributed on an "AS IS" basis,
6043
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6044
+ * See the License for the specific language governing permissions and
6045
+ * limitations under the License.
6046
+ *
6047
+ * SPDX-License-Identifier: EUPL-1.2
6048
+ */
4131
6049
  class EpistolaPluginModule {
4132
6050
  // Kept for back-compat with hosts that follow the README's `forRoot()`
4133
6051
  // setup. The providers above are now module-level so `imports: [EpistolaPluginModule]`
@@ -4162,11 +6080,6 @@ class EpistolaPluginModule {
4162
6080
  EpistolaPluginService,
4163
6081
  EpistolaAdminService,
4164
6082
  EpistolaMenuService,
4165
- {
4166
- provide: HTTP_INTERCEPTORS,
4167
- useClass: EpistolaTaskContextInterceptor,
4168
- multi: true,
4169
- },
4170
6083
  {
4171
6084
  provide: ENVIRONMENT_INITIALIZER,
4172
6085
  multi: true,
@@ -4233,11 +6146,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
4233
6146
  EpistolaPluginService,
4234
6147
  EpistolaAdminService,
4235
6148
  EpistolaMenuService,
4236
- {
4237
- provide: HTTP_INTERCEPTORS,
4238
- useClass: EpistolaTaskContextInterceptor,
4239
- multi: true,
4240
- },
4241
6149
  {
4242
6150
  provide: ENVIRONMENT_INITIALIZER,
4243
6151
  multi: true,
@@ -4258,8 +6166,60 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
4258
6166
  }]
4259
6167
  }] });
4260
6168
 
6169
+ /*
6170
+ * Copyright 2025 Epistola.
6171
+ *
6172
+ * Licensed under EUPL, Version 1.2 (the "License");
6173
+ * you may not use this file except in compliance with the License.
6174
+ * You may obtain a copy of the License at
6175
+ *
6176
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
6177
+ *
6178
+ * Unless required by applicable law or agreed to in writing, software
6179
+ * distributed under the License is distributed on an "AS IS" basis,
6180
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6181
+ * See the License for the specific language governing permissions and
6182
+ * limitations under the License.
6183
+ *
6184
+ * SPDX-License-Identifier: EUPL-1.2
6185
+ */
4261
6186
  const EPISTOLA_PLUGIN_LOGO_BASE64 = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAgMTIwIiB3aWR0aD0iMTIwIiBoZWlnaHQ9IjEyMCI+CiAgPCEtLSBTdGFjayBiYXNlIC0tPgogIDxyZWN0IHg9IjM2IiB5PSIxNiIgd2lkdGg9IjU0IiBoZWlnaHQ9IjcwIiByeD0iMyIgZmlsbD0iI2U2YzJiMCIgc3Ryb2tlPSIjNGYyZjJiIiBzdHJva2Utd2lkdGg9IjIiIHRyYW5zZm9ybT0icm90YXRlKDUgNjMgNTEpIi8+CiAgPHJlY3QgeD0iMzIiIHk9IjIyIiB3aWR0aD0iNTQiIGhlaWdodD0iNzAiIHJ4PSIzIiBmaWxsPSIjZjBkOGM4IiBzdHJva2U9IiM0ZjJmMmIiIHN0cm9rZS13aWR0aD0iMiIvPgogIDxyZWN0IHg9IjI4IiB5PSIyOCIgd2lkdGg9IjU0IiBoZWlnaHQ9IjcwIiByeD0iMyIgZmlsbD0iI2Y1ZWJlMyIgc3Ryb2tlPSIjNGYyZjJiIiBzdHJva2Utd2lkdGg9IjIuNSIvPgogIDxsaW5lIHgxPSIzOCIgeTE9IjQ0IiB4Mj0iNzIiIHkyPSI0NCIgc3Ryb2tlPSIjYzRhODgyIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgogIDxsaW5lIHgxPSIzOCIgeTE9IjU0IiB4Mj0iNzIiIHkyPSI1NCIgc3Ryb2tlPSIjYzRhODgyIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgogIDxsaW5lIHgxPSIzOCIgeTE9IjY0IiB4Mj0iNTgiIHkyPSI2NCIgc3Ryb2tlPSIjYzRhODgyIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgogIDwhLS0gV2F4IHNlYWwgd2l0aCBjaGVja21hcmsgLS0+CiAgPGNpcmNsZSBjeD0iNTUiIGN5PSI4NCIgcj0iMTUiIGZpbGw9IiNiODVjM2MiIHN0cm9rZT0iIzRmMmYyYiIgc3Ryb2tlLXdpZHRoPSIyIi8+CiAgPGNpcmNsZSBjeD0iNTUiIGN5PSI4NCIgcj0iMTAuNSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZDQ4MzZhIiBzdHJva2Utd2lkdGg9IjEiLz4KICA8cG9seWxpbmUgcG9pbnRzPSI0OSw4NCA1Myw4OSA2Miw3OCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNGYyZjJiIiBzdHJva2Utd2lkdGg9IjIuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo=';
4262
6187
 
6188
+ /*
6189
+ * Copyright 2025 Epistola.
6190
+ *
6191
+ * Licensed under EUPL, Version 1.2 (the "License");
6192
+ * you may not use this file except in compliance with the License.
6193
+ * You may obtain a copy of the License at
6194
+ *
6195
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
6196
+ *
6197
+ * Unless required by applicable law or agreed to in writing, software
6198
+ * distributed under the License is distributed on an "AS IS" basis,
6199
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6200
+ * See the License for the specific language governing permissions and
6201
+ * limitations under the License.
6202
+ *
6203
+ * SPDX-License-Identifier: EUPL-1.2
6204
+ */
6205
+
6206
+ /*
6207
+ * Copyright 2025 Epistola.
6208
+ *
6209
+ * Licensed under EUPL, Version 1.2 (the "License");
6210
+ * you may not use this file except in compliance with the License.
6211
+ * You may obtain a copy of the License at
6212
+ *
6213
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
6214
+ *
6215
+ * Unless required by applicable law or agreed to in writing, software
6216
+ * distributed under the License is distributed on an "AS IS" basis,
6217
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6218
+ * See the License for the specific language governing permissions and
6219
+ * limitations under the License.
6220
+ *
6221
+ * SPDX-License-Identifier: EUPL-1.2
6222
+ */
4263
6223
  const EPISTOLA_PLUGIN_ID = 'epistola';
4264
6224
  const DISABLED_EPISTOLA_PLUGIN_ID = '__epistola_disabled__';
4265
6225
  const epistolaPluginSpecification = {
@@ -4382,8 +6342,12 @@ const epistolaPluginSpecification = {
4382
6342
  'epistola-download-document': 'Download Document',
4383
6343
  documentVariable: 'Document Variabele',
4384
6344
  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.',
6345
+ storageTarget: 'Opslagdoel',
6346
+ storageTargetTooltip: 'Waar het gedownloade PDF wordt opgeslagen. "Tijdelijke resource-opslag" (aanbevolen) zet alleen een resource-id in de variabele — geschikt om door te geven aan documenten-api:store-temp-document. "Procesvariabele" zet de ruwe bytes inline in de variabele (alleen voor kleine, niet-gevoelige documenten; deze worden in de taakrespons meegestuurd).',
6347
+ resourceIdVariable: 'Resource-ID Variabele',
6348
+ resourceIdVariableTooltip: 'Naam van de procesvariabele waarin het tijdelijke resource-id wordt opgeslagen (geef deze door aan documenten-api:store-temp-document).',
4385
6349
  contentVariable: 'Inhoud Variabele',
4386
- contentVariableTooltip: 'Naam van de procesvariabele waarin de documentinhoud (Base64) wordt opgeslagen',
6350
+ contentVariableTooltip: 'Naam van de procesvariabele waarin de ruwe PDF-bytes inline worden opgeslagen (alleen voor kleine, niet-gevoelige documenten).',
4387
6351
  // Admin page
4388
6352
  epistolaAdminOverview: 'Overzicht',
4389
6353
  epistolaAdminRefresh: 'Vernieuwen',
@@ -4411,6 +6375,10 @@ const epistolaPluginSpecification = {
4411
6375
  epistolaAdminReconcile: 'Hersynchroniseer',
4412
6376
  epistolaAdminReconciling: 'Bezig...',
4413
6377
  epistolaAdminReconcileTooltip: 'Vraag de huidige status op bij Epistola en hervat het wachtende proces als het klaar is.',
6378
+ epistolaAdminStatusWaiting: 'Wachtend',
6379
+ epistolaAdminStatusUnwired: 'Vastgelopen',
6380
+ epistolaAdminUnwiredTooltip: 'Deze taak heeft geen correlatie-token (epistolaWaitFor), dus de collector kan hem nooit hervatten — het proces is vastgelopen. Meestal een dubbelzinnig samengevoegd catch event (zie BPMN-validatie). Op te lossen in het procesmodel; hersynchroniseren werkt hier niet.',
6381
+ epistolaAdminUnwiredHint: 'Niet te hersynchroniseren',
4414
6382
  epistolaAdminConfigurations: 'Configuraties',
4415
6383
  epistolaAdminValidations: 'BPMN-validatie',
4416
6384
  epistolaAdminNoValidations: 'Geen race-onveilige procesdefinities gevonden. Alles ziet er goed uit.',
@@ -4418,6 +6386,10 @@ const epistolaPluginSpecification = {
4418
6386
  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.',
4419
6387
  epistolaAdminValidationCode: 'Code',
4420
6388
  epistolaAdminValidationMessage: 'Bericht',
6389
+ epistolaAdminValidationLastChecked: 'Laatst gecontroleerd',
6390
+ epistolaAdminValidationNotYetRun: 'nog niet uitgevoerd',
6391
+ epistolaAdminValidationAutoRefresh: 'automatisch opnieuw gecontroleerd, ongeveer elke',
6392
+ epistolaAdminValidationLatestVersionNote: 'Alleen de meest recente versie van elke procesdefinitie wordt gecontroleerd; oudere versies met nog lopende instanties kunnen problemen hebben die hier niet worden getoond.',
4421
6393
  epistolaAdminCatalogs: 'Catalogi',
4422
6394
  epistolaAdminCatalogsIntro: 'Catalogi op het classpath van de applicatie. Deze worden bij het opstarten automatisch uitgerold; gebruik Opnieuw uitrollen om een catalogus handmatig (geforceerd) naar deze Epistola-omgeving te sturen, bijvoorbeeld als de automatische uitrol is mislukt.',
4423
6395
  epistolaAdminNoCatalogs: 'Geen catalogi gevonden op het classpath.',
@@ -4433,6 +6405,23 @@ const epistolaPluginSpecification = {
4433
6405
  epistolaAdminChangelog: 'Changelog',
4434
6406
  epistolaAdminRunningVersion: 'Actieve plugin-versie:',
4435
6407
  epistolaAdminNoChangelog: 'Geen changelog beschikbaar in deze build.',
6408
+ // TEMPORARY (remove in 1.0.0): task-id carrier repair (admin "Forms" tab)
6409
+ epistolaAdminForms: 'Formulieren',
6410
+ epistolaAdminFormsIntro: 'Formulieren met een Epistola-component dat het verborgen task-id veld mist. Zonder dat veld werkt het voorbeeld/downloaden/opnieuw genereren niet in elke taak-openflow. Herstel voegt het veld toe.',
6411
+ epistolaAdminFormName: 'Formulier',
6412
+ epistolaAdminFormMissing: 'Ontbrekende componenten',
6413
+ epistolaAdminFormReadOnly: 'Alleen-lezen',
6414
+ epistolaAdminFormReadOnlyHint: 'Dit formulier komt van het classpath en wordt bij de volgende herstart teruggezet naar de bron. Voeg het veld toe aan de bron (component opnieuw plaatsen) voor een blijvende oplossing.',
6415
+ epistolaAdminRepair: 'Herstellen',
6416
+ epistolaAdminRepairAll: 'Alles herstellen',
6417
+ epistolaAdminRepairing: 'Bezig...',
6418
+ epistolaAdminRepairTooltip: 'Voeg het verborgen task-id veld toe aan alle Epistola-componenten in dit formulier.',
6419
+ epistolaAdminNoFormIssues: 'Geen formulieren met een ontbrekend task-id veld gevonden.',
6420
+ // TEMPORARY: legacy override-mapping format detection (admin "Forms" tab)
6421
+ epistolaAdminLegacyOverrideTitle: 'Verouderd invoer-override formaat',
6422
+ epistolaAdminLegacyOverrideIntro: 'Formulieren waarvan een document-voorbeeldcomponent de invoer-overrides nog als object opslaat ("form:"-verwijzingen) in plaats van als JSONata-expressie over $form. Ze blijven werken, maar worden pas naar het nieuwe formaat omgezet als je het formulier opnieuw opslaat in de formulierbouwer.',
6423
+ epistolaAdminLegacyOverrideComponents: 'Verouderde componenten',
6424
+ epistolaAdminNoLegacyOverride: 'Geen formulieren met het verouderde override-formaat gevonden.',
4436
6425
  },
4437
6426
  en: {
4438
6427
  title: 'Epistola Document Suite',
@@ -4538,8 +6527,12 @@ const epistolaPluginSpecification = {
4538
6527
  'epistola-download-document': 'Download Document',
4539
6528
  documentVariable: 'Document Variable',
4540
6529
  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.',
6530
+ storageTarget: 'Storage Target',
6531
+ storageTargetTooltip: 'Where the downloaded PDF is stored. "Temporary resource storage" (recommended) writes only a resource id to the variable — ready to hand to documenten-api:store-temp-document. "Process variable" writes the raw bytes inline (small, non-sensitive documents only; they are included in the task response).',
6532
+ resourceIdVariable: 'Resource ID Variable',
6533
+ resourceIdVariableTooltip: 'Name of the process variable to store the temporary resource id in (hand this to documenten-api:store-temp-document).',
4541
6534
  contentVariable: 'Content Variable',
4542
- contentVariableTooltip: 'Name of the process variable to store the document content (Base64) in',
6535
+ contentVariableTooltip: 'Name of the process variable to store the raw PDF bytes inline in (small, non-sensitive documents only).',
4543
6536
  // Admin page
4544
6537
  epistolaAdminOverview: 'Overview',
4545
6538
  epistolaAdminRefresh: 'Refresh',
@@ -4567,6 +6560,10 @@ const epistolaPluginSpecification = {
4567
6560
  epistolaAdminReconcile: 'Reconcile',
4568
6561
  epistolaAdminReconciling: 'Reconciling...',
4569
6562
  epistolaAdminReconcileTooltip: "Ask Epistola for this job's current status and resume the waiting process if it has finished.",
6563
+ epistolaAdminStatusWaiting: 'Waiting',
6564
+ epistolaAdminStatusUnwired: 'Stuck',
6565
+ epistolaAdminUnwiredTooltip: 'This wait has no correlation token (epistolaWaitFor), so the collector can never resume it — the process is stuck. Usually an ambiguous merged catch event (see BPMN validation). Fix it in the process model; reconcile cannot recover it.',
6566
+ epistolaAdminUnwiredHint: 'Cannot reconcile',
4570
6567
  epistolaAdminConfigurations: 'Configurations',
4571
6568
  epistolaAdminValidations: 'BPMN validation',
4572
6569
  epistolaAdminNoValidations: 'No race-unsafe process definitions detected. Everything looks good.',
@@ -4574,6 +6571,10 @@ const epistolaPluginSpecification = {
4574
6571
  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.',
4575
6572
  epistolaAdminValidationCode: 'Code',
4576
6573
  epistolaAdminValidationMessage: 'Message',
6574
+ epistolaAdminValidationLastChecked: 'Last checked',
6575
+ epistolaAdminValidationNotYetRun: 'not yet run',
6576
+ epistolaAdminValidationAutoRefresh: 'automatically re-checked roughly every',
6577
+ epistolaAdminValidationLatestVersionNote: "Only the latest deployed version of each process definition is checked; older versions with running instances may have problems that aren't shown here.",
4577
6578
  epistolaAdminCatalogs: 'Catalogs',
4578
6579
  epistolaAdminCatalogsIntro: 'Catalogs on the application classpath. These are deployed automatically on startup; use Redeploy to manually (force) push one to this Epistola installation — for example when the automatic startup deploy failed.',
4579
6580
  epistolaAdminNoCatalogs: 'No catalogs found on the classpath.',
@@ -4589,10 +6590,44 @@ const epistolaPluginSpecification = {
4589
6590
  epistolaAdminChangelog: 'Changelog',
4590
6591
  epistolaAdminRunningVersion: 'Running plugin version:',
4591
6592
  epistolaAdminNoChangelog: 'No changelog available in this build.',
6593
+ // TEMPORARY (remove in 1.0.0): task-id carrier repair (admin "Forms" tab)
6594
+ epistolaAdminForms: 'Forms',
6595
+ epistolaAdminFormsIntro: 'Forms with an Epistola component that is missing the hidden task-id field. Without it, preview/download/retry do not work in every task-open flow. Repair adds the field.',
6596
+ epistolaAdminFormName: 'Form',
6597
+ epistolaAdminFormMissing: 'Missing components',
6598
+ epistolaAdminFormReadOnly: 'Read-only',
6599
+ epistolaAdminFormReadOnlyHint: 'This form is deployed from the classpath and is reconciled to its source on the next restart. Add the field to the source (re-drop the component) for a permanent fix.',
6600
+ epistolaAdminRepair: 'Repair',
6601
+ epistolaAdminRepairAll: 'Repair all',
6602
+ epistolaAdminRepairing: 'Working...',
6603
+ epistolaAdminRepairTooltip: 'Add the hidden task-id field to all Epistola components in this form.',
6604
+ epistolaAdminNoFormIssues: 'No forms with a missing task-id field found.',
6605
+ // TEMPORARY: legacy override-mapping format detection (admin "Forms" tab)
6606
+ epistolaAdminLegacyOverrideTitle: 'Legacy input-override format',
6607
+ epistolaAdminLegacyOverrideIntro: 'Forms whose document-preview component still stores input overrides as an object ("form:" references) instead of a JSONata expression over $form. They keep working, but only migrate to the new format once you re-save the form in the form builder.',
6608
+ epistolaAdminLegacyOverrideComponents: 'Legacy components',
6609
+ epistolaAdminNoLegacyOverride: 'No forms using the legacy override format found.',
4592
6610
  },
4593
6611
  },
4594
6612
  };
4595
6613
 
6614
+ /*
6615
+ * Copyright 2025 Epistola.
6616
+ *
6617
+ * Licensed under EUPL, Version 1.2 (the "License");
6618
+ * you may not use this file except in compliance with the License.
6619
+ * You may obtain a copy of the License at
6620
+ *
6621
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
6622
+ *
6623
+ * Unless required by applicable law or agreed to in writing, software
6624
+ * distributed under the License is distributed on an "AS IS" basis,
6625
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
6626
+ * See the License for the specific language governing permissions and
6627
+ * limitations under the License.
6628
+ *
6629
+ * SPDX-License-Identifier: EUPL-1.2
6630
+ */
4596
6631
  /*
4597
6632
  * Public API Surface of epistola plugin
4598
6633
  */
@@ -4601,5 +6636,5 @@ const epistolaPluginSpecification = {
4601
6636
  * Generated bundle index. Do not edit.
4602
6637
  */
4603
6638
 
4604
- 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 };
6639
+ 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, GenerateDocumentConfigurationComponent, JsonataEditorComponent, MappingBuilderComponent, PREFILLED_TASK_ID_CARRIER, PREFILLED_TASK_ID_DATA_KEY, PREFILLED_TASK_ID_SOURCE_KEY, epistolaPluginSpecification, errorResource, initialResource, isEpistolaEnabled, loadingResource, readPrefilledTaskId, registerEpistolaDocumentComponent, registerEpistolaDocumentPreviewComponent, registerEpistolaOverrideBuilderComponent, registerEpistolaProcessLinkSelectorComponent, registerEpistolaRetryFormComponent, successResource };
4605
6640
  //# sourceMappingURL=epistola.app-valtimo-plugin.mjs.map