@epistola.app/valtimo-plugin 0.10.0 → 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.
@@ -12,14 +12,14 @@ 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';
18
+ import * as i2$2 from '@valtimo/process-link';
19
+ import * as i2$3 from '@angular/platform-browser';
20
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
24
  import * as i5 from 'carbon-components-angular/tabs';
25
25
  import { TabsModule } from 'carbon-components-angular/tabs';
@@ -27,6 +27,41 @@ 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.
@@ -140,6 +264,11 @@ class EpistolaAdminService {
140
264
  repairAllFormCarriers() {
141
265
  return this.http.post(`${this.apiEndpoint}/forms/repair-carrier`, null);
142
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
+ }
143
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 });
144
273
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminService });
145
274
  }
@@ -147,6 +276,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
147
276
  type: Injectable
148
277
  }], ctorParameters: () => [{ type: i1.HttpClient }, { type: i2.ConfigService }] });
149
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
+ */
150
296
  /**
151
297
  * Registers the Epistola admin page menu item under the Admin > Other section.
152
298
  * Instantiated eagerly via ENVIRONMENT_INITIALIZER so the menu item
@@ -183,6 +329,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
183
329
  type: Injectable
184
330
  }], ctorParameters: () => [{ type: i3.MenuService }] });
185
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
+ */
186
349
  /**
187
350
  * Service for interacting with Epistola plugin API endpoints.
188
351
  * Provides methods to fetch templates, environments, variants,
@@ -250,6 +413,16 @@ class EpistolaPluginService {
250
413
  params: { processDefinitionKey },
251
414
  });
252
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
+ }
253
426
  /**
254
427
  * Get variable suggestions for JSONata autocompletion.
255
428
  */
@@ -341,6 +514,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
341
514
  type: Injectable
342
515
  }], ctorParameters: () => [{ type: i1.HttpClient }, { type: i2.ConfigService }] });
343
516
 
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
533
+ */
344
534
  /**
345
535
  * Helpers for reading the active user task's id out of a Valtimo task form that was
346
536
  * prefilled server-side by the {@code epistola:} value resolver (see the backend
@@ -433,12 +623,49 @@ function findSourceKeyDefaultValue(node, sourceKey) {
433
623
  return null;
434
624
  }
435
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
+ */
436
661
  class EpistolaConfigurationComponent {
437
662
  save$;
438
663
  disabled$;
439
664
  pluginId;
440
665
  prefillConfiguration$;
441
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.
442
669
  configuration = new EventEmitter();
443
670
  /** Epistola slug pattern: lowercase alphanumeric with hyphens, no leading/trailing hyphens. */
444
671
  static SLUG_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
@@ -487,7 +714,7 @@ class EpistolaConfigurationComponent {
487
714
  combineLatest([this.formValue$, this.valid$])
488
715
  .pipe(take(1))
489
716
  .subscribe(([formValue, valid]) => {
490
- if (valid) {
717
+ if (valid && formValue) {
491
718
  this.configuration.emit(formValue);
492
719
  }
493
720
  });
@@ -513,12 +740,34 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
513
740
  type: Output
514
741
  }] } });
515
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
+ */
516
760
  /**
517
761
  * Shared state for the JSONata completion provider.
518
762
  * Updated by the editor component when suggestions/functions change.
519
763
  */
520
764
  const jsonataCompletionData = {
521
- 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: {},
522
771
  functions: [],
523
772
  };
524
773
  /**
@@ -601,7 +850,8 @@ function registerJsonataLanguage(monaco) {
601
850
  const CompletionItemKind = monaco.languages.CompletionItemKind;
602
851
  // After "$" — suggest variables and functions
603
852
  if (textUntilPosition.endsWith('$')) {
604
- 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) => ({
605
855
  label: `$${v}`,
606
856
  kind: CompletionItemKind.Variable,
607
857
  insertText: v,
@@ -656,27 +906,20 @@ function registerJsonataLanguage(monaco) {
656
906
  });
657
907
  }
658
908
  }
659
- // After "$doc." — suggest document paths
660
- if (/\$doc\.\s*$/.test(textUntilPosition) || /\$doc\.[a-zA-Z_]*$/.test(textUntilPosition)) {
661
- const docPaths = jsonataCompletionData.suggestions?.doc || [];
662
- 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) {
663
915
  suggestions.push({
664
- label: path,
916
+ label: field,
665
917
  kind: CompletionItemKind.Field,
666
- insertText: path,
667
- detail: 'Document field',
668
- });
669
- }
670
- }
671
- // After "$pv." — suggest process variables
672
- if (/\$pv\.\s*$/.test(textUntilPosition) || /\$pv\.[a-zA-Z_]*$/.test(textUntilPosition)) {
673
- const pvNames = jsonataCompletionData.suggestions?.pv || [];
674
- for (const name of pvNames) {
675
- suggestions.push({
676
- label: name,
677
- kind: CompletionItemKind.Variable,
678
- insertText: name,
679
- 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`,
680
923
  });
681
924
  }
682
925
  }
@@ -685,12 +928,37 @@ function registerJsonataLanguage(monaco) {
685
928
  });
686
929
  }
687
930
 
688
- 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;
689
949
  class JsonataEditorComponent {
690
950
  expression = '';
691
951
  disabled = false;
692
- 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 = {};
693
959
  functions = [];
960
+ /** Footer hint listing the context variables in scope. */
961
+ variablesHint = '$doc · $pv · $case';
694
962
  expressionChange = new EventEmitter();
695
963
  validChange = new EventEmitter();
696
964
  editorModel = { value: '', language: 'jsonata' };
@@ -720,8 +988,8 @@ class JsonataEditorComponent {
720
988
  this.editorModel = { value: this.expression || '', language: 'jsonata' };
721
989
  this.validate$.next(this.expression);
722
990
  }
723
- if (changes['suggestions']) {
724
- jsonataCompletionData.suggestions = this.suggestions;
991
+ if (changes['contextVariables']) {
992
+ jsonataCompletionData.variables = this.contextVariables || {};
725
993
  }
726
994
  if (changes['functions']) {
727
995
  jsonataCompletionData.functions = this.functions;
@@ -749,7 +1017,7 @@ class JsonataEditorComponent {
749
1017
  if (m) {
750
1018
  registerJsonataLanguage(m);
751
1019
  this.languageRegistered = true;
752
- jsonataCompletionData.suggestions = this.suggestions;
1020
+ jsonataCompletionData.variables = this.contextVariables || {};
753
1021
  jsonataCompletionData.functions = this.functions;
754
1022
  }
755
1023
  }
@@ -760,7 +1028,7 @@ class JsonataEditorComponent {
760
1028
  return;
761
1029
  }
762
1030
  try {
763
- jsonata$1(value);
1031
+ jsonata$4(value);
764
1032
  this.error = null;
765
1033
  this.validChange.emit(true);
766
1034
  }
@@ -770,7 +1038,7 @@ class JsonataEditorComponent {
770
1038
  }
771
1039
  }
772
1040
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: JsonataEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
773
- 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: `
774
1042
  <div class="jsonata-editor">
775
1043
  <valtimo-editor
776
1044
  [model]="editorModel"
@@ -783,7 +1051,7 @@ class JsonataEditorComponent {
783
1051
  <div class="jsonata-editor__footer">
784
1052
  <span *ngIf="error" class="jsonata-editor__error">{{ error }}</span>
785
1053
  <span *ngIf="!error && expression" class="jsonata-editor__valid">&#x2713;</span>
786
- <span class="jsonata-editor__variables">$doc · $pv · $case</span>
1054
+ <span class="jsonata-editor__variables">{{ variablesHint }}</span>
787
1055
  </div>
788
1056
  </div>
789
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"] }] });
@@ -803,7 +1071,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
803
1071
  <div class="jsonata-editor__footer">
804
1072
  <span *ngIf="error" class="jsonata-editor__error">{{ error }}</span>
805
1073
  <span *ngIf="!error && expression" class="jsonata-editor__valid">&#x2713;</span>
806
- <span class="jsonata-editor__variables">$doc · $pv · $case</span>
1074
+ <span class="jsonata-editor__variables">{{ variablesHint }}</span>
807
1075
  </div>
808
1076
  </div>
809
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"] }]
@@ -811,16 +1079,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
811
1079
  type: Input
812
1080
  }], disabled: [{
813
1081
  type: Input
814
- }], suggestions: [{
1082
+ }], contextVariables: [{
815
1083
  type: Input
816
1084
  }], functions: [{
817
1085
  type: Input
1086
+ }], variablesHint: [{
1087
+ type: Input
818
1088
  }], expressionChange: [{
819
1089
  type: Output
820
1090
  }], validChange: [{
821
1091
  type: Output
822
1092
  }] } });
823
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
+ */
824
1111
  class ExpectedStructureComponent {
825
1112
  templateFields = [];
826
1113
  structureText = '{}';
@@ -885,6 +1172,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
885
1172
  type: Input
886
1173
  }] } });
887
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
+ */
888
1192
  class BuilderFieldComponent {
889
1193
  field;
890
1194
  path = [];
@@ -964,7 +1268,7 @@ class BuilderFieldComponent {
964
1268
  ></epistola-builder-field>
965
1269
  </div>
966
1270
  </div>
967
- `, 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"] }] });
968
1272
  }
969
1273
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: BuilderFieldComponent, decorators: [{
970
1274
  type: Component,
@@ -1055,7 +1359,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
1055
1359
  type: Output
1056
1360
  }] } });
1057
1361
 
1058
- 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;
1059
1380
  /**
1060
1381
  * Parse a JSONata expression into BuilderField array.
1061
1382
  * Only supports top-level object literals with simple path references or nested objects.
@@ -1066,7 +1387,7 @@ function parseJsonataToBuilder(expression) {
1066
1387
  return [];
1067
1388
  }
1068
1389
  try {
1069
- const ast = jsonata(expression).ast();
1390
+ const ast = jsonata$3(expression).ast();
1070
1391
  if (ast.type === 'unary' && ast.value === '{') {
1071
1392
  return parseObjectEntries(ast.lhs, expression);
1072
1393
  }
@@ -1212,6 +1533,23 @@ function formatFieldEntry(field, indent = ' ') {
1212
1533
  return `${indent}"${field.name}": ${value}`;
1213
1534
  }
1214
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
+ */
1215
1553
  class MappingBuilderComponent {
1216
1554
  expression = '';
1217
1555
  templateFields = [];
@@ -1413,6 +1751,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
1413
1751
  type: Output
1414
1752
  }] } });
1415
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
+ */
1416
1771
  class MappingPreviewComponent {
1417
1772
  epistolaPluginService;
1418
1773
  expression = '';
@@ -1547,7 +1902,7 @@ class MappingPreviewComponent {
1547
1902
  <strong>{{ missingRequired.join(', ') }}</strong>
1548
1903
  </div>
1549
1904
  </div>
1550
- `, 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" }] });
1551
1906
  }
1552
1907
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: MappingPreviewComponent, decorators: [{
1553
1908
  type: Component,
@@ -1618,7 +1973,227 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
1618
1973
  type: Input
1619
1974
  }] } });
1620
1975
 
1621
- 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;
1622
2197
  /**
1623
2198
  * Detect if a string value is a JSONata expression (vs a plain literal).
1624
2199
  * Checks for characters that indicate JSONata operators: $, &, (, {, ?, [
@@ -1652,7 +2227,11 @@ function expandDotNotation(flat) {
1652
2227
  * doc/case data.
1653
2228
  */
1654
2229
  function isOverrideDriven(mapping) {
1655
- return !!mapping && Object.keys(mapping).length > 0;
2230
+ if (!mapping)
2231
+ return false;
2232
+ if (typeof mapping === 'string')
2233
+ return mapping.trim().length > 0;
2234
+ return Object.keys(mapping).length > 0;
1656
2235
  }
1657
2236
  /**
1658
2237
  * Whether the computed input overrides carry any usable data yet.
@@ -1677,32 +2256,63 @@ function shouldLoadPreview(mapping, overrides) {
1677
2256
  return true;
1678
2257
  }
1679
2258
  /**
1680
- * Given an override mapping (scope -> { inputPath -> "form:<componentKey>" })
1681
- * and form data, produce the inputOverrides object for the backend.
1682
- * The "form:" prefix identifies form field references; the remainder is the Formio component key.
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.
1683
2268
  */
1684
- function computeInputOverrides(mapping, formData) {
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
+ }
1685
2289
  const result = {};
1686
- for (const [scope, fields] of Object.entries(mapping)) {
1687
- if (scope !== 'doc' && scope !== 'pv')
1688
- continue;
1689
- const flatOverrides = {};
1690
- for (const [inputPath, ref] of Object.entries(fields)) {
1691
- const formFieldKey = String(ref).startsWith(FORM_REF_PREFIX$1)
1692
- ? String(ref).substring(FORM_REF_PREFIX$1.length)
1693
- : String(ref);
1694
- const value = formData[formFieldKey];
1695
- if (value !== undefined) {
1696
- flatOverrides[inputPath] = value;
1697
- }
1698
- }
1699
- if (Object.keys(flatOverrides).length > 0) {
1700
- result[scope] = expandDotNotation(flatOverrides);
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;
1701
2294
  }
1702
2295
  }
1703
2296
  return result;
1704
2297
  }
1705
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
+ */
1706
2316
  class GenerateDocumentConfigurationComponent {
1707
2317
  epistolaPluginService;
1708
2318
  processLinkStateService;
@@ -1714,6 +2324,8 @@ class GenerateDocumentConfigurationComponent {
1714
2324
  selectedPluginConfigurationData$;
1715
2325
  context$;
1716
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.
1717
2329
  configuration = new EventEmitter();
1718
2330
  catalogs$ = new BehaviorSubject(initialResource([]));
1719
2331
  templates$ = new BehaviorSubject(initialResource([]));
@@ -1760,6 +2372,8 @@ class GenerateDocumentConfigurationComponent {
1760
2372
  processVariables = [];
1761
2373
  expressionFunctions = [];
1762
2374
  variableSuggestions = null;
2375
+ /** Context variables for the JSONata editor's autocomplete ($doc/$pv/$case). */
2376
+ editorContextVariables = { doc: [], pv: [], case: [] };
1763
2377
  prefillDataMapping = {};
1764
2378
  validationErrors$ = new BehaviorSubject([]);
1765
2379
  destroy$ = new Subject();
@@ -2073,6 +2687,12 @@ class GenerateDocumentConfigurationComponent {
2073
2687
  .pipe(takeUntil$1(this.destroy$), catchError(() => of({ doc: [], pv: [] })))
2074
2688
  .subscribe((suggestions) => {
2075
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
+ };
2076
2696
  this.cdr.markForCheck();
2077
2697
  });
2078
2698
  }
@@ -2165,8 +2785,8 @@ class GenerateDocumentConfigurationComponent {
2165
2785
  }
2166
2786
  });
2167
2787
  }
2168
- 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 });
2169
- 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 });
2170
2790
  }
2171
2791
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: GenerateDocumentConfigurationComponent, decorators: [{
2172
2792
  type: Component,
@@ -2181,8 +2801,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2181
2801
  JsonataEditorComponent,
2182
2802
  MappingBuilderComponent,
2183
2803
  MappingPreviewComponent,
2184
- ], 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"] }]
2185
- }], 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$: [{
2186
2806
  type: Input
2187
2807
  }], disabled$: [{
2188
2808
  type: Input
@@ -2200,12 +2820,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2200
2820
  type: Output
2201
2821
  }] } });
2202
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
+ */
2203
2840
  class CheckJobStatusConfigurationComponent {
2204
2841
  save$;
2205
2842
  disabled$;
2206
2843
  pluginId;
2207
2844
  prefillConfiguration$;
2208
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.
2209
2849
  configuration = new EventEmitter();
2210
2850
  saveSubscription;
2211
2851
  formValue$ = new BehaviorSubject(null);
@@ -2267,8 +2907,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2267
2907
  type: Output
2268
2908
  }] } });
2269
2909
 
2270
- const DEFAULT_STORAGE_TARGET = 'TEMPORARY_RESOURCE';
2271
- /** Normalize a (possibly undefined) storageTarget to a concrete target, applying the default. */
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. */
2272
2929
  function resolveStorageTarget(target) {
2273
2930
  return target === 'PROCESS_VARIABLE' ? 'PROCESS_VARIABLE' : DEFAULT_STORAGE_TARGET;
2274
2931
  }
@@ -2285,12 +2942,31 @@ function isDownloadDocumentConfigValid(config) {
2285
2942
  : !!config.resourceIdVariable;
2286
2943
  }
2287
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
+ */
2288
2962
  class DownloadDocumentConfigurationComponent {
2289
2963
  save$;
2290
2964
  disabled$;
2291
2965
  pluginId;
2292
2966
  prefillConfiguration$;
2293
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.
2294
2970
  configuration = new EventEmitter();
2295
2971
  saveSubscription;
2296
2972
  formValue$ = new BehaviorSubject(null);
@@ -2372,6 +3048,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2372
3048
  type: Output
2373
3049
  }] } });
2374
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
+ */
2375
3068
  /**
2376
3069
  * Unified Formio component for the after-generation Epistola PDF UX. Reads
2377
3070
  * the PDF id and tenant id from named process variables on the caller's
@@ -2530,7 +3223,7 @@ class EpistolaDocumentComponent {
2530
3223
  this.previewUrl = null;
2531
3224
  }
2532
3225
  }
2533
- 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: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
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 });
2534
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: `
2535
3228
  <!-- Design-time placeholder -->
2536
3229
  <div *ngIf="designMode" class="epistola-doc-panel">
@@ -2686,7 +3379,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2686
3379
  </div>
2687
3380
  </div>
2688
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"] }]
2689
- }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$4.DomSanitizer }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }], propDecorators: { value: [{
3382
+ }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$3.DomSanitizer }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }], propDecorators: { value: [{
2690
3383
  type: Input
2691
3384
  }], valueChange: [{
2692
3385
  type: Output
@@ -2706,6 +3399,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2706
3399
  type: Input
2707
3400
  }] } });
2708
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
+ */
2709
3419
  class EpistolaRetryFormComponent {
2710
3420
  epistolaPluginService;
2711
3421
  cdr;
@@ -2867,7 +3577,7 @@ class EpistolaRetryFormComponent {
2867
3577
  },
2868
3578
  });
2869
3579
  }
2870
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaRetryFormComponent, deps: [{ token: EpistolaPluginService }, { token: i0.ChangeDetectorRef }, { token: i2$4.DomSanitizer }], target: i0.ɵɵFactoryTarget.Component });
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 });
2871
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: `
2872
3582
  <div *ngIf="loading" class="epistola-retry-loading">Loading form...</div>
2873
3583
  <div *ngIf="error" class="epistola-retry-error">{{ error }}</div>
@@ -2949,7 +3659,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2949
3659
  </div>
2950
3660
  </div>
2951
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"] }]
2952
- }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i0.ChangeDetectorRef }, { type: i2$4.DomSanitizer }], propDecorators: { value: [{
3662
+ }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i0.ChangeDetectorRef }, { type: i2$3.DomSanitizer }], propDecorators: { value: [{
2953
3663
  type: Input
2954
3664
  }], valueChange: [{
2955
3665
  type: Output
@@ -2963,6 +3673,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2963
3673
  type: Input
2964
3674
  }] } });
2965
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
+ */
2966
3693
  class EpistolaDocumentPreviewComponent {
2967
3694
  epistolaPluginService;
2968
3695
  sanitizer;
@@ -2974,12 +3701,42 @@ class EpistolaDocumentPreviewComponent {
2974
3701
  label = 'Document Preview';
2975
3702
  processDefinitionKey;
2976
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
+ */
2977
3708
  overrideMapping;
2978
3709
  /**
2979
3710
  * Task id forwarded by the Formio wrapper from the server-prefilled form
2980
3711
  * ({@code epistola:taskId} value resolver), populated in every Valtimo task-open flow.
2981
3712
  */
2982
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;
2983
3740
  loading = false;
2984
3741
  error = null;
2985
3742
  previewUrl = null;
@@ -3001,16 +3758,21 @@ class EpistolaDocumentPreviewComponent {
3001
3758
  get currentTaskId() {
3002
3759
  return this.taskInstanceId ?? null;
3003
3760
  }
3004
- get overrideMappingScopes() {
3005
- return this.overrideMapping ? Object.keys(this.overrideMapping) : [];
3006
- }
3007
- overrideMappingEntries(scope) {
3008
- const fields = this.overrideMapping?.[scope];
3009
- if (!fields || typeof fields !== 'object')
3010
- return [];
3011
- 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);
3012
3770
  }
3013
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
+ }
3014
3776
  if (!this.initialized) {
3015
3777
  this.initialized = true;
3016
3778
  // Detect design mode: no runtime context (Formio builder)
@@ -3034,7 +3796,7 @@ class EpistolaDocumentPreviewComponent {
3034
3796
  // wrapper sets taskInstanceId after attach, so it can land after the first render —
3035
3797
  // re-run the preview once it does, instead of leaving the "only available from
3036
3798
  // within a user task" message until a manual refresh.
3037
- if (changes['value'] || changes['taskInstanceId']) {
3799
+ if (changes['inputOverrides'] || changes['taskInstanceId']) {
3038
3800
  this.triggerPreview();
3039
3801
  }
3040
3802
  }
@@ -3043,8 +3805,24 @@ class EpistolaDocumentPreviewComponent {
3043
3805
  this.revokeBlobUrl();
3044
3806
  }
3045
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
+ }
3046
3815
  this.triggerPreview();
3047
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
+ }
3048
3826
  /**
3049
3827
  * Load the preview only when there is enough data for it. Override-driven
3050
3828
  * previews (those with an override mapping) wait until the mapped form data
@@ -3052,7 +3830,7 @@ class EpistolaDocumentPreviewComponent {
3052
3830
  * request that Epistola would reject with a 400 for missing required fields.
3053
3831
  */
3054
3832
  triggerPreview() {
3055
- if (!shouldLoadPreview(this.overrideMapping, this.value)) {
3833
+ if (!shouldLoadPreview(this.overrideMapping, this.inputOverrides)) {
3056
3834
  this.showWaitingForInput();
3057
3835
  return;
3058
3836
  }
@@ -3094,7 +3872,7 @@ class EpistolaDocumentPreviewComponent {
3094
3872
  .previewToBlob({
3095
3873
  taskId,
3096
3874
  sourceActivityId: this.sourceActivityId,
3097
- inputOverrides: this.value || null,
3875
+ inputOverrides: this.inputOverrides || null,
3098
3876
  overrides: null,
3099
3877
  })
3100
3878
  .subscribe({
@@ -3137,8 +3915,8 @@ class EpistolaDocumentPreviewComponent {
3137
3915
  this.previewUrl = null;
3138
3916
  }
3139
3917
  }
3140
- 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 }], target: i0.ɵɵFactoryTarget.Component });
3141
- 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" }, 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: `
3142
3920
  <!-- Design-time view: show configuration summary when no runtime context -->
3143
3921
  <div *ngIf="designMode" class="epistola-preview-panel">
3144
3922
  <div class="preview-header">
@@ -3151,16 +3929,9 @@ class EpistolaDocumentPreviewComponent {
3151
3929
  <div class="design-label">Activity</div>
3152
3930
  <div class="design-value">{{ sourceActivityId }}</div>
3153
3931
  </div>
3154
- <div class="design-section" *ngIf="overrideMapping">
3155
- <div class="design-label">Input Overrides</div>
3156
- <div *ngFor="let scope of overrideMappingScopes" class="design-mapping">
3157
- <div *ngFor="let entry of overrideMappingEntries(scope)" class="design-entry">
3158
- <span class="design-scope">{{ scope }}</span
3159
- >.{{ entry.path }}
3160
- <i class="mdi mdi-arrow-left"></i>
3161
- <span class="design-field">{{ entry.field }}</span>
3162
- </div>
3163
- </div>
3932
+ <div class="design-section" *ngIf="overrideExpression">
3933
+ <div class="design-label">Input Overrides ($form)</div>
3934
+ <pre class="design-expression">{{ overrideExpression }}</pre>
3164
3935
  </div>
3165
3936
  <div *ngIf="!sourceActivityId" class="design-unconfigured">
3166
3937
  Auto-discover mode (no process link configured)
@@ -3173,6 +3944,18 @@ class EpistolaDocumentPreviewComponent {
3173
3944
  <div class="preview-header">
3174
3945
  <span>{{ label || 'Document Preview' }}</span>
3175
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>
3176
3959
  <button type="button" class="preview-refresh" [disabled]="loading" (click)="refresh()">
3177
3960
  <i class="mdi mdi-refresh mr-1"></i>
3178
3961
  {{ loading ? 'Generating...' : 'Refresh' }}
@@ -3195,7 +3978,7 @@ class EpistolaDocumentPreviewComponent {
3195
3978
  </object>
3196
3979
  </div>
3197
3980
  </div>
3198
- `, 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 });
3199
3982
  }
3200
3983
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDocumentPreviewComponent, decorators: [{
3201
3984
  type: Component,
@@ -3212,16 +3995,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3212
3995
  <div class="design-label">Activity</div>
3213
3996
  <div class="design-value">{{ sourceActivityId }}</div>
3214
3997
  </div>
3215
- <div class="design-section" *ngIf="overrideMapping">
3216
- <div class="design-label">Input Overrides</div>
3217
- <div *ngFor="let scope of overrideMappingScopes" class="design-mapping">
3218
- <div *ngFor="let entry of overrideMappingEntries(scope)" class="design-entry">
3219
- <span class="design-scope">{{ scope }}</span
3220
- >.{{ entry.path }}
3221
- <i class="mdi mdi-arrow-left"></i>
3222
- <span class="design-field">{{ entry.field }}</span>
3223
- </div>
3224
- </div>
3998
+ <div class="design-section" *ngIf="overrideExpression">
3999
+ <div class="design-label">Input Overrides ($form)</div>
4000
+ <pre class="design-expression">{{ overrideExpression }}</pre>
3225
4001
  </div>
3226
4002
  <div *ngIf="!sourceActivityId" class="design-unconfigured">
3227
4003
  Auto-discover mode (no process link configured)
@@ -3234,6 +4010,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3234
4010
  <div class="preview-header">
3235
4011
  <span>{{ label || 'Document Preview' }}</span>
3236
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>
3237
4025
  <button type="button" class="preview-refresh" [disabled]="loading" (click)="refresh()">
3238
4026
  <i class="mdi mdi-refresh mr-1"></i>
3239
4027
  {{ loading ? 'Generating...' : 'Refresh' }}
@@ -3256,8 +4044,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3256
4044
  </object>
3257
4045
  </div>
3258
4046
  </div>
3259
- `, 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"] }]
3260
- }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$4.DomSanitizer }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }], 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: [{
3261
4049
  type: Input
3262
4050
  }], valueChange: [{
3263
4051
  type: Output
@@ -3273,8 +4061,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3273
4061
  type: Input
3274
4062
  }], taskInstanceId: [{
3275
4063
  type: Input
4064
+ }], inputOverrides: [{
4065
+ type: Input
4066
+ }], requestOverrides: [{
4067
+ type: Input
4068
+ }], autoRefresh: [{
4069
+ type: Input
4070
+ }], setAutoRefresh: [{
4071
+ type: Input
3276
4072
  }] } });
3277
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
+ */
3278
4091
  class EpistolaAdminPageComponent {
3279
4092
  adminService;
3280
4093
  route;
@@ -3301,6 +4114,9 @@ class EpistolaAdminPageComponent {
3301
4114
  repairingFormIds = new Set();
3302
4115
  repairingAll = false;
3303
4116
  formFeedback = null;
4117
+ // TEMPORARY: forms still using the legacy override-mapping object format.
4118
+ legacyOverrideForms = null;
4119
+ legacyOverrideLoading = false;
3304
4120
  connectionStatuses = [];
3305
4121
  usageEntries = [];
3306
4122
  pendingJobs = [];
@@ -3321,6 +4137,14 @@ class EpistolaAdminPageComponent {
3321
4137
  get refreshIntervalMinutes() {
3322
4138
  return Math.round((this.validationReport?.refreshIntervalMs ?? 600000) / 60000);
3323
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
+ }
3324
4148
  ngOnInit() {
3325
4149
  this.deepLinkConfigId = this.route.snapshot.queryParamMap.get('configurationId');
3326
4150
  const tab = this.route.snapshot.queryParamMap.get('tab');
@@ -3356,6 +4180,23 @@ class EpistolaAdminPageComponent {
3356
4180
  if (tab === 'forms' && this.formIssues === null && !this.formIssuesLoading) {
3357
4181
  this.loadFormIssues();
3358
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
+ });
3359
4200
  }
3360
4201
  // ---- TEMPORARY (removed in 1.0.0): task-id carrier detection + repair ----
3361
4202
  loadFormIssues() {
@@ -3659,14 +4500,31 @@ class EpistolaAdminPageComponent {
3659
4500
  },
3660
4501
  });
3661
4502
  }
3662
- 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 });
3663
- 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=\"formIssues\"\n size=\"sm\"\n [type]=\"formIssues.length > 0 ? 'red' : 'gray'\"\n class=\"ms-1\"\n >{{ formIssues.length }}</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 </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: "pipe", type: i1$1.DatePipe, name: "date" }, { 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.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"] }] });
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"] }] });
3664
4505
  }
3665
4506
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminPageComponent, decorators: [{
3666
4507
  type: Component,
3667
- 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=\"formIssues\"\n size=\"sm\"\n [type]=\"formIssues.length > 0 ? 'red' : 'gray'\"\n class=\"ms-1\"\n >{{ formIssues.length }}</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 </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"] }]
3668
- }], 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 }] });
3669
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
+ */
3670
4528
  function isRuntimeWindow(value) {
3671
4529
  return typeof value === 'object' && value !== null;
3672
4530
  }
@@ -3696,12 +4554,46 @@ function isEpistolaEnabled() {
3696
4554
  return flag !== false && flag !== 'false';
3697
4555
  }
3698
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
+ */
3699
4574
  const epistolaEnabledGuard = () => {
3700
4575
  if (isEpistolaEnabled())
3701
4576
  return true;
3702
4577
  return inject(Router).parseUrl('/');
3703
4578
  };
3704
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
+ */
3705
4597
  const routes = [
3706
4598
  {
3707
4599
  path: 'epistola',
@@ -3712,7 +4604,7 @@ const routes = [
3712
4604
  ];
3713
4605
  class EpistolaAdminRoutingModule {
3714
4606
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
3715
- 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] });
3716
4608
  static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, imports: [RouterModule.forChild(routes), RouterModule] });
3717
4609
  }
3718
4610
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, decorators: [{
@@ -3723,6 +4615,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
3723
4615
  }]
3724
4616
  }] });
3725
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
+ */
3726
4635
  const EPISTOLA_DOCUMENT_OPTIONS = {
3727
4636
  type: 'epistola-document',
3728
4637
  selector: 'epistola-document-element',
@@ -3730,10 +4639,60 @@ const EPISTOLA_DOCUMENT_OPTIONS = {
3730
4639
  group: 'basic',
3731
4640
  icon: 'file-pdf-o',
3732
4641
  emptyValue: null,
3733
- 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'],
3734
4647
  // Embed the hidden task-id carrier so dropping this component is enough — no separate
3735
4648
  // field for the author to add. Valtimo prefills it server-side via the epistola: resolver.
3736
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
+ }),
3737
4696
  };
3738
4697
  function registerEpistolaDocumentComponent(injector) {
3739
4698
  if (customElements.get(EPISTOLA_DOCUMENT_OPTIONS.selector)) {
@@ -3763,6 +4722,23 @@ function registerEpistolaDocumentComponent(injector) {
3763
4722
  Formio.Components.setComponent(EPISTOLA_DOCUMENT_OPTIONS.type, EpistolaDocumentWithTaskContext);
3764
4723
  }
3765
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
+ */
3766
4742
  /**
3767
4743
  * Hides a registered custom Formio component from the builder's component palette,
3768
4744
  * while keeping it fully usable inside other components' `editForm`s and at runtime.
@@ -3786,6 +4762,23 @@ function hideFormioComponentFromBuilder(type) {
3786
4762
  }
3787
4763
  }
3788
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
+ */
3789
4782
  const EPISTOLA_RETRY_FORM_OPTIONS = {
3790
4783
  type: 'epistola-retry-form',
3791
4784
  selector: 'epistola-retry-form-element',
@@ -3829,6 +4822,25 @@ function registerEpistolaRetryFormComponent(injector) {
3829
4822
  hideFormioComponentFromBuilder(EPISTOLA_RETRY_FORM_OPTIONS.type);
3830
4823
  }
3831
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;
3832
4844
  const EPISTOLA_DOCUMENT_PREVIEW_OPTIONS = {
3833
4845
  type: 'epistola-document-preview',
3834
4846
  selector: 'epistola-document-preview-element',
@@ -3855,6 +4867,23 @@ const EPISTOLA_DOCUMENT_PREVIEW_OPTIONS = {
3855
4867
  label: 'Input Overrides',
3856
4868
  weight: 20,
3857
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
+ },
3858
4887
  ],
3859
4888
  }),
3860
4889
  };
@@ -3876,7 +4905,25 @@ function registerEpistolaDocumentPreviewComponent(injector) {
3876
4905
  _debounceTimer = null;
3877
4906
  _changeListenerAttached = false;
3878
4907
  _changeHandler = null;
4908
+ _blurHandler = null;
4909
+ _blurTarget = null;
3879
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;
3880
4927
  attach(element) {
3881
4928
  // Formio detaches and re-attaches components on every redraw — not only at
3882
4929
  // teardown — so a re-attach means the component is alive again. Clear the
@@ -3884,6 +4931,12 @@ function registerEpistolaDocumentPreviewComponent(injector) {
3884
4931
  // (genuine teardown, e.g. task completion), which is what suppresses the
3885
4932
  // post-submit preview.
3886
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
+ }
3887
4940
  // Bidirectional sync between processLinkSelection object and separate properties.
3888
4941
  // The editForm uses processLinkSelection (single field), while the component
3889
4942
  // config and Angular inputs use processDefinitionKey + sourceActivityId.
@@ -3909,15 +4962,65 @@ function registerEpistolaDocumentPreviewComponent(injector) {
3909
4962
  if (prefilledTaskId) {
3910
4963
  this._customAngularElement['taskInstanceId'] = prefilledTaskId;
3911
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
+ }
3912
4981
  }
3913
- // Listen to form changes and compute input overrides from the mapping
4982
+ // Compute input overrides from the mapping and wire up the live listeners.
3914
4983
  if (this.root && this.component?.overrideMapping && !this._changeListenerAttached) {
3915
4984
  this._changeListenerAttached = true;
3916
- this._changeHandler = () => this._computeAndSetOverrides();
3917
- this.root.on('change', this._changeHandler);
4985
+ this._debounceMs = this._resolveDebounceMs();
3918
4986
  // Compute the initial overrides immediately (no debounce) so a pre-filled
3919
- // form paints its preview without the 1.5s delay.
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.
3920
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
+ }
3921
5024
  }
3922
5025
  return result;
3923
5026
  }
@@ -3931,79 +5034,358 @@ function registerEpistolaDocumentPreviewComponent(injector) {
3931
5034
  clearTimeout(this._debounceTimer);
3932
5035
  this._debounceTimer = null;
3933
5036
  }
5037
+ this._initialPaintTimers.forEach((t) => clearTimeout(t));
5038
+ this._initialPaintTimers = [];
5039
+ this._hasUsableValue = false;
3934
5040
  if (this._changeHandler && this.root?.off) {
3935
5041
  this.root.off('change', this._changeHandler);
3936
5042
  this._changeHandler = null;
3937
5043
  }
5044
+ if (this._blurHandler && this._blurTarget?.removeEventListener) {
5045
+ this._blurTarget.removeEventListener('focusout', this._blurHandler);
5046
+ this._blurHandler = null;
5047
+ this._blurTarget = null;
5048
+ }
3938
5049
  this._changeListenerAttached = false;
5050
+ this._lastPushedJson = undefined;
3939
5051
  return super.detach();
3940
5052
  }
3941
5053
  _computeAndSetOverrides(immediate = false) {
3942
5054
  if (this._debounceTimer) {
3943
5055
  clearTimeout(this._debounceTimer);
3944
5056
  }
3945
- this._debounceTimer = setTimeout(() => {
3946
- // Skip if the form is being/has been submitted or the component is gone —
3947
- // those previews would run with incomplete/reset data and 400 from Epistola.
3948
- if (this._destroyed || this.root?.submitting || this.root?.submitted) {
3949
- return;
3950
- }
3951
- const mapping = this.component?.overrideMapping;
3952
- const formData = this.root?.data;
3953
- if (!mapping || !formData) {
3954
- return;
3955
- }
3956
- const overrides = computeInputOverrides(mapping, formData);
3957
- // Push null when there's nothing usable yet so the component reverts to
3958
- // its "complete the form" placeholder instead of keeping a stale preview.
3959
- this.setValue(Object.keys(overrides).length > 0 ? overrides : null);
3960
- }, immediate ? 0 : 1500);
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;
3961
5114
  }
3962
5115
  }
3963
5116
  // Re-register with the extended class
3964
5117
  Formio.Components.setComponent(EPISTOLA_DOCUMENT_PREVIEW_OPTIONS.type, PreviewWithOverrides);
3965
5118
  }
3966
5119
 
3967
- 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
+ */
3968
5270
  class EpistolaOverrideBuilderComponent {
3969
5271
  cdr;
5272
+ pluginService;
3970
5273
  value;
3971
5274
  valueChange = new EventEmitter();
3972
5275
  disabled = false;
3973
5276
  label = 'Input Overrides';
3974
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 = '';
3975
5285
  rows = [];
3976
5286
  advancedMode = false;
3977
- jsonText = '';
3978
- 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 } }';
3979
5293
  initialized = false;
3980
- 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) {
3981
5298
  this.cdr = cdr;
5299
+ this.pluginService = pluginService;
5300
+ }
5301
+ get formFieldKeys() {
5302
+ return this.availableFields.map((f) => f.key);
5303
+ }
5304
+ get hasReferencedPaths() {
5305
+ return this.referencedPaths.length > 0;
3982
5306
  }
3983
- ngOnChanges() {
3984
- if (!this.initialized && this.value) {
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) {
3985
5326
  this.initialized = true;
3986
- this.rows = this.mappingToRows(this.value);
3987
- 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);
3988
5338
  }
5339
+ this.refreshReferencedPaths();
3989
5340
  this.cdr.markForCheck();
3990
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
+ }
3991
5375
  toggleMode() {
3992
- this.advancedMode = !this.advancedMode;
3993
5376
  if (this.advancedMode) {
3994
- const mapping = this.rowsToMapping();
3995
- this.jsonText = Object.keys(mapping).length > 0 ? JSON.stringify(mapping, null, 2) : '';
3996
- 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;
3997
5386
  }
3998
5387
  else {
3999
- try {
4000
- const parsed = this.jsonText.trim() ? JSON.parse(this.jsonText) : {};
4001
- this.rows = this.mappingToRows(parsed);
4002
- this.jsonError = null;
4003
- }
4004
- catch {
4005
- // Keep current rows if JSON is invalid
4006
- }
5388
+ this.advancedMode = true;
4007
5389
  }
4008
5390
  }
4009
5391
  addRow() {
@@ -4013,65 +5395,105 @@ class EpistolaOverrideBuilderComponent {
4013
5395
  this.rows.splice(index, 1);
4014
5396
  this.emitChange();
4015
5397
  }
5398
+ /** Simple-table change: serialize rows back to a JSONata expression. */
4016
5399
  emitChange() {
4017
- const mapping = this.rowsToMapping();
4018
- this.value = Object.keys(mapping).length > 0 ? mapping : null;
4019
- this.valueChange.emit(this.value);
4020
- }
4021
- onJsonChange(text) {
4022
- this.jsonText = text;
4023
- if (!text.trim()) {
4024
- this.jsonError = null;
4025
- this.value = null;
4026
- this.valueChange.emit(null);
4027
- return;
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 = [];
4028
5416
  }
4029
- try {
4030
- const parsed = JSON.parse(text);
4031
- this.jsonError = null;
4032
- this.value = parsed;
4033
- this.valueChange.emit(parsed);
4034
- }
4035
- catch (e) {
4036
- this.jsonError = 'Invalid JSON';
5417
+ else {
5418
+ this.simpleUnavailable = false;
5419
+ this.rows = parsed;
4037
5420
  }
4038
5421
  }
4039
- rowsToMapping() {
4040
- const mapping = {};
4041
- for (const row of this.rows) {
4042
- if (row.inputPath && row.formFieldKey) {
4043
- if (!mapping[row.scope]) {
4044
- mapping[row.scope] = {};
4045
- }
4046
- mapping[row.scope][row.inputPath] = FORM_REF_PREFIX + row.formFieldKey;
4047
- }
4048
- }
4049
- return mapping;
4050
- }
4051
- mappingToRows(mapping) {
4052
- const rows = [];
4053
- for (const [scope, fields] of Object.entries(mapping)) {
4054
- if (scope === 'doc' || scope === 'pv') {
4055
- for (const [path, ref] of Object.entries(fields)) {
4056
- const formFieldKey = String(ref).startsWith(FORM_REF_PREFIX)
4057
- ? String(ref).substring(FORM_REF_PREFIX.length)
4058
- : String(ref);
4059
- rows.push({ scope, inputPath: path, formFieldKey });
4060
- }
4061
- }
4062
- }
4063
- return rows;
5422
+ emit(expression) {
5423
+ const next = expression && expression.trim() ? expression : null;
5424
+ this.value = next;
5425
+ this.valueChange.emit(next);
4064
5426
  }
4065
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaOverrideBuilderComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
4066
- 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: `
4067
5429
  <div class="override-builder">
4068
5430
  <div class="builder-header">
4069
5431
  <span class="builder-label">{{ label || 'Input Overrides' }}</span>
4070
- <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
+ >
4071
5438
  {{ advancedMode ? 'Simple' : 'Advanced' }}
4072
5439
  </button>
4073
5440
  </div>
4074
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
+
4075
5497
  <!-- Simple mode: table -->
4076
5498
  <div *ngIf="!advancedMode" class="builder-table">
4077
5499
  <div *ngIf="rows.length > 0" class="table-header">
@@ -4090,6 +5512,7 @@ class EpistolaOverrideBuilderComponent {
4090
5512
  type="text"
4091
5513
  [(ngModel)]="row.inputPath"
4092
5514
  (ngModelChange)="emitChange()"
5515
+ [attr.list]="'epistola-override-paths-' + row.scope"
4093
5516
  placeholder="e.g. beslissing.tekst"
4094
5517
  />
4095
5518
  <!-- Dropdown when form fields are available, text input as fallback -->
@@ -4121,31 +5544,96 @@ class EpistolaOverrideBuilderComponent {
4121
5544
  </button>
4122
5545
  </div>
4123
5546
 
4124
- <!-- Advanced mode: JSON editor -->
5547
+ <!-- Advanced mode: JSONata editor over $form -->
4125
5548
  <div *ngIf="advancedMode" class="builder-advanced">
4126
- <textarea
4127
- class="json-editor"
4128
- [ngModel]="jsonText"
4129
- (ngModelChange)="onJsonChange($event)"
4130
- placeholder='{ "pv": { "motivation": "form:pv:motivation" } }'
4131
- rows="6"
4132
- ></textarea>
4133
- <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>
4134
5562
  </div>
4135
5563
  </div>
4136
- `, 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 });
4137
5565
  }
4138
5566
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaOverrideBuilderComponent, decorators: [{
4139
5567
  type: Component,
4140
- 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: `
4141
5569
  <div class="override-builder">
4142
5570
  <div class="builder-header">
4143
5571
  <span class="builder-label">{{ label || 'Input Overrides' }}</span>
4144
- <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
+ >
4145
5578
  {{ advancedMode ? 'Simple' : 'Advanced' }}
4146
5579
  </button>
4147
5580
  </div>
4148
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
+
4149
5637
  <!-- Simple mode: table -->
4150
5638
  <div *ngIf="!advancedMode" class="builder-table">
4151
5639
  <div *ngIf="rows.length > 0" class="table-header">
@@ -4164,6 +5652,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
4164
5652
  type="text"
4165
5653
  [(ngModel)]="row.inputPath"
4166
5654
  (ngModelChange)="emitChange()"
5655
+ [attr.list]="'epistola-override-paths-' + row.scope"
4167
5656
  placeholder="e.g. beslissing.tekst"
4168
5657
  />
4169
5658
  <!-- Dropdown when form fields are available, text input as fallback -->
@@ -4195,20 +5684,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
4195
5684
  </button>
4196
5685
  </div>
4197
5686
 
4198
- <!-- Advanced mode: JSON editor -->
5687
+ <!-- Advanced mode: JSONata editor over $form -->
4199
5688
  <div *ngIf="advancedMode" class="builder-advanced">
4200
- <textarea
4201
- class="json-editor"
4202
- [ngModel]="jsonText"
4203
- (ngModelChange)="onJsonChange($event)"
4204
- placeholder='{ "pv": { "motivation": "form:pv:motivation" } }'
4205
- rows="6"
4206
- ></textarea>
4207
- <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>
4208
5702
  </div>
4209
5703
  </div>
4210
- `, 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"] }]
4211
- }], 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: [{
4212
5706
  type: Input
4213
5707
  }], valueChange: [{
4214
5708
  type: Output
@@ -4218,8 +5712,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
4218
5712
  type: Input
4219
5713
  }], availableFields: [{
4220
5714
  type: Input
5715
+ }], processDefinitionKey: [{
5716
+ type: Input
5717
+ }], sourceActivityId: [{
5718
+ type: Input
4221
5719
  }] } });
4222
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
+ */
4223
5738
  const EPISTOLA_OVERRIDE_BUILDER_OPTIONS = {
4224
5739
  type: 'epistola-override-builder',
4225
5740
  selector: 'epistola-override-builder-element',
@@ -4227,7 +5742,7 @@ const EPISTOLA_OVERRIDE_BUILDER_OPTIONS = {
4227
5742
  group: 'basic',
4228
5743
  icon: 'list',
4229
5744
  emptyValue: null,
4230
- fieldOptions: ['label', 'availableFields'],
5745
+ fieldOptions: ['label', 'availableFields', 'processDefinitionKey', 'sourceActivityId'],
4231
5746
  };
4232
5747
  /**
4233
5748
  * Recursively collect input field keys and labels from a Formio component tree.
@@ -4265,12 +5780,45 @@ function registerEpistolaOverrideBuilderComponent(injector) {
4265
5780
  const BaseComponent = Formio.Components.components[EPISTOLA_OVERRIDE_BUILDER_OPTIONS.type];
4266
5781
  if (!BaseComponent)
4267
5782
  return;
4268
- // 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.
4269
5785
  class OverrideBuilderWithFields extends BaseComponent {
5786
+ _selectionChangeHandler = null;
4270
5787
  attach(element) {
4271
- // Set form fields on the component BEFORE super.attach() reads fieldOptions
5788
+ // Set inputs on the component BEFORE super.attach() reads fieldOptions.
4272
5789
  this.component.availableFields = this._extractFormFields();
4273
- 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 || '';
4274
5822
  }
4275
5823
  _extractFormFields() {
4276
5824
  // The Formio builder passes the main form schema as options.editForm
@@ -4288,6 +5836,23 @@ function registerEpistolaOverrideBuilderComponent(injector) {
4288
5836
  hideFormioComponentFromBuilder(EPISTOLA_OVERRIDE_BUILDER_OPTIONS.type);
4289
5837
  }
4290
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
+ */
4291
5856
  /**
4292
5857
  * The plugin action definition key the backend serializes for generate-document
4293
5858
  * process links. It carries the `epistola-` prefix — see `EPISTOLA_ACTION_KEYS`
@@ -4300,6 +5865,23 @@ function filterGenerateDocumentEntries(entries) {
4300
5865
  return entries.filter((e) => e.actionKey === GENERATE_DOCUMENT_ACTION_KEY);
4301
5866
  }
4302
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
+ */
4303
5885
  class EpistolaProcessLinkSelectorComponent {
4304
5886
  adminService;
4305
5887
  cdr;
@@ -4382,7 +5964,7 @@ class EpistolaProcessLinkSelectorComponent {
4382
5964
  </select>
4383
5965
  <div *ngIf="error" class="selector-error">{{ error }}</div>
4384
5966
  </div>
4385
- `, 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 });
4386
5968
  }
4387
5969
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaProcessLinkSelectorComponent, decorators: [{
4388
5970
  type: Component,
@@ -4413,6 +5995,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
4413
5995
  type: Input
4414
5996
  }] } });
4415
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
+ */
4416
6015
  const EPISTOLA_PROCESS_LINK_SELECTOR_OPTIONS = {
4417
6016
  type: 'epistola-process-link-selector',
4418
6017
  selector: 'epistola-process-link-selector-element',
@@ -4430,6 +6029,23 @@ function registerEpistolaProcessLinkSelectorComponent(injector) {
4430
6029
  }
4431
6030
  }
4432
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
+ */
4433
6049
  class EpistolaPluginModule {
4434
6050
  // Kept for back-compat with hosts that follow the README's `forRoot()`
4435
6051
  // setup. The providers above are now module-level so `imports: [EpistolaPluginModule]`
@@ -4550,8 +6166,60 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
4550
6166
  }]
4551
6167
  }] });
4552
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
+ */
4553
6186
  const EPISTOLA_PLUGIN_LOGO_BASE64 = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAgMTIwIiB3aWR0aD0iMTIwIiBoZWlnaHQ9IjEyMCI+CiAgPCEtLSBTdGFjayBiYXNlIC0tPgogIDxyZWN0IHg9IjM2IiB5PSIxNiIgd2lkdGg9IjU0IiBoZWlnaHQ9IjcwIiByeD0iMyIgZmlsbD0iI2U2YzJiMCIgc3Ryb2tlPSIjNGYyZjJiIiBzdHJva2Utd2lkdGg9IjIiIHRyYW5zZm9ybT0icm90YXRlKDUgNjMgNTEpIi8+CiAgPHJlY3QgeD0iMzIiIHk9IjIyIiB3aWR0aD0iNTQiIGhlaWdodD0iNzAiIHJ4PSIzIiBmaWxsPSIjZjBkOGM4IiBzdHJva2U9IiM0ZjJmMmIiIHN0cm9rZS13aWR0aD0iMiIvPgogIDxyZWN0IHg9IjI4IiB5PSIyOCIgd2lkdGg9IjU0IiBoZWlnaHQ9IjcwIiByeD0iMyIgZmlsbD0iI2Y1ZWJlMyIgc3Ryb2tlPSIjNGYyZjJiIiBzdHJva2Utd2lkdGg9IjIuNSIvPgogIDxsaW5lIHgxPSIzOCIgeTE9IjQ0IiB4Mj0iNzIiIHkyPSI0NCIgc3Ryb2tlPSIjYzRhODgyIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgogIDxsaW5lIHgxPSIzOCIgeTE9IjU0IiB4Mj0iNzIiIHkyPSI1NCIgc3Ryb2tlPSIjYzRhODgyIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgogIDxsaW5lIHgxPSIzOCIgeTE9IjY0IiB4Mj0iNTgiIHkyPSI2NCIgc3Ryb2tlPSIjYzRhODgyIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgogIDwhLS0gV2F4IHNlYWwgd2l0aCBjaGVja21hcmsgLS0+CiAgPGNpcmNsZSBjeD0iNTUiIGN5PSI4NCIgcj0iMTUiIGZpbGw9IiNiODVjM2MiIHN0cm9rZT0iIzRmMmYyYiIgc3Ryb2tlLXdpZHRoPSIyIi8+CiAgPGNpcmNsZSBjeD0iNTUiIGN5PSI4NCIgcj0iMTAuNSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZDQ4MzZhIiBzdHJva2Utd2lkdGg9IjEiLz4KICA8cG9seWxpbmUgcG9pbnRzPSI0OSw4NCA1Myw4OSA2Miw3OCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNGYyZjJiIiBzdHJva2Utd2lkdGg9IjIuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo=';
4554
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
+ */
4555
6223
  const EPISTOLA_PLUGIN_ID = 'epistola';
4556
6224
  const DISABLED_EPISTOLA_PLUGIN_ID = '__epistola_disabled__';
4557
6225
  const epistolaPluginSpecification = {
@@ -4707,6 +6375,10 @@ const epistolaPluginSpecification = {
4707
6375
  epistolaAdminReconcile: 'Hersynchroniseer',
4708
6376
  epistolaAdminReconciling: 'Bezig...',
4709
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',
4710
6382
  epistolaAdminConfigurations: 'Configuraties',
4711
6383
  epistolaAdminValidations: 'BPMN-validatie',
4712
6384
  epistolaAdminNoValidations: 'Geen race-onveilige procesdefinities gevonden. Alles ziet er goed uit.',
@@ -4745,6 +6417,11 @@ const epistolaPluginSpecification = {
4745
6417
  epistolaAdminRepairing: 'Bezig...',
4746
6418
  epistolaAdminRepairTooltip: 'Voeg het verborgen task-id veld toe aan alle Epistola-componenten in dit formulier.',
4747
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.',
4748
6425
  },
4749
6426
  en: {
4750
6427
  title: 'Epistola Document Suite',
@@ -4883,6 +6560,10 @@ const epistolaPluginSpecification = {
4883
6560
  epistolaAdminReconcile: 'Reconcile',
4884
6561
  epistolaAdminReconciling: 'Reconciling...',
4885
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',
4886
6567
  epistolaAdminConfigurations: 'Configurations',
4887
6568
  epistolaAdminValidations: 'BPMN validation',
4888
6569
  epistolaAdminNoValidations: 'No race-unsafe process definitions detected. Everything looks good.',
@@ -4921,10 +6602,32 @@ const epistolaPluginSpecification = {
4921
6602
  epistolaAdminRepairing: 'Working...',
4922
6603
  epistolaAdminRepairTooltip: 'Add the hidden task-id field to all Epistola components in this form.',
4923
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.',
4924
6610
  },
4925
6611
  },
4926
6612
  };
4927
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
+ */
4928
6631
  /*
4929
6632
  * Public API Surface of epistola plugin
4930
6633
  */