@epistola.app/valtimo-plugin 0.5.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/fesm2022/epistola.app-valtimo-plugin.mjs +2531 -561
  2. package/fesm2022/epistola.app-valtimo-plugin.mjs.map +1 -1
  3. package/lib/assets/epistola-logo.d.ts +1 -1
  4. package/lib/components/epistola-admin-page/epistola-admin-page.component.d.ts +52 -0
  5. package/lib/components/epistola-document-preview/epistola-document-preview.component.d.ts +28 -5
  6. package/lib/components/epistola-document-preview/preview-utils.d.ts +18 -0
  7. package/lib/components/expected-structure/expected-structure.component.d.ts +11 -0
  8. package/lib/components/generate-document-configuration/generate-document-configuration.component.d.ts +29 -3
  9. package/lib/components/jsonata-editor/jsonata-editor.component.d.ts +29 -0
  10. package/lib/components/mapping-builder/builder-field/builder-field.component.d.ts +21 -0
  11. package/lib/components/mapping-builder/mapping-builder.component.d.ts +37 -0
  12. package/lib/components/mapping-preview/mapping-preview.component.d.ts +26 -0
  13. package/lib/components/override-builder/override-builder.component.d.ts +42 -0
  14. package/lib/components/override-builder/override-builder.formio.d.ts +4 -0
  15. package/lib/components/process-link-selector/process-link-selector.component.d.ts +31 -0
  16. package/lib/components/process-link-selector/process-link-selector.formio.d.ts +4 -0
  17. package/lib/epistola-admin-routing.module.d.ts +7 -0
  18. package/lib/epistola-enabled.guard.d.ts +2 -0
  19. package/lib/epistola-runtime-config.d.ts +28 -0
  20. package/lib/epistola.module.d.ts +11 -14
  21. package/lib/models/admin.d.ts +49 -0
  22. package/lib/models/config.d.ts +30 -2
  23. package/lib/models/expression.d.ts +13 -0
  24. package/lib/models/index.d.ts +2 -0
  25. package/lib/models/template.d.ts +0 -6
  26. package/lib/services/epistola-admin.service.d.ts +37 -0
  27. package/lib/services/epistola-menu.service.d.ts +13 -0
  28. package/lib/services/epistola-plugin.service.d.ts +16 -3
  29. package/lib/services/index.d.ts +2 -0
  30. package/lib/utils/jsonata-converter.d.ts +26 -0
  31. package/lib/utils/jsonata-monaco.d.ts +14 -0
  32. package/package.json +10 -6
  33. package/public_api.d.ts +9 -5
  34. package/lib/components/array-field/array-field.component.d.ts +0 -27
  35. package/lib/components/data-mapping-tree/data-mapping-tree.component.d.ts +0 -27
  36. package/lib/components/field-tree/field-tree.component.d.ts +0 -27
  37. package/lib/components/scalar-field/scalar-field.component.d.ts +0 -16
  38. package/lib/components/value-input/value-input.component.d.ts +0 -34
  39. package/lib/utils/template-field-utils.d.ts +0 -6
@@ -1,22 +1,31 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, EventEmitter, Output, Input, Component, ChangeDetectionStrategy, forwardRef, ENVIRONMENT_INITIALIZER, inject, Injector, NgModule } from '@angular/core';
2
+ import { Injectable, EventEmitter, Output, Input, Component, ChangeDetectionStrategy, inject, NgModule, ENVIRONMENT_INITIALIZER, Injector } from '@angular/core';
3
3
  import * as i1 from '@angular/common/http';
4
4
  import { HttpHeaders, HttpClientModule } from '@angular/common/http';
5
5
  import * as i2 from '@valtimo/shared';
6
+ import { ROLE_ADMIN } from '@valtimo/shared';
7
+ import { of, BehaviorSubject, combineLatest, take, Subject, debounceTime, takeUntil, merge } from 'rxjs';
8
+ import * as i3 from '@valtimo/components';
9
+ import { FormModule, InputModule, EditorModule, SelectModule, registerCustomFormioComponent } from '@valtimo/components';
6
10
  import * as i1$1 from '@angular/common';
7
11
  import { CommonModule } from '@angular/common';
8
12
  import * as i2$1 from '@valtimo/plugin';
9
13
  import { PluginTranslatePipeModule } from '@valtimo/plugin';
10
- import * as i3 from '@valtimo/components';
11
- import { FormModule, InputModule, ValuePathSelectorPrefix, ValuePathSelectorComponent, SelectModule, registerCustomFormioComponent } from '@valtimo/components';
12
- import { BehaviorSubject, combineLatest, take, Subject, of, merge } from 'rxjs';
13
- import { startWith, delay, shareReplay, take as take$1, takeUntil, filter, map, distinctUntilChanged, tap, switchMap, catchError, debounceTime } from 'rxjs/operators';
14
- import * as i4 from '@angular/forms';
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
16
  import { FormsModule } from '@angular/forms';
16
- import * as i2$2 from '@valtimo/process-link';
17
+ import * as _jsonata from 'jsonata';
18
+ import * as i2$3 from '@valtimo/process-link';
17
19
  import * as i7 from '@formio/angular';
18
20
  import { FormioModule } from '@formio/angular';
19
- import * as i4$1 from '@angular/platform-browser';
21
+ import * as i4 from '@angular/platform-browser';
22
+ import * as i2$4 from '@angular/router';
23
+ import { RouterModule, Router } from '@angular/router';
24
+ import * as i5 from 'carbon-components-angular/tabs';
25
+ import { TabsModule } from 'carbon-components-angular/tabs';
26
+ import * as i6 from 'carbon-components-angular/tag';
27
+ import { TagModule } from 'carbon-components-angular/tag';
28
+ import { AuthGuardService } from '@valtimo/security';
20
29
 
21
30
  function initialResource(empty) {
22
31
  return { data: empty, loading: false, error: null };
@@ -31,6 +40,94 @@ function errorResource(current, error) {
31
40
  return { data: current, loading: false, error };
32
41
  }
33
42
 
43
+ /**
44
+ * Service for Epistola plugin administrative operations.
45
+ * Provides health checks, version info, and usage overview.
46
+ */
47
+ class EpistolaAdminService {
48
+ http;
49
+ configService;
50
+ apiEndpoint;
51
+ constructor(http, configService) {
52
+ this.http = http;
53
+ this.configService = configService;
54
+ this.apiEndpoint = `${this.configService.config.valtimoApi.endpointUri}v1/plugin/epistola/admin`;
55
+ }
56
+ /**
57
+ * Check connectivity to Epistola for all plugin configurations.
58
+ */
59
+ getConnectionStatus() {
60
+ return this.http.get(`${this.apiEndpoint}/health`);
61
+ }
62
+ /**
63
+ * Get version information for the plugin and connected Epistola server.
64
+ */
65
+ getVersions() {
66
+ return this.http.get(`${this.apiEndpoint}/versions`);
67
+ }
68
+ /**
69
+ * Get an overview of all Epistola plugin usages across process definitions.
70
+ */
71
+ getPluginUsage() {
72
+ return this.http.get(`${this.apiEndpoint}/usage`);
73
+ }
74
+ /**
75
+ * Get all process instances currently waiting for an Epistola document generation result.
76
+ */
77
+ getPendingJobs() {
78
+ return this.http.get(`${this.apiEndpoint}/pending`);
79
+ }
80
+ /**
81
+ * Export a single process link as a .process-link.json file.
82
+ */
83
+ exportProcessLink(processLinkId) {
84
+ return this.http.get(`${this.apiEndpoint}/export/${encodeURIComponent(processLinkId)}`, {
85
+ responseType: 'blob',
86
+ });
87
+ }
88
+ 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 });
89
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminService });
90
+ }
91
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminService, decorators: [{
92
+ type: Injectable
93
+ }], ctorParameters: () => [{ type: i1.HttpClient }, { type: i2.ConfigService }] });
94
+
95
+ /**
96
+ * Registers the Epistola admin page menu item under the Admin > Other section.
97
+ * Instantiated eagerly via ENVIRONMENT_INITIALIZER so the menu item
98
+ * appears without any manual configuration in the host application.
99
+ */
100
+ class EpistolaMenuService {
101
+ menuService;
102
+ constructor(menuService) {
103
+ this.menuService = menuService;
104
+ this.menuService.registerAppendMenuItemsFunction((items) => {
105
+ return of(items.map((item) => {
106
+ const isAdminMenu = item.roles?.includes(ROLE_ADMIN) && item.children;
107
+ if (!isAdminMenu) {
108
+ return item;
109
+ }
110
+ return {
111
+ ...item,
112
+ children: [
113
+ ...item.children,
114
+ {
115
+ link: ['/epistola'],
116
+ title: 'Epistola',
117
+ sequence: 18,
118
+ },
119
+ ],
120
+ };
121
+ }));
122
+ });
123
+ }
124
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaMenuService, deps: [{ token: i3.MenuService }], target: i0.ɵɵFactoryTarget.Injectable });
125
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaMenuService });
126
+ }
127
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaMenuService, decorators: [{
128
+ type: Injectable
129
+ }], ctorParameters: () => [{ type: i3.MenuService }] });
130
+
34
131
  /**
35
132
  * Service for interacting with Epistola plugin API endpoints.
36
133
  * Provides methods to fetch templates, environments, variants,
@@ -94,13 +191,32 @@ class EpistolaPluginService {
94
191
  * Discover process variable names for a given process definition.
95
192
  */
96
193
  getProcessVariables(processDefinitionKey) {
97
- return this.http.get(`${this.apiEndpoint}/process-variables`, { params: { processDefinitionKey } });
194
+ return this.http.get(`${this.apiEndpoint}/process-variables`, {
195
+ params: { processDefinitionKey },
196
+ });
197
+ }
198
+ /**
199
+ * Get variable suggestions for JSONata autocompletion.
200
+ */
201
+ getVariableSuggestions(caseDefinitionKey, processDefinitionKey) {
202
+ const params = {};
203
+ if (caseDefinitionKey)
204
+ params['caseDefinitionKey'] = caseDefinitionKey;
205
+ if (processDefinitionKey)
206
+ params['processDefinitionKey'] = processDefinitionKey;
207
+ return this.http.get(`${this.apiEndpoint}/variable-suggestions`, {
208
+ params,
209
+ });
98
210
  }
99
211
  /**
100
- * Validate that a data mapping covers all required template fields.
212
+ * Evaluate a JSONata expression against a real document.
101
213
  */
102
- validateMapping(pluginConfigurationId, templateId, dataMapping) {
103
- return this.http.post(`${this.apiEndpoint}/configurations/${pluginConfigurationId}/templates/${templateId}/validate-mapping`, { dataMapping });
214
+ evaluateMapping(expression, documentId, processInstanceId) {
215
+ return this.http.post(`${this.apiEndpoint}/evaluate-mapping`, {
216
+ expression,
217
+ documentId,
218
+ processInstanceId: processInstanceId ?? null,
219
+ });
104
220
  }
105
221
  /**
106
222
  * Get a dynamically generated Formio form for retrying a failed document generation.
@@ -115,11 +231,26 @@ class EpistolaPluginService {
115
231
  }
116
232
  return this.http.get(`${this.apiEndpoint}/retry-form`, { params });
117
233
  }
234
+ /**
235
+ * List all available expression functions for expr: data mapping values.
236
+ */
237
+ getExpressionFunctions() {
238
+ return this.http.get(`${this.apiEndpoint}/expression-functions`);
239
+ }
240
+ /**
241
+ * Validate the JSONata syntax of action-config expressions before save.
242
+ * Parse-only; runtime errors (missing variables, type mismatches) are not detected.
243
+ */
244
+ validateJsonata(request) {
245
+ return this.http.post(`${this.apiEndpoint}/validate-jsonata`, request);
246
+ }
118
247
  /**
119
248
  * Discover all previewable document sources for a given Valtimo document.
120
249
  */
121
250
  getPreviewSources(documentId) {
122
- return this.http.get(`${this.apiEndpoint}/preview-sources`, { params: { documentId } });
251
+ return this.http.get(`${this.apiEndpoint}/preview-sources`, {
252
+ params: { documentId },
253
+ });
123
254
  }
124
255
  /**
125
256
  * Preview a document by dry-running the generate-document process link.
@@ -131,7 +262,7 @@ class EpistolaPluginService {
131
262
  processDefinitionKey,
132
263
  sourceActivityId,
133
264
  processInstanceId: processInstanceId || null,
134
- overrides: overrides || null
265
+ overrides: overrides || null,
135
266
  });
136
267
  }
137
268
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaPluginService, deps: [{ token: i1.HttpClient }, { token: i2.ConfigService }], target: i0.ɵɵFactoryTarget.Injectable });
@@ -202,11 +333,11 @@ class EpistolaConfigurationComponent {
202
333
  });
203
334
  }
204
335
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaConfigurationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
205
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaConfigurationComponent, isStandalone: true, selector: "epistola-configuration", inputs: { save$: "save$", disabled$: "disabled$", pluginId: "pluginId", prefillConfiguration$: "prefillConfiguration$" }, outputs: { valid: "valid", configuration: "configuration" }, ngImport: i0, template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: safeDisabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null\n } as obs\"\n>\n <v-input\n name=\"configurationTitle\"\n [title]=\"'configurationTitle' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.configurationTitle\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"baseUrl\"\n [title]=\"'baseUrl' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'baseUrlTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.baseUrl\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"apiKey\"\n [title]=\"'apiKey' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'apiKeyTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.apiKey\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n type=\"password\"\n >\n </v-input>\n\n <v-input\n name=\"tenantId\"\n [title]=\"'tenantId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'tenantIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.tenantId\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"defaultEnvironmentId\"\n [title]=\"'defaultEnvironmentId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'defaultEnvironmentIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.defaultEnvironmentId\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"templateSyncEnabled\"\n type=\"checkbox\"\n [title]=\"'templateSyncEnabled' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'templateSyncEnabledTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.templateSyncEnabled ? 'true' : ''\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n</v-form>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: FormModule }, { kind: "component", type: i3.FormComponent, selector: "v-form", inputs: ["className"], outputs: ["valueChange"] }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i3.InputComponent, selector: "v-input", inputs: ["name", "type", "title", "titleTranslationKey", "defaultValue", "widthPx", "fullWidth", "margin", "smallMargin", "disabled", "step", "min", "maxLength", "tooltip", "required", "hideNumberSpinBox", "smallLabel", "rows", "clear$", "carbonTheme", "placeholder", "dataTestId", "trim", "presetsTitle", "presetOptions"], outputs: ["valueChange"] }] });
336
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaConfigurationComponent, isStandalone: true, selector: "epistola-configuration", inputs: { save$: "save$", disabled$: "disabled$", pluginId: "pluginId", prefillConfiguration$: "prefillConfiguration$" }, outputs: { valid: "valid", configuration: "configuration" }, ngImport: i0, template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: safeDisabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n } as obs\"\n>\n <v-input\n name=\"configurationTitle\"\n [title]=\"'configurationTitle' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.configurationTitle\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"baseUrl\"\n [title]=\"'baseUrl' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'baseUrlTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.baseUrl\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"apiKey\"\n [title]=\"'apiKey' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'apiKeyTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.apiKey\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n type=\"password\"\n >\n </v-input>\n\n <v-input\n name=\"tenantId\"\n [title]=\"'tenantId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'tenantIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.tenantId\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"defaultEnvironmentId\"\n [title]=\"'defaultEnvironmentId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'defaultEnvironmentIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.defaultEnvironmentId\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"templateSyncEnabled\"\n type=\"checkbox\"\n [title]=\"'templateSyncEnabled' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'templateSyncEnabledTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.templateSyncEnabled ? 'true' : ''\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n</v-form>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: FormModule }, { kind: "component", type: i3.FormComponent, selector: "v-form", inputs: ["className"], outputs: ["valueChange"] }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i3.InputComponent, selector: "v-input", inputs: ["name", "type", "title", "titleTranslationKey", "defaultValue", "widthPx", "fullWidth", "margin", "smallMargin", "disabled", "step", "min", "maxLength", "tooltip", "required", "hideNumberSpinBox", "smallLabel", "rows", "clear$", "carbonTheme", "placeholder", "dataTestId", "trim", "presetsTitle", "presetOptions"], outputs: ["valueChange"] }] });
206
337
  }
207
338
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaConfigurationComponent, decorators: [{
208
339
  type: Component,
209
- args: [{ selector: 'epistola-configuration', standalone: true, imports: [CommonModule, PluginTranslatePipeModule, FormModule, InputModule], template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: safeDisabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null\n } as obs\"\n>\n <v-input\n name=\"configurationTitle\"\n [title]=\"'configurationTitle' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.configurationTitle\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"baseUrl\"\n [title]=\"'baseUrl' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'baseUrlTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.baseUrl\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"apiKey\"\n [title]=\"'apiKey' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'apiKeyTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.apiKey\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n type=\"password\"\n >\n </v-input>\n\n <v-input\n name=\"tenantId\"\n [title]=\"'tenantId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'tenantIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.tenantId\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"defaultEnvironmentId\"\n [title]=\"'defaultEnvironmentId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'defaultEnvironmentIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.defaultEnvironmentId\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"templateSyncEnabled\"\n type=\"checkbox\"\n [title]=\"'templateSyncEnabled' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'templateSyncEnabledTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.templateSyncEnabled ? 'true' : ''\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n</v-form>\n" }]
340
+ args: [{ selector: 'epistola-configuration', standalone: true, imports: [CommonModule, PluginTranslatePipeModule, FormModule, InputModule], template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: safeDisabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n } as obs\"\n>\n <v-input\n name=\"configurationTitle\"\n [title]=\"'configurationTitle' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.configurationTitle\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"baseUrl\"\n [title]=\"'baseUrl' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'baseUrlTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.baseUrl\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"apiKey\"\n [title]=\"'apiKey' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'apiKeyTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.apiKey\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n type=\"password\"\n >\n </v-input>\n\n <v-input\n name=\"tenantId\"\n [title]=\"'tenantId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'tenantIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.tenantId\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"defaultEnvironmentId\"\n [title]=\"'defaultEnvironmentId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'defaultEnvironmentIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.defaultEnvironmentId\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"templateSyncEnabled\"\n type=\"checkbox\"\n [title]=\"'templateSyncEnabled' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'templateSyncEnabledTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.templateSyncEnabled ? 'true' : ''\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n</v-form>\n" }]
210
341
  }], propDecorators: { save$: [{
211
342
  type: Input
212
343
  }], disabled$: [{
@@ -221,453 +352,1164 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
221
352
  type: Output
222
353
  }] } });
223
354
 
224
- function countRequiredMapped(fields, mapping) {
225
- let mapped = 0;
226
- let total = 0;
227
- for (const field of fields) {
228
- if (field.fieldType === 'SCALAR' && field.required) {
229
- total++;
230
- const val = mapping[field.name];
231
- if (typeof val === 'string' && val.trim().length > 0) {
232
- mapped++;
355
+ /**
356
+ * Shared state for the JSONata completion provider.
357
+ * Updated by the editor component when suggestions/functions change.
358
+ */
359
+ const jsonataCompletionData = {
360
+ suggestions: null,
361
+ functions: [],
362
+ };
363
+ /**
364
+ * Register the JSONata language in Monaco editor.
365
+ * Call this once when Monaco is available (e.g., in editor component OnInit).
366
+ */
367
+ function registerJsonataLanguage(monaco) {
368
+ // Only register once
369
+ if (monaco.languages.getLanguages().some((lang) => lang.id === 'jsonata')) {
370
+ return;
371
+ }
372
+ monaco.languages.register({ id: 'jsonata' });
373
+ // Syntax highlighting via Monarch tokenizer
374
+ monaco.languages.setMonarchTokensProvider('jsonata', {
375
+ defaultToken: '',
376
+ tokenPostfix: '.jsonata',
377
+ keywords: ['true', 'false', 'null', 'in', 'and', 'or', 'not'],
378
+ operators: ['&', '?', ':', '=', '!=', '>', '<', '>=', '<=', '+', '-', '*', '/', '%', '~>'],
379
+ symbols: /[=><!~?:&|+\-*/^%]+/,
380
+ tokenizer: {
381
+ root: [
382
+ // Variables: $identifier
383
+ [/\$[a-zA-Z_]\w*/, 'variable'],
384
+ // Identifiers and keywords
385
+ [
386
+ /[a-zA-Z_]\w*/,
387
+ {
388
+ cases: {
389
+ '@keywords': 'keyword',
390
+ '@default': 'identifier',
391
+ },
392
+ },
393
+ ],
394
+ // Whitespace
395
+ { include: '@whitespace' },
396
+ // Strings
397
+ [/"([^"\\]|\\.)*$/, 'string.invalid'],
398
+ [/"/, 'string', '@string_double'],
399
+ [/'([^'\\]|\\.)*$/, 'string.invalid'],
400
+ [/'/, 'string', '@string_single'],
401
+ // Numbers
402
+ [/\d+(\.\d+)?([eE][-+]?\d+)?/, 'number'],
403
+ // Delimiters and operators
404
+ [/[{}()\[\]]/, '@brackets'],
405
+ [/[,;.]/, 'delimiter'],
406
+ [
407
+ /@symbols/,
408
+ {
409
+ cases: {
410
+ '@operators': 'operator',
411
+ '@default': '',
412
+ },
413
+ },
414
+ ],
415
+ ],
416
+ string_double: [
417
+ [/[^\\"]+/, 'string'],
418
+ [/\\./, 'string.escape'],
419
+ [/"/, 'string', '@pop'],
420
+ ],
421
+ string_single: [
422
+ [/[^\\']+/, 'string'],
423
+ [/\\./, 'string.escape'],
424
+ [/'/, 'string', '@pop'],
425
+ ],
426
+ whitespace: [[/[ \t\r\n]+/, 'white']],
427
+ },
428
+ });
429
+ // Autocomplete provider
430
+ monaco.languages.registerCompletionItemProvider('jsonata', {
431
+ triggerCharacters: ['$', '.'],
432
+ provideCompletionItems: (model, position) => {
433
+ const textUntilPosition = model.getValueInRange({
434
+ startLineNumber: position.lineNumber,
435
+ startColumn: 1,
436
+ endLineNumber: position.lineNumber,
437
+ endColumn: position.column,
438
+ });
439
+ const suggestions = [];
440
+ const CompletionItemKind = monaco.languages.CompletionItemKind;
441
+ // After "$" — suggest variables and functions
442
+ if (textUntilPosition.endsWith('$')) {
443
+ suggestions.push(...['doc', 'pv', 'case'].map((v) => ({
444
+ label: `$${v}`,
445
+ kind: CompletionItemKind.Variable,
446
+ insertText: v,
447
+ detail: `Context variable`,
448
+ })));
449
+ // Custom functions
450
+ for (const func of jsonataCompletionData.functions) {
451
+ suggestions.push({
452
+ label: `$${func.name}`,
453
+ kind: CompletionItemKind.Function,
454
+ insertText: `${func.name}($0)`,
455
+ insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
456
+ detail: func.description || 'Custom function',
457
+ });
458
+ }
459
+ // Built-in JSONata functions
460
+ const builtins = [
461
+ 'string',
462
+ 'number',
463
+ 'boolean',
464
+ 'length',
465
+ 'substring',
466
+ 'uppercase',
467
+ 'lowercase',
468
+ 'trim',
469
+ 'contains',
470
+ 'split',
471
+ 'join',
472
+ 'sum',
473
+ 'count',
474
+ 'max',
475
+ 'min',
476
+ 'average',
477
+ 'append',
478
+ 'sort',
479
+ 'reverse',
480
+ 'keys',
481
+ 'values',
482
+ 'lookup',
483
+ 'now',
484
+ 'exists',
485
+ 'type',
486
+ 'not',
487
+ ];
488
+ for (const fn of builtins) {
489
+ suggestions.push({
490
+ label: `$${fn}`,
491
+ kind: CompletionItemKind.Function,
492
+ insertText: `${fn}($0)`,
493
+ insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
494
+ detail: 'Built-in JSONata function',
495
+ });
496
+ }
233
497
  }
234
- }
235
- else if (field.fieldType === 'ARRAY' && field.required) {
236
- total++;
237
- const val = mapping[field.name];
238
- if (typeof val === 'string' && val.trim().length > 0) {
239
- mapped++;
498
+ // After "$doc." — suggest document paths
499
+ if (/\$doc\.\s*$/.test(textUntilPosition) || /\$doc\.[a-zA-Z_]*$/.test(textUntilPosition)) {
500
+ const docPaths = jsonataCompletionData.suggestions?.doc || [];
501
+ for (const path of docPaths) {
502
+ suggestions.push({
503
+ label: path,
504
+ kind: CompletionItemKind.Field,
505
+ insertText: path,
506
+ detail: 'Document field',
507
+ });
508
+ }
240
509
  }
241
- else if (typeof val === 'object' && val !== null && '_source' in val) {
242
- if (typeof val['_source'] === 'string' && val['_source'].trim().length > 0) {
243
- mapped++;
510
+ // After "$pv." suggest process variables
511
+ if (/\$pv\.\s*$/.test(textUntilPosition) || /\$pv\.[a-zA-Z_]*$/.test(textUntilPosition)) {
512
+ const pvNames = jsonataCompletionData.suggestions?.pv || [];
513
+ for (const name of pvNames) {
514
+ suggestions.push({
515
+ label: name,
516
+ kind: CompletionItemKind.Variable,
517
+ insertText: name,
518
+ detail: 'Process variable',
519
+ });
244
520
  }
245
521
  }
246
- }
247
- else if (field.fieldType === 'OBJECT' && field.children) {
248
- const nested = (typeof mapping[field.name] === 'object' && mapping[field.name] !== null)
249
- ? mapping[field.name]
250
- : {};
251
- const childStats = countRequiredMapped(field.children, nested);
252
- mapped += childStats.mapped;
253
- total += childStats.total;
254
- }
255
- }
256
- return { mapped, total };
522
+ return { suggestions };
523
+ },
524
+ });
257
525
  }
258
526
 
259
- /**
260
- * Reusable 3-mode input (browse / pv / expression) for value resolver expressions.
261
- * Used by both ScalarFieldComponent and ArrayFieldComponent for source mapping.
262
- */
263
- class ValueInputComponent {
264
- cdr;
265
- name = '';
266
- value = '';
267
- pluginId = '';
268
- caseDefinitionKey = null;
269
- processVariables = [];
527
+ const jsonata$1 = _jsonata.default || _jsonata;
528
+ class JsonataEditorComponent {
529
+ expression = '';
270
530
  disabled = false;
271
- placeholder = 'e.g. pv:variableName or doc:path.to.field';
272
- valueChange = new EventEmitter();
273
- ValuePathSelectorPrefix = ValuePathSelectorPrefix;
274
- inputMode = 'browse';
275
- selectedPv = '';
276
- browseDefault = '';
277
- constructor(cdr) {
278
- this.cdr = cdr;
531
+ suggestions = null;
532
+ functions = [];
533
+ expressionChange = new EventEmitter();
534
+ validChange = new EventEmitter();
535
+ editorModel = { value: '', language: 'jsonata' };
536
+ editorOptions = {
537
+ minimap: { enabled: false },
538
+ lineNumbers: 'on',
539
+ scrollBeyondLastLine: false,
540
+ fontSize: 13,
541
+ tabSize: 2,
542
+ wordWrap: 'on',
543
+ renderWhitespace: 'none',
544
+ };
545
+ error = null;
546
+ destroy$ = new Subject();
547
+ validate$ = new Subject();
548
+ suppressChange = false;
549
+ languageRegistered = false;
550
+ constructor() {
551
+ this.validate$.pipe(debounceTime(300), takeUntil(this.destroy$)).subscribe((value) => {
552
+ this.validateExpression(value);
553
+ });
554
+ // Try to register language eagerly if Monaco is already loaded
555
+ this.tryRegisterLanguage();
279
556
  }
280
557
  ngOnChanges(changes) {
281
- if (changes['value']) {
282
- this.inputMode = this.detectInputMode(this.value);
283
- this.browseDefault = normalizeToDots(this.value);
284
- this.selectedPv = extractPvName(this.value);
558
+ if (changes['expression'] && !this.suppressChange) {
559
+ this.editorModel = { value: this.expression || '', language: 'jsonata' };
560
+ this.validate$.next(this.expression);
285
561
  }
286
- if (changes['processVariables']) {
287
- this.cdr.markForCheck();
562
+ if (changes['suggestions']) {
563
+ jsonataCompletionData.suggestions = this.suggestions;
564
+ }
565
+ if (changes['functions']) {
566
+ jsonataCompletionData.functions = this.functions;
288
567
  }
289
568
  }
290
- setInputMode(mode) {
291
- this.inputMode = mode;
292
- }
293
- onBrowseValueChange(newValue) {
294
- this.valueChange.emit(normalizeToDots(newValue));
569
+ ngOnDestroy() {
570
+ this.destroy$.next();
571
+ this.destroy$.complete();
295
572
  }
296
- onPvChange(newValue) {
297
- this.selectedPv = newValue;
298
- this.valueChange.emit(newValue ? 'pv:' + newValue : '');
573
+ onEditorValueChange(value) {
574
+ // Register language on first editor event (Monaco is now loaded)
575
+ if (!this.languageRegistered) {
576
+ this.tryRegisterLanguage();
577
+ }
578
+ if (this.suppressChange)
579
+ return;
580
+ this.suppressChange = true;
581
+ this.expression = value;
582
+ this.expressionChange.emit(value);
583
+ this.validate$.next(value);
584
+ setTimeout(() => (this.suppressChange = false));
299
585
  }
300
- onExpressionValueChange(newValue) {
301
- this.valueChange.emit(newValue);
586
+ tryRegisterLanguage() {
587
+ const m = window.monaco;
588
+ if (m) {
589
+ registerJsonataLanguage(m);
590
+ this.languageRegistered = true;
591
+ jsonataCompletionData.suggestions = this.suggestions;
592
+ jsonataCompletionData.functions = this.functions;
593
+ }
302
594
  }
303
- detectInputMode(value) {
304
- if (!value)
305
- return 'browse';
306
- if (value.startsWith('doc:') || value.startsWith('case:'))
307
- return 'browse';
308
- if (value.startsWith('pv:'))
309
- return 'pv';
310
- if (value.length > 0)
311
- return 'expression';
312
- return 'browse';
595
+ validateExpression(value) {
596
+ if (!value || !value.trim()) {
597
+ this.error = null;
598
+ this.validChange.emit(true);
599
+ return;
600
+ }
601
+ try {
602
+ jsonata$1(value);
603
+ this.error = null;
604
+ this.validChange.emit(true);
605
+ }
606
+ catch (e) {
607
+ this.error = e.message || 'Invalid expression';
608
+ this.validChange.emit(false);
609
+ }
313
610
  }
314
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ValueInputComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
315
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: ValueInputComponent, isStandalone: true, selector: "epistola-value-input", inputs: { name: "name", value: "value", pluginId: "pluginId", caseDefinitionKey: "caseDefinitionKey", processVariables: "processVariables", disabled: "disabled", placeholder: "placeholder" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"value-input\">\n <div class=\"input-mode-group\">\n <button type=\"button\" class=\"mode-btn\" [class.mode-active]=\"inputMode === 'browse'\" [disabled]=\"disabled\" (click)=\"setInputMode('browse')\" [title]=\"'browseMode' | pluginTranslate: pluginId | async\">\u229E</button>\n <button type=\"button\" class=\"mode-btn\" [class.mode-active]=\"inputMode === 'pv'\" [disabled]=\"disabled\" (click)=\"setInputMode('pv')\" [title]=\"'pvMode' | pluginTranslate: pluginId | async\">pv</button>\n <button type=\"button\" class=\"mode-btn\" [class.mode-active]=\"inputMode === 'expression'\" [disabled]=\"disabled\" (click)=\"setInputMode('expression')\" [title]=\"'expressionMode' | pluginTranslate: pluginId | async\">fx</button>\n </div>\n\n <!-- Browse mode: ValuePathSelector -->\n <div class=\"input-control\" *ngIf=\"inputMode === 'browse'\">\n <valtimo-value-path-selector\n [name]=\"name\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n [prefixes]=\"[ValuePathSelectorPrefix.DOC, ValuePathSelectorPrefix.CASE]\"\n [notation]=\"'dots'\"\n [disabled]=\"disabled\"\n [defaultValue]=\"browseDefault\"\n [showCaseDefinitionSelector]=\"!caseDefinitionKey\"\n (valueChangeEvent)=\"onBrowseValueChange($event)\"\n ></valtimo-value-path-selector>\n </div>\n\n <!-- PV mode: dropdown (when available) or text fallback -->\n <div class=\"input-control\" *ngIf=\"inputMode === 'pv'\">\n <select\n *ngIf=\"processVariables.length > 0; else pvFallback\"\n class=\"pv-select\"\n [disabled]=\"disabled\"\n [(ngModel)]=\"selectedPv\"\n (ngModelChange)=\"onPvChange($event)\"\n >\n <option value=\"\">{{ 'pvPlaceholder' | pluginTranslate: pluginId | async }}</option>\n <option *ngFor=\"let pv of processVariables\" [value]=\"pv\">{{ pv }}</option>\n </select>\n <ng-template #pvFallback>\n <v-input\n [name]=\"'pvfb_' + name\"\n [defaultValue]=\"selectedPv\"\n [disabled]=\"disabled\"\n [placeholder]=\"'pvPlaceholder' | pluginTranslate: pluginId | async\"\n (valueChange)=\"onPvChange($event)\"\n ></v-input>\n </ng-template>\n </div>\n\n <!-- Expression mode: text input -->\n <div class=\"input-control\" *ngIf=\"inputMode === 'expression'\">\n <v-input\n [name]=\"'fx_' + name\"\n [defaultValue]=\"value\"\n [disabled]=\"disabled\"\n [placeholder]=\"placeholder\"\n (valueChange)=\"onExpressionValueChange($event)\"\n ></v-input>\n </div>\n</div>\n", styles: [".value-input{display:flex;align-items:flex-start;gap:.5rem;min-width:0}.value-input ::ng-deep [data-test-id=valuePathSelectorToggle]{display:none!important}.input-control{flex:1;min-width:0}.input-mode-group{flex:0 0 auto;display:flex;margin-top:.25rem;border:1px solid #c6c6c6;border-radius:4px;overflow:hidden}.input-mode-group .mode-btn{width:32px;height:32px;padding:0;border:none;border-right:1px solid #c6c6c6;background:#f4f4f4;color:#525252;font-size:.75rem;font-weight:600;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background-color .15s}.input-mode-group .mode-btn:last-child{border-right:none}.input-mode-group .mode-btn:hover:not(:disabled){background:#e0e0e0}.input-mode-group .mode-btn.mode-active{background:#0f62fe;color:#fff}.input-mode-group .mode-btn.mode-active:hover:not(:disabled){background:#0353e9}.input-mode-group .mode-btn:disabled{opacity:.5;cursor:not-allowed}.pv-select{width:100%;height:2.5rem;padding:0 .75rem;border:1px solid #c6c6c6;border-radius:4px;background:#fff;color:#161616;font-size:.875rem;cursor:pointer}.pv-select:focus{outline:2px solid #0f62fe;outline-offset:-2px}.pv-select:disabled{opacity:.5;cursor:not-allowed;background:#f4f4f4}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i4.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i4.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i4.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: 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: "component", type: ValuePathSelectorComponent, selector: "valtimo-value-path-selector", inputs: ["name", "appendInline", "margin", "marginLg", "marginXl", "disabled", "caseDefinitionKey", "caseDefinitionVersionTag", "buildingBlockDefinitionKey", "buildingBlockDefinitionVersionTag", "prefixes", "label", "tooltip", "required", "showCaseDefinitionSelector", "notation", "dropUp", "defaultValue", "type", "parentItem", "filterItems"], outputs: ["valueChangeEvent", "collectionSelected"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
611
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: JsonataEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
612
+ 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: `
613
+ <div class="jsonata-editor">
614
+ <valtimo-editor
615
+ [model]="editorModel"
616
+ [editorOptions]="editorOptions"
617
+ [disabled]="disabled"
618
+ [heightPx]="250"
619
+ [formatOnLoad]="false"
620
+ (valueChangeEvent)="onEditorValueChange($event)"
621
+ ></valtimo-editor>
622
+ <div class="jsonata-editor__footer">
623
+ <span *ngIf="error" class="jsonata-editor__error">{{ error }}</span>
624
+ <span *ngIf="!error && expression" class="jsonata-editor__valid">&#x2713;</span>
625
+ <span class="jsonata-editor__variables">$doc · $pv · $case</span>
626
+ </div>
627
+ </div>
628
+ `, 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"] }] });
316
629
  }
317
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ValueInputComponent, decorators: [{
630
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: JsonataEditorComponent, decorators: [{
318
631
  type: Component,
319
- args: [{ selector: 'epistola-value-input', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [
320
- CommonModule,
321
- FormsModule,
322
- PluginTranslatePipeModule,
323
- InputModule,
324
- ValuePathSelectorComponent
325
- ], template: "<div class=\"value-input\">\n <div class=\"input-mode-group\">\n <button type=\"button\" class=\"mode-btn\" [class.mode-active]=\"inputMode === 'browse'\" [disabled]=\"disabled\" (click)=\"setInputMode('browse')\" [title]=\"'browseMode' | pluginTranslate: pluginId | async\">\u229E</button>\n <button type=\"button\" class=\"mode-btn\" [class.mode-active]=\"inputMode === 'pv'\" [disabled]=\"disabled\" (click)=\"setInputMode('pv')\" [title]=\"'pvMode' | pluginTranslate: pluginId | async\">pv</button>\n <button type=\"button\" class=\"mode-btn\" [class.mode-active]=\"inputMode === 'expression'\" [disabled]=\"disabled\" (click)=\"setInputMode('expression')\" [title]=\"'expressionMode' | pluginTranslate: pluginId | async\">fx</button>\n </div>\n\n <!-- Browse mode: ValuePathSelector -->\n <div class=\"input-control\" *ngIf=\"inputMode === 'browse'\">\n <valtimo-value-path-selector\n [name]=\"name\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n [prefixes]=\"[ValuePathSelectorPrefix.DOC, ValuePathSelectorPrefix.CASE]\"\n [notation]=\"'dots'\"\n [disabled]=\"disabled\"\n [defaultValue]=\"browseDefault\"\n [showCaseDefinitionSelector]=\"!caseDefinitionKey\"\n (valueChangeEvent)=\"onBrowseValueChange($event)\"\n ></valtimo-value-path-selector>\n </div>\n\n <!-- PV mode: dropdown (when available) or text fallback -->\n <div class=\"input-control\" *ngIf=\"inputMode === 'pv'\">\n <select\n *ngIf=\"processVariables.length > 0; else pvFallback\"\n class=\"pv-select\"\n [disabled]=\"disabled\"\n [(ngModel)]=\"selectedPv\"\n (ngModelChange)=\"onPvChange($event)\"\n >\n <option value=\"\">{{ 'pvPlaceholder' | pluginTranslate: pluginId | async }}</option>\n <option *ngFor=\"let pv of processVariables\" [value]=\"pv\">{{ pv }}</option>\n </select>\n <ng-template #pvFallback>\n <v-input\n [name]=\"'pvfb_' + name\"\n [defaultValue]=\"selectedPv\"\n [disabled]=\"disabled\"\n [placeholder]=\"'pvPlaceholder' | pluginTranslate: pluginId | async\"\n (valueChange)=\"onPvChange($event)\"\n ></v-input>\n </ng-template>\n </div>\n\n <!-- Expression mode: text input -->\n <div class=\"input-control\" *ngIf=\"inputMode === 'expression'\">\n <v-input\n [name]=\"'fx_' + name\"\n [defaultValue]=\"value\"\n [disabled]=\"disabled\"\n [placeholder]=\"placeholder\"\n (valueChange)=\"onExpressionValueChange($event)\"\n ></v-input>\n </div>\n</div>\n", styles: [".value-input{display:flex;align-items:flex-start;gap:.5rem;min-width:0}.value-input ::ng-deep [data-test-id=valuePathSelectorToggle]{display:none!important}.input-control{flex:1;min-width:0}.input-mode-group{flex:0 0 auto;display:flex;margin-top:.25rem;border:1px solid #c6c6c6;border-radius:4px;overflow:hidden}.input-mode-group .mode-btn{width:32px;height:32px;padding:0;border:none;border-right:1px solid #c6c6c6;background:#f4f4f4;color:#525252;font-size:.75rem;font-weight:600;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background-color .15s}.input-mode-group .mode-btn:last-child{border-right:none}.input-mode-group .mode-btn:hover:not(:disabled){background:#e0e0e0}.input-mode-group .mode-btn.mode-active{background:#0f62fe;color:#fff}.input-mode-group .mode-btn.mode-active:hover:not(:disabled){background:#0353e9}.input-mode-group .mode-btn:disabled{opacity:.5;cursor:not-allowed}.pv-select{width:100%;height:2.5rem;padding:0 .75rem;border:1px solid #c6c6c6;border-radius:4px;background:#fff;color:#161616;font-size:.875rem;cursor:pointer}.pv-select:focus{outline:2px solid #0f62fe;outline-offset:-2px}.pv-select:disabled{opacity:.5;cursor:not-allowed;background:#f4f4f4}\n"] }]
326
- }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { name: [{
327
- type: Input
328
- }], value: [{
329
- type: Input
330
- }], pluginId: [{
331
- type: Input
332
- }], caseDefinitionKey: [{
333
- type: Input
334
- }], processVariables: [{
632
+ args: [{ selector: 'epistola-jsonata-editor', standalone: true, imports: [CommonModule, PluginTranslatePipeModule, EditorModule], template: `
633
+ <div class="jsonata-editor">
634
+ <valtimo-editor
635
+ [model]="editorModel"
636
+ [editorOptions]="editorOptions"
637
+ [disabled]="disabled"
638
+ [heightPx]="250"
639
+ [formatOnLoad]="false"
640
+ (valueChangeEvent)="onEditorValueChange($event)"
641
+ ></valtimo-editor>
642
+ <div class="jsonata-editor__footer">
643
+ <span *ngIf="error" class="jsonata-editor__error">{{ error }}</span>
644
+ <span *ngIf="!error && expression" class="jsonata-editor__valid">&#x2713;</span>
645
+ <span class="jsonata-editor__variables">$doc · $pv · $case</span>
646
+ </div>
647
+ </div>
648
+ `, 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"] }]
649
+ }], ctorParameters: () => [], propDecorators: { expression: [{
335
650
  type: Input
336
651
  }], disabled: [{
337
652
  type: Input
338
- }], placeholder: [{
653
+ }], suggestions: [{
339
654
  type: Input
340
- }], valueChange: [{
655
+ }], functions: [{
656
+ type: Input
657
+ }], expressionChange: [{
658
+ type: Output
659
+ }], validChange: [{
341
660
  type: Output
342
661
  }] } });
343
- /** Convert slash-notation paths (e.g. doc:/a/b) to dot notation (doc:a.b). */
344
- function normalizeToDots(value) {
345
- if (typeof value !== 'string')
346
- return value;
347
- const colonIndex = value.indexOf(':');
348
- if (colonIndex < 0)
349
- return value;
350
- const prefix = value.substring(0, colonIndex);
351
- const path = value.substring(colonIndex + 1);
352
- if (!path.includes('/'))
353
- return value;
354
- const normalized = path.split('/').filter(p => p.length > 0).join('.');
355
- return `${prefix}:${normalized}`;
356
- }
357
- function extractPvName(value) {
358
- if (typeof value === 'string' && value.startsWith('pv:')) {
359
- return value.substring(3);
662
+
663
+ class ExpectedStructureComponent {
664
+ templateFields = [];
665
+ structureText = '{}';
666
+ ngOnChanges(changes) {
667
+ if (changes['templateFields']) {
668
+ this.structureText = this.buildStructure(this.templateFields, 0);
669
+ }
670
+ }
671
+ buildStructure(fields, depth) {
672
+ if (!fields || fields.length === 0)
673
+ return '{}';
674
+ const indent = ' '.repeat(depth + 1);
675
+ const closing = ' '.repeat(depth);
676
+ const lines = fields.map((f) => {
677
+ const req = f.required ? ' (required)' : '';
678
+ if (f.fieldType === 'OBJECT' && f.children?.length) {
679
+ const nested = this.buildStructure(f.children, depth + 1);
680
+ return `${indent}"${f.name}": ${nested}${req}`;
681
+ }
682
+ if (f.fieldType === 'ARRAY') {
683
+ if (f.children?.length) {
684
+ const itemStructure = this.buildStructure(f.children, depth + 2);
685
+ return `${indent}"${f.name}": [${itemStructure}]${req}`;
686
+ }
687
+ return `${indent}"${f.name}": array${req}`;
688
+ }
689
+ return `${indent}"${f.name}": ${f.type || 'any'}${req}`;
690
+ });
691
+ return `{\n${lines.join(',\n')}\n${closing}}`;
360
692
  }
361
- return '';
693
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ExpectedStructureComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
694
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: ExpectedStructureComponent, isStandalone: true, selector: "epistola-expected-structure", inputs: { templateFields: "templateFields" }, usesOnChanges: true, ngImport: i0, template: `
695
+ <div class="expected">
696
+ <div class="expected__header">
697
+ {{ 'expectedStructure' | pluginTranslate: 'epistola' | async }}
698
+ </div>
699
+ <div *ngIf="!templateFields || templateFields.length === 0" class="expected__empty">
700
+ {{ 'expectedStructureLoading' | pluginTranslate: 'epistola' | async }}
701
+ </div>
702
+ <pre *ngIf="templateFields && templateFields.length > 0" class="expected__code">{{
703
+ structureText
704
+ }}</pre>
705
+ </div>
706
+ `, isInline: true, styles: [".expected{border:1px solid #e0e0e0;border-radius:4px;overflow:hidden;height:100%;display:flex;flex-direction:column}.expected__header{padding:6px 12px;background:#f4f4f4;border-bottom:1px solid #e0e0e0;font-size:.75em;color:#6f6f6f;text-transform:uppercase;letter-spacing:.5px}.expected__code{flex:1;font-family:IBM Plex Mono,monospace;font-size:.8em;line-height:1.5;margin:0;padding:8px 12px;white-space:pre-wrap;overflow-y:auto}.expected__empty{padding:8px 12px;color:#8d8d8d;font-size:.85em;font-style:italic}\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: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }] });
362
707
  }
708
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ExpectedStructureComponent, decorators: [{
709
+ type: Component,
710
+ args: [{ selector: 'epistola-expected-structure', standalone: true, imports: [CommonModule, PluginTranslatePipeModule], template: `
711
+ <div class="expected">
712
+ <div class="expected__header">
713
+ {{ 'expectedStructure' | pluginTranslate: 'epistola' | async }}
714
+ </div>
715
+ <div *ngIf="!templateFields || templateFields.length === 0" class="expected__empty">
716
+ {{ 'expectedStructureLoading' | pluginTranslate: 'epistola' | async }}
717
+ </div>
718
+ <pre *ngIf="templateFields && templateFields.length > 0" class="expected__code">{{
719
+ structureText
720
+ }}</pre>
721
+ </div>
722
+ `, styles: [".expected{border:1px solid #e0e0e0;border-radius:4px;overflow:hidden;height:100%;display:flex;flex-direction:column}.expected__header{padding:6px 12px;background:#f4f4f4;border-bottom:1px solid #e0e0e0;font-size:.75em;color:#6f6f6f;text-transform:uppercase;letter-spacing:.5px}.expected__code{flex:1;font-family:IBM Plex Mono,monospace;font-size:.8em;line-height:1.5;margin:0;padding:8px 12px;white-space:pre-wrap;overflow-y:auto}.expected__empty{padding:8px 12px;color:#8d8d8d;font-size:.85em;font-style:italic}\n"] }]
723
+ }], propDecorators: { templateFields: [{
724
+ type: Input
725
+ }] } });
363
726
 
364
- class ScalarFieldComponent {
727
+ class BuilderFieldComponent {
365
728
  field;
366
- value = undefined;
367
- pluginId;
368
- caseDefinitionKey = null;
369
- processVariables = [];
729
+ path = [];
730
+ suggestions = [];
370
731
  disabled = false;
732
+ collapsed = false;
733
+ required = false;
734
+ collapsedPaths = new Set();
371
735
  valueChange = new EventEmitter();
372
- get stringValue() {
373
- return typeof this.value === 'string' ? this.value : '';
374
- }
375
- onValueChange(newValue) {
376
- this.valueChange.emit(newValue || undefined);
736
+ modeToggle = new EventEmitter();
737
+ collapseToggle = new EventEmitter();
738
+ isChildCollapsed(childIndex) {
739
+ return this.collapsedPaths.has(this.path.concat(childIndex).join('.'));
377
740
  }
378
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ScalarFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
379
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: ScalarFieldComponent, isStandalone: true, selector: "epistola-scalar-field", inputs: { field: "field", value: "value", pluginId: "pluginId", caseDefinitionKey: "caseDefinitionKey", processVariables: "processVariables", disabled: "disabled" }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: "<div class=\"field-row\" [class.field-required-unmapped]=\"field.required && !stringValue\">\n <div class=\"field-label\">\n <span class=\"field-name\">{{ field.name }}</span>\n <span class=\"field-meta\">({{ field.type }}{{ field.required ? ', required' : '' }})</span>\n </div>\n <epistola-value-input\n [name]=\"'field_' + field.path\"\n [value]=\"stringValue\"\n [pluginId]=\"pluginId\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n [processVariables]=\"processVariables\"\n [disabled]=\"disabled\"\n (valueChange)=\"onValueChange($event)\"\n ></epistola-value-input>\n</div>\n", styles: [".field-row{display:flex;align-items:flex-start;gap:.75rem;padding:.5rem 0;border-left:3px solid transparent}.field-row.field-required-unmapped{border-left-color:#dc3545;background-color:#fff5f5;padding-left:.5rem}.field-label{flex:0 0 200px;min-width:140px;padding-top:.5rem;word-break:break-word}.field-label .field-name{font-weight:500}.field-label .field-meta{font-size:.8125rem;color:#6c757d;margin-left:.25rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "component", type: ValueInputComponent, selector: "epistola-value-input", inputs: ["name", "value", "pluginId", "caseDefinitionKey", "processVariables", "disabled", "placeholder"], outputs: ["valueChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
741
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: BuilderFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
742
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: BuilderFieldComponent, isStandalone: true, selector: "epistola-builder-field", inputs: { field: "field", path: "path", suggestions: "suggestions", disabled: "disabled", collapsed: "collapsed", required: "required", collapsedPaths: "collapsedPaths" }, outputs: { valueChange: "valueChange", modeToggle: "modeToggle", collapseToggle: "collapseToggle" }, ngImport: i0, template: `
743
+ <div class="builder-field">
744
+ <div
745
+ class="builder-field__name"
746
+ [class.builder-field__name--clickable]="field.children"
747
+ (click)="field.children && collapseToggle.emit(path)"
748
+ >
749
+ <span *ngIf="field.children" class="builder-field__chevron">{{
750
+ collapsed ? '&#x25B6;' : '&#x25BC;'
751
+ }}</span>
752
+ <span class="builder-field__label">{{ field.name }}</span>
753
+ <span *ngIf="required" class="builder-field__required">*</span>
754
+ <span *ngIf="field.children" class="builder-field__type">(object)</span>
755
+ </div>
756
+
757
+ <div class="builder-field__value" *ngIf="!field.children">
758
+ <input
759
+ *ngIf="field.mode === 'ref'"
760
+ type="text"
761
+ class="builder-field__input"
762
+ [ngModel]="field.value"
763
+ (ngModelChange)="valueChange.emit({ path: path, value: $event })"
764
+ [disabled]="disabled"
765
+ placeholder="$doc.path.to.field"
766
+ [attr.list]="'suggestions-' + path.join('-')"
767
+ />
768
+ <datalist *ngIf="field.mode === 'ref'" [id]="'suggestions-' + path.join('-')">
769
+ <option *ngFor="let s of suggestions" [value]="s"></option>
770
+ </datalist>
771
+ <input
772
+ *ngIf="field.mode === 'raw'"
773
+ type="text"
774
+ class="builder-field__input builder-field__input--raw"
775
+ [ngModel]="field.value"
776
+ (ngModelChange)="valueChange.emit({ path: path, value: $event })"
777
+ [disabled]="disabled"
778
+ placeholder="JSONata expression"
779
+ />
780
+ <button
781
+ class="builder-field__mode-toggle"
782
+ (click)="modeToggle.emit(path)"
783
+ [disabled]="disabled"
784
+ [title]="field.mode === 'ref' ? 'Switch to raw JSONata' : 'Switch to reference'"
785
+ >
786
+ {{ field.mode === 'ref' ? 'fx' : '·' }}
787
+ </button>
788
+ </div>
789
+
790
+ <div *ngIf="field.children && !collapsed" class="builder-field__children">
791
+ <epistola-builder-field
792
+ *ngFor="let child of field.children; let j = index"
793
+ [field]="child"
794
+ [path]="path.concat(j)"
795
+ [suggestions]="suggestions"
796
+ [disabled]="disabled"
797
+ [collapsed]="isChildCollapsed(j)"
798
+ [collapsedPaths]="collapsedPaths"
799
+ [required]="false"
800
+ (valueChange)="valueChange.emit($event)"
801
+ (modeToggle)="modeToggle.emit($event)"
802
+ (collapseToggle)="collapseToggle.emit($event)"
803
+ ></epistola-builder-field>
804
+ </div>
805
+ </div>
806
+ `, 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"] }] });
380
807
  }
381
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ScalarFieldComponent, decorators: [{
808
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: BuilderFieldComponent, decorators: [{
382
809
  type: Component,
383
- args: [{ selector: 'epistola-scalar-field', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, PluginTranslatePipeModule, ValueInputComponent], template: "<div class=\"field-row\" [class.field-required-unmapped]=\"field.required && !stringValue\">\n <div class=\"field-label\">\n <span class=\"field-name\">{{ field.name }}</span>\n <span class=\"field-meta\">({{ field.type }}{{ field.required ? ', required' : '' }})</span>\n </div>\n <epistola-value-input\n [name]=\"'field_' + field.path\"\n [value]=\"stringValue\"\n [pluginId]=\"pluginId\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n [processVariables]=\"processVariables\"\n [disabled]=\"disabled\"\n (valueChange)=\"onValueChange($event)\"\n ></epistola-value-input>\n</div>\n", styles: [".field-row{display:flex;align-items:flex-start;gap:.75rem;padding:.5rem 0;border-left:3px solid transparent}.field-row.field-required-unmapped{border-left-color:#dc3545;background-color:#fff5f5;padding-left:.5rem}.field-label{flex:0 0 200px;min-width:140px;padding-top:.5rem;word-break:break-word}.field-label .field-name{font-weight:500}.field-label .field-meta{font-size:.8125rem;color:#6c757d;margin-left:.25rem}\n"] }]
810
+ args: [{ selector: 'epistola-builder-field', standalone: true, imports: [CommonModule, FormsModule], template: `
811
+ <div class="builder-field">
812
+ <div
813
+ class="builder-field__name"
814
+ [class.builder-field__name--clickable]="field.children"
815
+ (click)="field.children && collapseToggle.emit(path)"
816
+ >
817
+ <span *ngIf="field.children" class="builder-field__chevron">{{
818
+ collapsed ? '&#x25B6;' : '&#x25BC;'
819
+ }}</span>
820
+ <span class="builder-field__label">{{ field.name }}</span>
821
+ <span *ngIf="required" class="builder-field__required">*</span>
822
+ <span *ngIf="field.children" class="builder-field__type">(object)</span>
823
+ </div>
824
+
825
+ <div class="builder-field__value" *ngIf="!field.children">
826
+ <input
827
+ *ngIf="field.mode === 'ref'"
828
+ type="text"
829
+ class="builder-field__input"
830
+ [ngModel]="field.value"
831
+ (ngModelChange)="valueChange.emit({ path: path, value: $event })"
832
+ [disabled]="disabled"
833
+ placeholder="$doc.path.to.field"
834
+ [attr.list]="'suggestions-' + path.join('-')"
835
+ />
836
+ <datalist *ngIf="field.mode === 'ref'" [id]="'suggestions-' + path.join('-')">
837
+ <option *ngFor="let s of suggestions" [value]="s"></option>
838
+ </datalist>
839
+ <input
840
+ *ngIf="field.mode === 'raw'"
841
+ type="text"
842
+ class="builder-field__input builder-field__input--raw"
843
+ [ngModel]="field.value"
844
+ (ngModelChange)="valueChange.emit({ path: path, value: $event })"
845
+ [disabled]="disabled"
846
+ placeholder="JSONata expression"
847
+ />
848
+ <button
849
+ class="builder-field__mode-toggle"
850
+ (click)="modeToggle.emit(path)"
851
+ [disabled]="disabled"
852
+ [title]="field.mode === 'ref' ? 'Switch to raw JSONata' : 'Switch to reference'"
853
+ >
854
+ {{ field.mode === 'ref' ? 'fx' : '·' }}
855
+ </button>
856
+ </div>
857
+
858
+ <div *ngIf="field.children && !collapsed" class="builder-field__children">
859
+ <epistola-builder-field
860
+ *ngFor="let child of field.children; let j = index"
861
+ [field]="child"
862
+ [path]="path.concat(j)"
863
+ [suggestions]="suggestions"
864
+ [disabled]="disabled"
865
+ [collapsed]="isChildCollapsed(j)"
866
+ [collapsedPaths]="collapsedPaths"
867
+ [required]="false"
868
+ (valueChange)="valueChange.emit($event)"
869
+ (modeToggle)="modeToggle.emit($event)"
870
+ (collapseToggle)="collapseToggle.emit($event)"
871
+ ></epistola-builder-field>
872
+ </div>
873
+ </div>
874
+ `, 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"] }]
384
875
  }], propDecorators: { field: [{
385
876
  type: Input
386
- }], value: [{
877
+ }], path: [{
387
878
  type: Input
388
- }], pluginId: [{
879
+ }], suggestions: [{
389
880
  type: Input
390
- }], caseDefinitionKey: [{
881
+ }], disabled: [{
391
882
  type: Input
392
- }], processVariables: [{
883
+ }], collapsed: [{
393
884
  type: Input
394
- }], disabled: [{
885
+ }], required: [{
886
+ type: Input
887
+ }], collapsedPaths: [{
395
888
  type: Input
396
889
  }], valueChange: [{
397
890
  type: Output
891
+ }], modeToggle: [{
892
+ type: Output
893
+ }], collapseToggle: [{
894
+ type: Output
398
895
  }] } });
399
896
 
400
- class ArrayFieldComponent {
401
- field;
402
- value = undefined;
403
- pluginId;
404
- caseDefinitionKey = null;
405
- processVariables = [];
406
- disabled = false;
407
- valueChange = new EventEmitter();
408
- expanded = false;
409
- arrayPerFieldMode = false;
410
- mappedCount = 0;
411
- totalRequired = 0;
412
- ngOnChanges(changes) {
413
- if (changes['value'] || changes['field']) {
414
- this.updateCompleteness();
415
- if (!this.expanded && this.totalRequired > 0 && this.mappedCount < this.totalRequired) {
416
- this.expanded = true;
417
- }
418
- // Detect per-field mode from value shape
419
- if (changes['value'] && typeof this.value === 'object' && this.value !== null && '_source' in this.value) {
420
- this.arrayPerFieldMode = true;
421
- }
897
+ const jsonata = _jsonata.default || _jsonata;
898
+ /**
899
+ * Parse a JSONata expression into BuilderField array.
900
+ * Only supports top-level object literals with simple path references or nested objects.
901
+ * Anything else is stored as raw JSONata text.
902
+ */
903
+ function parseJsonataToBuilder(expression) {
904
+ if (!expression || !expression.trim()) {
905
+ return [];
906
+ }
907
+ try {
908
+ const ast = jsonata(expression).ast();
909
+ if (ast.type === 'unary' && ast.value === '{') {
910
+ return parseObjectEntries(ast.lhs, expression);
422
911
  }
912
+ // Not a top-level object — can't represent in builder
913
+ return [{ name: '_root', mode: 'raw', value: expression }];
423
914
  }
424
- toggleExpanded() {
425
- this.expanded = !this.expanded;
915
+ catch {
916
+ // Invalid JSONata — return as single raw field
917
+ return [{ name: '_root', mode: 'raw', value: expression }];
426
918
  }
427
- getSourceValue() {
428
- if (typeof this.value === 'string') {
429
- return normalizeToDots(this.value);
430
- }
431
- if (typeof this.value === 'object' && this.value !== null && '_source' in this.value) {
432
- return normalizeToDots(this.value['_source'] || '');
919
+ }
920
+ /**
921
+ * Convert BuilderField array back to a JSONata expression string.
922
+ */
923
+ function builderToJsonata(fields) {
924
+ if (fields.length === 0) {
925
+ return '';
926
+ }
927
+ // Special case: single _root raw field means the whole expression is raw
928
+ if (fields.length === 1 && fields[0].name === '_root' && fields[0].mode === 'raw') {
929
+ return fields[0].value;
930
+ }
931
+ const entries = fields.map((field) => formatFieldEntry(field)).filter(Boolean);
932
+ return `{\n${entries.join(',\n')}\n}`;
933
+ }
934
+ /**
935
+ * Check if a JSONata expression can be fully represented by the builder
936
+ * (i.e., all fields are simple refs or nested objects of simple refs).
937
+ */
938
+ function isBuilderCompatible(expression) {
939
+ const fields = parseJsonataToBuilder(expression);
940
+ return fields.every((f) => f.mode === 'ref' || (f.children && f.children.every(isFieldSimple)));
941
+ }
942
+ function isFieldSimple(field) {
943
+ if (field.mode === 'raw')
944
+ return false;
945
+ if (field.children)
946
+ return field.children.every(isFieldSimple);
947
+ return true;
948
+ }
949
+ function parseObjectEntries(entries, source) {
950
+ return entries.map(([keyNode, valueNode]) => {
951
+ const name = keyNode.value;
952
+ return classifyValue(name, valueNode, source);
953
+ });
954
+ }
955
+ function classifyValue(name, node, source) {
956
+ // Simple path reference: $doc.x.y, $pv.x, $case.x — store as JSONata directly
957
+ if (node.type === 'path' && node.steps?.length > 0 && node.steps[0].type === 'variable') {
958
+ const varName = node.steps[0].value; // doc, pv, case
959
+ if (['doc', 'pv', 'case'].includes(varName)) {
960
+ const path = node.steps
961
+ .slice(1)
962
+ .map((s) => s.value)
963
+ .join('.');
964
+ return { name, mode: 'ref', value: `$${varName}.${path}` };
433
965
  }
966
+ }
967
+ // String literal
968
+ if (node.type === 'string') {
969
+ return { name, mode: 'ref', value: `"${node.value}"` };
970
+ }
971
+ // Number literal
972
+ if (node.type === 'number') {
973
+ return { name, mode: 'ref', value: String(node.value) };
974
+ }
975
+ // Boolean literal (value node)
976
+ if (node.type === 'value' && typeof node.value === 'boolean') {
977
+ return { name, mode: 'ref', value: String(node.value) };
978
+ }
979
+ // Nested object
980
+ if (node.type === 'unary' && node.value === '{' && node.lhs) {
981
+ const children = parseObjectEntries(node.lhs, source);
982
+ return { name, mode: 'ref', value: '', children };
983
+ }
984
+ // Anything else: extract raw source text
985
+ const raw = extractSourceFragment(node, source);
986
+ return { name, mode: 'raw', value: raw };
987
+ }
988
+ /**
989
+ * Extract the source text for a node using position info.
990
+ * Falls back to a generic representation if positions aren't useful.
991
+ */
992
+ function extractSourceFragment(node, source) {
993
+ // Try to reconstruct from AST for common patterns
994
+ if (node.type === 'condition') {
995
+ const cond = reconstructExpression(node.condition);
996
+ const then = reconstructExpression(node.then);
997
+ const els = reconstructExpression(node.else);
998
+ return `${cond} ? ${then} : ${els}`;
999
+ }
1000
+ if (node.type === 'binary') {
1001
+ const left = reconstructExpression(node.lhs);
1002
+ const right = reconstructExpression(node.rhs);
1003
+ return `${left} ${node.value} ${right}`;
1004
+ }
1005
+ if (node.type === 'function') {
1006
+ const name = reconstructExpression(node.procedure);
1007
+ const args = (node.arguments || []).map(reconstructExpression).join(', ');
1008
+ return `${name}(${args})`;
1009
+ }
1010
+ // Generic fallback
1011
+ return reconstructExpression(node);
1012
+ }
1013
+ function reconstructExpression(node) {
1014
+ if (!node)
434
1015
  return '';
1016
+ if (node.type === 'string')
1017
+ return `"${node.value}"`;
1018
+ if (node.type === 'number')
1019
+ return String(node.value);
1020
+ if (node.type === 'value')
1021
+ return String(node.value);
1022
+ if (node.type === 'path' && node.steps) {
1023
+ return node.steps.map((s) => (s.type === 'variable' ? `$${s.value}` : s.value)).join('.');
1024
+ }
1025
+ if (node.type === 'binary') {
1026
+ return `${reconstructExpression(node.lhs)} ${node.value} ${reconstructExpression(node.rhs)}`;
1027
+ }
1028
+ if (node.type === 'condition') {
1029
+ return `${reconstructExpression(node.condition)} ? ${reconstructExpression(node.then)} : ${reconstructExpression(node.else)}`;
1030
+ }
1031
+ if (node.type === 'function') {
1032
+ const proc = reconstructExpression(node.procedure);
1033
+ const args = (node.arguments || []).map(reconstructExpression).join(', ');
1034
+ return `${proc}(${args})`;
1035
+ }
1036
+ if (node.type === 'unary' && node.value === '{') {
1037
+ const entries = (node.lhs || [])
1038
+ .map(([k, v]) => `"${k.value}": ${reconstructExpression(v)}`)
1039
+ .join(', ');
1040
+ return `{${entries}}`;
435
1041
  }
436
- onSourceValueChange(newValue) {
437
- if (this.arrayPerFieldMode) {
438
- const current = (typeof this.value === 'object' && this.value !== null) ? { ...this.value } : {};
439
- current['_source'] = newValue || '';
440
- this.valueChange.emit(current);
1042
+ return '...';
1043
+ }
1044
+ function formatFieldEntry(field, indent = ' ') {
1045
+ if (field.children && field.children.length > 0) {
1046
+ const childEntries = field.children.map((c) => formatFieldEntry(c, indent + ' ')).join(',\n');
1047
+ return `${indent}"${field.name}": {\n${childEntries}\n${indent}}`;
1048
+ }
1049
+ // Value is already valid JSONata (e.g. $doc.x.y, "string", 42, or raw expression)
1050
+ const value = field.value || 'null';
1051
+ return `${indent}"${field.name}": ${value}`;
1052
+ }
1053
+
1054
+ class MappingBuilderComponent {
1055
+ expression = '';
1056
+ templateFields = [];
1057
+ suggestions = null;
1058
+ disabled = false;
1059
+ expressionChange = new EventEmitter();
1060
+ fields = [];
1061
+ allSuggestions = [];
1062
+ collapsedPaths = new Set();
1063
+ initialCollapseApplied = false;
1064
+ ngOnChanges(changes) {
1065
+ // Skip re-parse only when expression alone changed (from our own emit)
1066
+ const expressionChanged = !!changes['expression'];
1067
+ const templateFieldsChanged = !!changes['templateFields'];
1068
+ if (expressionChanged && !templateFieldsChanged && !changes['expression'].firstChange) {
1069
+ return; // Don't re-parse when we emit changes ourselves
441
1070
  }
442
- else {
443
- this.valueChange.emit(newValue || undefined);
1071
+ if (expressionChanged || templateFieldsChanged) {
1072
+ this.rebuildFields();
1073
+ if (!this.initialCollapseApplied && this.fields.length > 0) {
1074
+ this.collapseAll();
1075
+ this.initialCollapseApplied = true;
1076
+ }
1077
+ }
1078
+ if (changes['suggestions']) {
1079
+ this.buildSuggestionList();
444
1080
  }
445
1081
  }
446
- toggleArrayPerFieldMode() {
447
- this.arrayPerFieldMode = !this.arrayPerFieldMode;
448
- if (this.arrayPerFieldMode) {
449
- const currentSource = this.getSourceValue();
450
- this.valueChange.emit({ _source: currentSource });
1082
+ onNestedValueChange(path, value) {
1083
+ const field = this.getFieldAtPath(path);
1084
+ if (field) {
1085
+ field.value = value;
1086
+ this.emit();
451
1087
  }
452
- else {
453
- const source = this.getSourceValue();
454
- this.valueChange.emit(source || undefined);
1088
+ }
1089
+ onNestedModeToggle(path) {
1090
+ const field = this.getFieldAtPath(path);
1091
+ if (field) {
1092
+ field.mode = field.mode === 'ref' ? 'raw' : 'ref';
1093
+ this.emit();
455
1094
  }
456
1095
  }
457
- onItemFieldChange(childName, sourceFieldName) {
458
- const current = (typeof this.value === 'object' && this.value !== null) ? { ...this.value } : { _source: '' };
459
- if (sourceFieldName && sourceFieldName.trim().length > 0) {
460
- current[childName] = sourceFieldName;
1096
+ isRequired(fieldName) {
1097
+ return this.templateFields?.find((tf) => tf.name === fieldName)?.required ?? false;
1098
+ }
1099
+ isCollapsed(path) {
1100
+ return this.collapsedPaths.has(path.join('.'));
1101
+ }
1102
+ toggleCollapse(path) {
1103
+ const key = path.join('.');
1104
+ if (this.collapsedPaths.has(key)) {
1105
+ this.collapsedPaths.delete(key);
461
1106
  }
462
1107
  else {
463
- delete current[childName];
1108
+ this.collapsedPaths.add(key);
464
1109
  }
465
- this.valueChange.emit(current);
466
1110
  }
467
- getItemFieldValue(childName) {
468
- if (typeof this.value === 'object' && this.value !== null) {
469
- return this.value[childName] || '';
1111
+ collapseAll() {
1112
+ this.collapsedPaths.clear();
1113
+ this.fields.forEach((field, i) => {
1114
+ if (field.children) {
1115
+ this.collapsedPaths.add(String(i));
1116
+ this.collapseChildren(field.children, [i]);
1117
+ }
1118
+ });
1119
+ }
1120
+ collapseChildren(children, parentPath) {
1121
+ children.forEach((child, j) => {
1122
+ if (child.children) {
1123
+ this.collapsedPaths.add([...parentPath, j].join('.'));
1124
+ this.collapseChildren(child.children, [...parentPath, j]);
1125
+ }
1126
+ });
1127
+ }
1128
+ getFieldAtPath(path) {
1129
+ if (path.length === 0)
1130
+ return null;
1131
+ let current = this.fields[path[0]];
1132
+ for (let i = 1; i < path.length; i++) {
1133
+ if (!current.children)
1134
+ return null;
1135
+ current = current.children[path[i]];
470
1136
  }
471
- return '';
1137
+ return current;
472
1138
  }
473
- hasArrayChildren() {
474
- return !!(this.field?.children && this.field.children.length > 0);
1139
+ emit() {
1140
+ const jsonata = builderToJsonata(this.fields);
1141
+ this.expressionChange.emit(jsonata);
475
1142
  }
476
- updateCompleteness() {
477
- if (!this.field?.children || this.field.children.length === 0) {
478
- this.totalRequired = this.field?.required ? 1 : 0;
479
- this.mappedCount = this.getSourceValue() ? (this.field?.required ? 1 : 0) : 0;
1143
+ /**
1144
+ * Ensure all template fields have a corresponding builder field.
1145
+ * Adds missing fields with empty values.
1146
+ */
1147
+ buildSuggestionList() {
1148
+ if (!this.suggestions) {
1149
+ this.allSuggestions = [];
480
1150
  return;
481
1151
  }
482
- if (typeof this.value === 'object' && this.value !== null && '_source' in this.value) {
483
- let total = 0;
484
- let mapped = 0;
485
- if (this.field?.required) {
486
- total++;
487
- if (this.value['_source'] && this.value['_source'].trim().length > 0) {
488
- mapped++;
489
- }
1152
+ const docSuggestions = (this.suggestions.doc || []).map((p) => `$doc.${p}`);
1153
+ const pvSuggestions = (this.suggestions.pv || []).map((p) => `$pv.${p}`);
1154
+ this.allSuggestions = [...docSuggestions, ...pvSuggestions];
1155
+ }
1156
+ /**
1157
+ * Rebuild fields using template fields as the source of truth.
1158
+ * Expression values fill in where available; unmapped fields show empty.
1159
+ */
1160
+ rebuildFields() {
1161
+ const parsed = parseJsonataToBuilder(this.expression);
1162
+ const parsedByName = new Map(parsed.map((f) => [f.name, f]));
1163
+ if (!this.templateFields || this.templateFields.length === 0) {
1164
+ // No template fields yet — use whatever we parsed
1165
+ this.fields = parsed;
1166
+ return;
1167
+ }
1168
+ // Template fields drive the structure
1169
+ this.fields = this.templateFields.map((tf) => {
1170
+ const existing = parsedByName.get(tf.name);
1171
+ if (existing) {
1172
+ return existing;
490
1173
  }
491
- for (const child of this.field.children) {
492
- if (child.required) {
493
- total++;
494
- const val = this.value[child.name];
495
- if (typeof val === 'string' && val.trim().length > 0) {
496
- mapped++;
497
- }
498
- }
1174
+ if (tf.fieldType === 'OBJECT' && tf.children?.length) {
1175
+ return {
1176
+ name: tf.name,
1177
+ mode: 'ref',
1178
+ value: '',
1179
+ children: tf.children.map((c) => ({ name: c.name, mode: 'ref', value: '' })),
1180
+ };
1181
+ }
1182
+ return { name: tf.name, mode: 'ref', value: '' };
1183
+ });
1184
+ // Include extra fields from expression not in the template schema
1185
+ for (const p of parsed) {
1186
+ if (!this.templateFields.find((tf) => tf.name === p.name)) {
1187
+ this.fields.push(p);
499
1188
  }
500
- this.mappedCount = mapped;
501
- this.totalRequired = total;
502
- }
503
- else {
504
- this.totalRequired = this.field?.required ? 1 : 0;
505
- this.mappedCount = this.getSourceValue() ? (this.field?.required ? 1 : 0) : 0;
506
1189
  }
507
1190
  }
508
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ArrayFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
509
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: ArrayFieldComponent, isStandalone: true, selector: "epistola-array-field", inputs: { field: "field", value: "value", pluginId: "pluginId", caseDefinitionKey: "caseDefinitionKey", processVariables: "processVariables", disabled: "disabled" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"field-array\">\n <div class=\"field-array-header\" (click)=\"toggleExpanded()\" [class.field-required-unmapped]=\"field.required && !getSourceValue()\">\n <span class=\"expand-icon\">{{ expanded ? '\u25BC' : '\u25B6' }}</span>\n <span class=\"field-name\">{{ field.name }}</span>\n <span class=\"field-meta\">(array{{ field.required ? ', required' : '' }})</span>\n <span class=\"completeness-badge\" *ngIf=\"totalRequired > 0 && !expanded\">\n {{ mappedCount }}/{{ totalRequired }}\n </span>\n <span class=\"mapped-indicator\" *ngIf=\"totalRequired === 0 && getSourceValue() && !expanded\">\u2713</span>\n </div>\n <div class=\"field-array-content\" *ngIf=\"expanded\">\n <!-- Source collection input -->\n <div class=\"field-row\">\n <div class=\"field-label\">\n <span class=\"field-name\">{{ 'mapCollectionTo' | pluginTranslate: pluginId | async }}</span>\n </div>\n <epistola-value-input\n [name]=\"'field_' + field.path\"\n [value]=\"getSourceValue()\"\n [pluginId]=\"pluginId\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n [processVariables]=\"processVariables\"\n [disabled]=\"disabled\"\n (valueChange)=\"onSourceValueChange($event)\"\n ></epistola-value-input>\n </div>\n\n <!-- Per-item field mapping toggle -->\n <div class=\"array-per-field-toggle\" *ngIf=\"hasArrayChildren()\">\n <label class=\"toggle-label\">\n <input\n type=\"checkbox\"\n [checked]=\"arrayPerFieldMode\"\n [disabled]=\"disabled\"\n (change)=\"toggleArrayPerFieldMode()\"\n />\n <span>{{ 'itemFieldMapping' | pluginTranslate: pluginId | async }}</span>\n </label>\n </div>\n\n <!-- Per-item field mappings -->\n <div class=\"array-item-fields\" *ngIf=\"arrayPerFieldMode && hasArrayChildren()\">\n <div class=\"item-fields-header\">\n <span class=\"item-fields-title\">{{ 'itemFieldMappingTitle' | pluginTranslate: pluginId | async }}</span>\n </div>\n <div class=\"item-field-row\" *ngFor=\"let child of field.children\">\n <div class=\"item-field-label\">\n <span class=\"field-name\">{{ child.name }}</span>\n <span class=\"field-meta\">({{ child.type }}{{ child.required ? ', required' : '' }})</span>\n </div>\n <div class=\"item-field-input\">\n <v-input\n [name]=\"'itemField_' + child.path\"\n [defaultValue]=\"getItemFieldValue(child.name)\"\n [disabled]=\"disabled\"\n [placeholder]=\"'sourceFieldPlaceholder' | pluginTranslate: pluginId | async\"\n (valueChange)=\"onItemFieldChange(child.name, $event)\"\n ></v-input>\n </div>\n </div>\n </div>\n </div>\n</div>\n", styles: [".field-array{margin:.25rem 0}.field-array-header{display:flex;align-items:center;gap:.5rem;padding:.5rem .25rem;cursor:pointer;-webkit-user-select:none;user-select:none;border-left:3px solid transparent;border-radius:2px}.field-array-header:hover{background:#f4f4f4}.field-array-header.field-required-unmapped{border-left-color:#dc3545;background-color:#fff5f5}.field-array-header .expand-icon{flex:0 0 1rem;font-size:.75rem;color:#525252}.field-array-header .field-name{font-weight:500}.field-array-header .field-meta{font-size:.8125rem;color:#6c757d}.field-array-header .completeness-badge{margin-left:auto;font-size:.75rem;padding:.125rem .5rem;border-radius:10px;background:#e0e0e0;color:#525252;font-weight:500}.field-array-header .mapped-indicator{margin-left:auto;color:#198754;font-weight:600}.field-array-content{padding-left:1.25rem;border-left:1px solid #e0e0e0;margin-left:.5rem}.field-row{display:flex;align-items:flex-start;gap:.75rem;padding:.5rem 0}.field-label{flex:0 0 200px;min-width:140px;padding-top:.5rem;word-break:break-word}.field-label .field-name{font-weight:500}.array-per-field-toggle{padding:.5rem 0}.array-per-field-toggle .toggle-label{display:flex;align-items:center;gap:.5rem;cursor:pointer;font-size:.875rem;color:#525252}.array-per-field-toggle .toggle-label input[type=checkbox]{cursor:pointer}.array-item-fields{margin-left:.5rem;border-left:1px dashed #c6c6c6;padding:.25rem 0 .5rem 1rem}.item-fields-header{padding-bottom:.25rem}.item-fields-header .item-fields-title{font-size:.8125rem;font-weight:500;color:#6c757d}.item-field-row{display:flex;align-items:center;gap:.75rem;padding:.25rem 0}.item-field-label{flex:0 0 180px;min-width:120px;word-break:break-word}.item-field-label .field-name{font-weight:500;font-size:.875rem}.item-field-label .field-meta{font-size:.75rem;color:#6c757d;margin-left:.25rem}.item-field-input{flex:1;min-width:0}\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: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { 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: "component", type: ValueInputComponent, selector: "epistola-value-input", inputs: ["name", "value", "pluginId", "caseDefinitionKey", "processVariables", "disabled", "placeholder"], outputs: ["valueChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1191
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: MappingBuilderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1192
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: MappingBuilderComponent, isStandalone: true, selector: "epistola-mapping-builder", inputs: { expression: "expression", templateFields: "templateFields", suggestions: "suggestions", disabled: "disabled" }, outputs: { expressionChange: "expressionChange" }, usesOnChanges: true, ngImport: i0, template: `
1193
+ <div class="mapping-builder">
1194
+ <div
1195
+ *ngIf="fields.length === 0 && (!templateFields || templateFields.length === 0)"
1196
+ class="mapping-builder__empty"
1197
+ >
1198
+ {{ 'noTemplateFields' | pluginTranslate: 'epistola' | async }}
1199
+ </div>
1200
+
1201
+ <epistola-builder-field
1202
+ *ngFor="let field of fields; let i = index"
1203
+ [field]="field"
1204
+ [path]="[i]"
1205
+ [suggestions]="allSuggestions"
1206
+ [disabled]="disabled"
1207
+ [collapsed]="isCollapsed([i])"
1208
+ [collapsedPaths]="collapsedPaths"
1209
+ [required]="isRequired(field.name)"
1210
+ (valueChange)="onNestedValueChange($event.path, $event.value)"
1211
+ (modeToggle)="onNestedModeToggle($event)"
1212
+ (collapseToggle)="toggleCollapse($event)"
1213
+ ></epistola-builder-field>
1214
+ </div>
1215
+ `, isInline: true, styles: [".mapping-builder__empty{color:#6f6f6f;font-size:.9em;padding:12px 0}.mapping-builder__row{margin-bottom:8px}.mapping-builder__row--child{margin-left:20px;margin-bottom:4px}.mapping-builder__name{margin-bottom:2px}.mapping-builder__name--clickable{cursor:pointer;-webkit-user-select:none;user-select:none}.mapping-builder__name--clickable:hover{color:#0f62fe}.mapping-builder__chevron{font-size:.7em;margin-right:4px}.mapping-builder__field-name{font-weight:500;font-size:.9em}.mapping-builder__required{color:#da1e28;margin-left:2px}.mapping-builder__type{color:#8d8d8d;font-size:.8em;margin-left:4px}.mapping-builder__value{display:flex;align-items:center;gap:4px}.mapping-builder__input{flex:1;padding:6px 8px;border:1px solid #e0e0e0;border-radius:4px;font-size:.85em;font-family:IBM Plex Mono,monospace}.mapping-builder__input:focus{outline:2px solid #0f62fe;border-color:#0f62fe}.mapping-builder__input--raw{background:#f4f4f4}.mapping-builder__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}.mapping-builder__mode-toggle:hover{background:#f4f4f4}.mapping-builder__children{border-left:2px solid #e0e0e0;padding-left:12px;margin-top:4px}\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: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "component", type: BuilderFieldComponent, selector: "epistola-builder-field", inputs: ["field", "path", "suggestions", "disabled", "collapsed", "required", "collapsedPaths"], outputs: ["valueChange", "modeToggle", "collapseToggle"] }] });
510
1216
  }
511
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ArrayFieldComponent, decorators: [{
1217
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: MappingBuilderComponent, decorators: [{
512
1218
  type: Component,
513
- args: [{ selector: 'epistola-array-field', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, PluginTranslatePipeModule, InputModule, ValueInputComponent], template: "<div class=\"field-array\">\n <div class=\"field-array-header\" (click)=\"toggleExpanded()\" [class.field-required-unmapped]=\"field.required && !getSourceValue()\">\n <span class=\"expand-icon\">{{ expanded ? '\u25BC' : '\u25B6' }}</span>\n <span class=\"field-name\">{{ field.name }}</span>\n <span class=\"field-meta\">(array{{ field.required ? ', required' : '' }})</span>\n <span class=\"completeness-badge\" *ngIf=\"totalRequired > 0 && !expanded\">\n {{ mappedCount }}/{{ totalRequired }}\n </span>\n <span class=\"mapped-indicator\" *ngIf=\"totalRequired === 0 && getSourceValue() && !expanded\">\u2713</span>\n </div>\n <div class=\"field-array-content\" *ngIf=\"expanded\">\n <!-- Source collection input -->\n <div class=\"field-row\">\n <div class=\"field-label\">\n <span class=\"field-name\">{{ 'mapCollectionTo' | pluginTranslate: pluginId | async }}</span>\n </div>\n <epistola-value-input\n [name]=\"'field_' + field.path\"\n [value]=\"getSourceValue()\"\n [pluginId]=\"pluginId\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n [processVariables]=\"processVariables\"\n [disabled]=\"disabled\"\n (valueChange)=\"onSourceValueChange($event)\"\n ></epistola-value-input>\n </div>\n\n <!-- Per-item field mapping toggle -->\n <div class=\"array-per-field-toggle\" *ngIf=\"hasArrayChildren()\">\n <label class=\"toggle-label\">\n <input\n type=\"checkbox\"\n [checked]=\"arrayPerFieldMode\"\n [disabled]=\"disabled\"\n (change)=\"toggleArrayPerFieldMode()\"\n />\n <span>{{ 'itemFieldMapping' | pluginTranslate: pluginId | async }}</span>\n </label>\n </div>\n\n <!-- Per-item field mappings -->\n <div class=\"array-item-fields\" *ngIf=\"arrayPerFieldMode && hasArrayChildren()\">\n <div class=\"item-fields-header\">\n <span class=\"item-fields-title\">{{ 'itemFieldMappingTitle' | pluginTranslate: pluginId | async }}</span>\n </div>\n <div class=\"item-field-row\" *ngFor=\"let child of field.children\">\n <div class=\"item-field-label\">\n <span class=\"field-name\">{{ child.name }}</span>\n <span class=\"field-meta\">({{ child.type }}{{ child.required ? ', required' : '' }})</span>\n </div>\n <div class=\"item-field-input\">\n <v-input\n [name]=\"'itemField_' + child.path\"\n [defaultValue]=\"getItemFieldValue(child.name)\"\n [disabled]=\"disabled\"\n [placeholder]=\"'sourceFieldPlaceholder' | pluginTranslate: pluginId | async\"\n (valueChange)=\"onItemFieldChange(child.name, $event)\"\n ></v-input>\n </div>\n </div>\n </div>\n </div>\n</div>\n", styles: [".field-array{margin:.25rem 0}.field-array-header{display:flex;align-items:center;gap:.5rem;padding:.5rem .25rem;cursor:pointer;-webkit-user-select:none;user-select:none;border-left:3px solid transparent;border-radius:2px}.field-array-header:hover{background:#f4f4f4}.field-array-header.field-required-unmapped{border-left-color:#dc3545;background-color:#fff5f5}.field-array-header .expand-icon{flex:0 0 1rem;font-size:.75rem;color:#525252}.field-array-header .field-name{font-weight:500}.field-array-header .field-meta{font-size:.8125rem;color:#6c757d}.field-array-header .completeness-badge{margin-left:auto;font-size:.75rem;padding:.125rem .5rem;border-radius:10px;background:#e0e0e0;color:#525252;font-weight:500}.field-array-header .mapped-indicator{margin-left:auto;color:#198754;font-weight:600}.field-array-content{padding-left:1.25rem;border-left:1px solid #e0e0e0;margin-left:.5rem}.field-row{display:flex;align-items:flex-start;gap:.75rem;padding:.5rem 0}.field-label{flex:0 0 200px;min-width:140px;padding-top:.5rem;word-break:break-word}.field-label .field-name{font-weight:500}.array-per-field-toggle{padding:.5rem 0}.array-per-field-toggle .toggle-label{display:flex;align-items:center;gap:.5rem;cursor:pointer;font-size:.875rem;color:#525252}.array-per-field-toggle .toggle-label input[type=checkbox]{cursor:pointer}.array-item-fields{margin-left:.5rem;border-left:1px dashed #c6c6c6;padding:.25rem 0 .5rem 1rem}.item-fields-header{padding-bottom:.25rem}.item-fields-header .item-fields-title{font-size:.8125rem;font-weight:500;color:#6c757d}.item-field-row{display:flex;align-items:center;gap:.75rem;padding:.25rem 0}.item-field-label{flex:0 0 180px;min-width:120px;word-break:break-word}.item-field-label .field-name{font-weight:500;font-size:.875rem}.item-field-label .field-meta{font-size:.75rem;color:#6c757d;margin-left:.25rem}.item-field-input{flex:1;min-width:0}\n"] }]
514
- }], propDecorators: { field: [{
515
- type: Input
516
- }], value: [{
517
- type: Input
518
- }], pluginId: [{
1219
+ args: [{ selector: 'epistola-mapping-builder', standalone: true, imports: [CommonModule, FormsModule, PluginTranslatePipeModule, BuilderFieldComponent], template: `
1220
+ <div class="mapping-builder">
1221
+ <div
1222
+ *ngIf="fields.length === 0 && (!templateFields || templateFields.length === 0)"
1223
+ class="mapping-builder__empty"
1224
+ >
1225
+ {{ 'noTemplateFields' | pluginTranslate: 'epistola' | async }}
1226
+ </div>
1227
+
1228
+ <epistola-builder-field
1229
+ *ngFor="let field of fields; let i = index"
1230
+ [field]="field"
1231
+ [path]="[i]"
1232
+ [suggestions]="allSuggestions"
1233
+ [disabled]="disabled"
1234
+ [collapsed]="isCollapsed([i])"
1235
+ [collapsedPaths]="collapsedPaths"
1236
+ [required]="isRequired(field.name)"
1237
+ (valueChange)="onNestedValueChange($event.path, $event.value)"
1238
+ (modeToggle)="onNestedModeToggle($event)"
1239
+ (collapseToggle)="toggleCollapse($event)"
1240
+ ></epistola-builder-field>
1241
+ </div>
1242
+ `, styles: [".mapping-builder__empty{color:#6f6f6f;font-size:.9em;padding:12px 0}.mapping-builder__row{margin-bottom:8px}.mapping-builder__row--child{margin-left:20px;margin-bottom:4px}.mapping-builder__name{margin-bottom:2px}.mapping-builder__name--clickable{cursor:pointer;-webkit-user-select:none;user-select:none}.mapping-builder__name--clickable:hover{color:#0f62fe}.mapping-builder__chevron{font-size:.7em;margin-right:4px}.mapping-builder__field-name{font-weight:500;font-size:.9em}.mapping-builder__required{color:#da1e28;margin-left:2px}.mapping-builder__type{color:#8d8d8d;font-size:.8em;margin-left:4px}.mapping-builder__value{display:flex;align-items:center;gap:4px}.mapping-builder__input{flex:1;padding:6px 8px;border:1px solid #e0e0e0;border-radius:4px;font-size:.85em;font-family:IBM Plex Mono,monospace}.mapping-builder__input:focus{outline:2px solid #0f62fe;border-color:#0f62fe}.mapping-builder__input--raw{background:#f4f4f4}.mapping-builder__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}.mapping-builder__mode-toggle:hover{background:#f4f4f4}.mapping-builder__children{border-left:2px solid #e0e0e0;padding-left:12px;margin-top:4px}\n"] }]
1243
+ }], propDecorators: { expression: [{
519
1244
  type: Input
520
- }], caseDefinitionKey: [{
1245
+ }], templateFields: [{
521
1246
  type: Input
522
- }], processVariables: [{
1247
+ }], suggestions: [{
523
1248
  type: Input
524
1249
  }], disabled: [{
525
1250
  type: Input
526
- }], valueChange: [{
1251
+ }], expressionChange: [{
527
1252
  type: Output
528
1253
  }] } });
529
1254
 
530
- /**
531
- * Recursive field tree component.
532
- * Dispatches SCALAR and ARRAY to dedicated sub-components.
533
- * Handles OBJECT inline to avoid circular import issues (OBJECT children recurse back to this component).
534
- * Uses forwardRef(() => FieldTreeComponent) in imports to allow self-referencing in the template.
535
- */
536
- class FieldTreeComponent {
537
- field;
538
- value = undefined;
539
- pluginId;
1255
+ class MappingPreviewComponent {
1256
+ epistolaPluginService;
1257
+ expression = '';
1258
+ templateFields = [];
540
1259
  caseDefinitionKey = null;
541
- processVariables = [];
542
- disabled = false;
543
- valueChange = new EventEmitter();
544
- // OBJECT-specific state
545
- expanded = false;
546
- mappedCount = 0;
547
- totalRequired = 0;
1260
+ documentId = '';
1261
+ loading = false;
1262
+ result = null;
1263
+ expectedJson = '';
1264
+ missingRequired = [];
1265
+ destroy$ = new Subject();
1266
+ evaluate$ = new Subject();
1267
+ constructor(epistolaPluginService) {
1268
+ this.epistolaPluginService = epistolaPluginService;
1269
+ this.evaluate$.pipe(debounceTime(300), takeUntil(this.destroy$)).subscribe(() => {
1270
+ this.doEvaluate();
1271
+ });
1272
+ }
548
1273
  ngOnChanges(changes) {
549
- if (this.field?.fieldType === 'OBJECT' && (changes['value'] || changes['field'])) {
550
- if (this.field.children) {
551
- const stats = countRequiredMapped(this.field.children, this.value || {});
552
- this.mappedCount = stats.mapped;
553
- this.totalRequired = stats.total;
554
- }
555
- if (!this.expanded && this.totalRequired > 0 && this.mappedCount < this.totalRequired) {
556
- this.expanded = true;
557
- }
1274
+ if (changes['templateFields']) {
1275
+ this.expectedJson = this.buildExpectedJson();
1276
+ }
1277
+ if (changes['expression'] || changes['templateFields']) {
1278
+ this.checkMissingRequired();
558
1279
  }
559
1280
  }
560
- toggleExpanded() {
561
- this.expanded = !this.expanded;
1281
+ ngOnDestroy() {
1282
+ this.destroy$.next();
1283
+ this.destroy$.complete();
1284
+ }
1285
+ runPreview() {
1286
+ this.evaluate$.next();
562
1287
  }
563
- onChildChange(childName, childValue) {
564
- const current = (typeof this.value === 'object' && this.value !== null) ? { ...this.value } : {};
565
- if (childValue === undefined || childValue === null || childValue === '') {
566
- delete current[childName];
567
- }
568
- else {
569
- current[childName] = childValue;
1288
+ doEvaluate() {
1289
+ if (!this.documentId || !this.expression)
1290
+ return;
1291
+ this.loading = true;
1292
+ this.epistolaPluginService.evaluateMapping(this.expression, this.documentId).subscribe({
1293
+ next: (result) => {
1294
+ this.result = result;
1295
+ this.loading = false;
1296
+ if (result.success) {
1297
+ this.checkMissingRequired();
1298
+ }
1299
+ },
1300
+ error: (err) => {
1301
+ this.result = { success: false, result: null, error: err.message || 'Request failed' };
1302
+ this.loading = false;
1303
+ },
1304
+ });
1305
+ }
1306
+ buildExpectedJson() {
1307
+ if (!this.templateFields || this.templateFields.length === 0)
1308
+ return '{}';
1309
+ const obj = {};
1310
+ for (const field of this.templateFields) {
1311
+ const type = field.type || 'any';
1312
+ obj[field.name] = field.required ? `${type} (required)` : type;
570
1313
  }
571
- this.valueChange.emit(Object.keys(current).length > 0 ? current : undefined);
1314
+ return JSON.stringify(obj, null, 2);
572
1315
  }
573
- getChildValue(childName) {
574
- if (typeof this.value === 'object' && this.value !== null) {
575
- return this.value[childName];
1316
+ checkMissingRequired() {
1317
+ if (!this.templateFields) {
1318
+ this.missingRequired = [];
1319
+ return;
1320
+ }
1321
+ const requiredFields = this.templateFields.filter((f) => f.required).map((f) => f.name);
1322
+ if (!this.result?.success || !this.result.result) {
1323
+ // If no evaluation result yet, check statically from expression
1324
+ this.missingRequired = requiredFields;
1325
+ return;
576
1326
  }
577
- return undefined;
1327
+ const producedKeys = new Set(Object.keys(this.result.result));
1328
+ this.missingRequired = requiredFields.filter((f) => !producedKeys.has(f));
578
1329
  }
579
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: FieldTreeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
580
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.20", type: FieldTreeComponent, isStandalone: true, selector: "epistola-field-tree", inputs: { field: "field", value: "value", pluginId: "pluginId", caseDefinitionKey: "caseDefinitionKey", processVariables: "processVariables", disabled: "disabled" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: "@switch (field.fieldType) {\n @case ('SCALAR') {\n <epistola-scalar-field\n [field]=\"field\"\n [value]=\"value\"\n [pluginId]=\"pluginId\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n [processVariables]=\"processVariables\"\n [disabled]=\"disabled\"\n (valueChange)=\"valueChange.emit($event)\"\n ></epistola-scalar-field>\n }\n @case ('OBJECT') {\n <div class=\"field-object\">\n <div class=\"field-object-header\" (click)=\"toggleExpanded()\" [class.field-required-unmapped]=\"totalRequired > 0 && mappedCount < totalRequired\">\n <span class=\"expand-icon\">{{ expanded ? '\u25BC' : '\u25B6' }}</span>\n <span class=\"field-name\">{{ field.name }}</span>\n <span class=\"field-meta\">(object{{ field.required ? ', required' : '' }})</span>\n <span class=\"completeness-badge\" *ngIf=\"totalRequired > 0 && !expanded\">\n {{ mappedCount }}/{{ totalRequired }}\n </span>\n </div>\n <div class=\"field-object-children\" *ngIf=\"expanded\">\n <epistola-field-tree\n *ngFor=\"let child of field.children\"\n [field]=\"child\"\n [value]=\"getChildValue(child.name)\"\n [pluginId]=\"pluginId\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n [processVariables]=\"processVariables\"\n [disabled]=\"disabled\"\n (valueChange)=\"onChildChange(child.name, $event)\"\n ></epistola-field-tree>\n </div>\n </div>\n }\n @case ('ARRAY') {\n <epistola-array-field\n [field]=\"field\"\n [value]=\"value\"\n [pluginId]=\"pluginId\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n [processVariables]=\"processVariables\"\n [disabled]=\"disabled\"\n (valueChange)=\"valueChange.emit($event)\"\n ></epistola-array-field>\n }\n}\n", styles: [".field-object{margin:.25rem 0}.field-object-header{display:flex;align-items:center;gap:.5rem;padding:.5rem .25rem;cursor:pointer;-webkit-user-select:none;user-select:none;border-left:3px solid transparent;border-radius:2px}.field-object-header:hover{background:#f4f4f4}.field-object-header.field-required-unmapped{border-left-color:#dc3545;background-color:#fff5f5}.field-object-header .expand-icon{flex:0 0 1rem;font-size:.75rem;color:#525252}.field-object-header .field-name{font-weight:500}.field-object-header .field-meta{font-size:.8125rem;color:#6c757d}.field-object-header .completeness-badge{margin-left:auto;font-size:.75rem;padding:.125rem .5rem;border-radius:10px;background:#e0e0e0;color:#525252;font-weight:500}.field-object-children{padding-left:1.25rem;border-left:1px solid #e0e0e0;margin-left:.5rem}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => FieldTreeComponent), selector: "epistola-field-tree", inputs: ["field", "value", "pluginId", "caseDefinitionKey", "processVariables", "disabled"], outputs: ["valueChange"] }, { kind: "ngmodule", type: i0.forwardRef(() => CommonModule) }, { kind: "directive", type: i0.forwardRef(() => i1$1.NgForOf), selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i0.forwardRef(() => i1$1.NgIf), selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: i0.forwardRef(() => PluginTranslatePipeModule) }, { kind: "component", type: i0.forwardRef(() => ScalarFieldComponent), selector: "epistola-scalar-field", inputs: ["field", "value", "pluginId", "caseDefinitionKey", "processVariables", "disabled"], outputs: ["valueChange"] }, { kind: "component", type: i0.forwardRef(() => ArrayFieldComponent), selector: "epistola-array-field", inputs: ["field", "value", "pluginId", "caseDefinitionKey", "processVariables", "disabled"], outputs: ["valueChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1330
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: MappingPreviewComponent, deps: [{ token: EpistolaPluginService }], target: i0.ɵɵFactoryTarget.Component });
1331
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: MappingPreviewComponent, isStandalone: true, selector: "epistola-mapping-preview", inputs: { expression: "expression", templateFields: "templateFields", caseDefinitionKey: "caseDefinitionKey" }, usesOnChanges: true, ngImport: i0, template: `
1332
+ <div class="preview">
1333
+ <div class="preview__header">
1334
+ <span class="preview__title">{{
1335
+ 'previewTitle' | pluginTranslate: 'epistola' | async
1336
+ }}</span>
1337
+ <div class="preview__controls">
1338
+ <input
1339
+ type="text"
1340
+ class="preview__doc-input"
1341
+ [ngModel]="documentId"
1342
+ (ngModelChange)="documentId = $event"
1343
+ [placeholder]="'previewDocPlaceholder' | pluginTranslate: 'epistola' | async"
1344
+ />
1345
+ <button
1346
+ class="preview__run-btn"
1347
+ (click)="runPreview()"
1348
+ [disabled]="!documentId || !expression || loading"
1349
+ >
1350
+ &#x25B6;
1351
+ </button>
1352
+ </div>
1353
+ </div>
1354
+
1355
+ <div class="preview__panels">
1356
+ <!-- Expected structure -->
1357
+ <div class="preview__panel">
1358
+ <div class="preview__panel-label">
1359
+ {{ 'previewExpected' | pluginTranslate: 'epistola' | async }}
1360
+ </div>
1361
+ <pre class="preview__code">{{ expectedJson }}</pre>
1362
+ </div>
1363
+
1364
+ <!-- Produced output -->
1365
+ <div class="preview__panel">
1366
+ <div class="preview__panel-label">
1367
+ {{ 'previewProduced' | pluginTranslate: 'epistola' | async }}
1368
+ </div>
1369
+ <div *ngIf="loading" class="preview__loading">...</div>
1370
+ <pre *ngIf="!loading && result?.success" class="preview__code">{{
1371
+ result.result | json
1372
+ }}</pre>
1373
+ <div *ngIf="!loading && result && !result.success" class="preview__error">
1374
+ {{ result.error }}
1375
+ </div>
1376
+ <div *ngIf="!loading && !result" class="preview__placeholder">
1377
+ {{ 'previewRunHint' | pluginTranslate: 'epistola' | async }}
1378
+ </div>
1379
+ </div>
1380
+ </div>
1381
+
1382
+ <!-- Missing fields warning -->
1383
+ <div *ngIf="missingRequired.length > 0" class="preview__warnings">
1384
+ <span class="preview__warning-icon">&#x26A0;</span>
1385
+ {{ 'previewMissing' | pluginTranslate: 'epistola' | async }}:
1386
+ <strong>{{ missingRequired.join(', ') }}</strong>
1387
+ </div>
1388
+ </div>
1389
+ `, 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" }] });
581
1390
  }
582
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: FieldTreeComponent, decorators: [{
1391
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: MappingPreviewComponent, decorators: [{
583
1392
  type: Component,
584
- args: [{ selector: 'epistola-field-tree', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, PluginTranslatePipeModule, ScalarFieldComponent, ArrayFieldComponent, forwardRef(() => FieldTreeComponent)], template: "@switch (field.fieldType) {\n @case ('SCALAR') {\n <epistola-scalar-field\n [field]=\"field\"\n [value]=\"value\"\n [pluginId]=\"pluginId\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n [processVariables]=\"processVariables\"\n [disabled]=\"disabled\"\n (valueChange)=\"valueChange.emit($event)\"\n ></epistola-scalar-field>\n }\n @case ('OBJECT') {\n <div class=\"field-object\">\n <div class=\"field-object-header\" (click)=\"toggleExpanded()\" [class.field-required-unmapped]=\"totalRequired > 0 && mappedCount < totalRequired\">\n <span class=\"expand-icon\">{{ expanded ? '\u25BC' : '\u25B6' }}</span>\n <span class=\"field-name\">{{ field.name }}</span>\n <span class=\"field-meta\">(object{{ field.required ? ', required' : '' }})</span>\n <span class=\"completeness-badge\" *ngIf=\"totalRequired > 0 && !expanded\">\n {{ mappedCount }}/{{ totalRequired }}\n </span>\n </div>\n <div class=\"field-object-children\" *ngIf=\"expanded\">\n <epistola-field-tree\n *ngFor=\"let child of field.children\"\n [field]=\"child\"\n [value]=\"getChildValue(child.name)\"\n [pluginId]=\"pluginId\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n [processVariables]=\"processVariables\"\n [disabled]=\"disabled\"\n (valueChange)=\"onChildChange(child.name, $event)\"\n ></epistola-field-tree>\n </div>\n </div>\n }\n @case ('ARRAY') {\n <epistola-array-field\n [field]=\"field\"\n [value]=\"value\"\n [pluginId]=\"pluginId\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n [processVariables]=\"processVariables\"\n [disabled]=\"disabled\"\n (valueChange)=\"valueChange.emit($event)\"\n ></epistola-array-field>\n }\n}\n", styles: [".field-object{margin:.25rem 0}.field-object-header{display:flex;align-items:center;gap:.5rem;padding:.5rem .25rem;cursor:pointer;-webkit-user-select:none;user-select:none;border-left:3px solid transparent;border-radius:2px}.field-object-header:hover{background:#f4f4f4}.field-object-header.field-required-unmapped{border-left-color:#dc3545;background-color:#fff5f5}.field-object-header .expand-icon{flex:0 0 1rem;font-size:.75rem;color:#525252}.field-object-header .field-name{font-weight:500}.field-object-header .field-meta{font-size:.8125rem;color:#6c757d}.field-object-header .completeness-badge{margin-left:auto;font-size:.75rem;padding:.125rem .5rem;border-radius:10px;background:#e0e0e0;color:#525252;font-weight:500}.field-object-children{padding-left:1.25rem;border-left:1px solid #e0e0e0;margin-left:.5rem}\n"] }]
585
- }], propDecorators: { field: [{
586
- type: Input
587
- }], value: [{
1393
+ args: [{ selector: 'epistola-mapping-preview', standalone: true, imports: [CommonModule, FormsModule, PluginTranslatePipeModule], template: `
1394
+ <div class="preview">
1395
+ <div class="preview__header">
1396
+ <span class="preview__title">{{
1397
+ 'previewTitle' | pluginTranslate: 'epistola' | async
1398
+ }}</span>
1399
+ <div class="preview__controls">
1400
+ <input
1401
+ type="text"
1402
+ class="preview__doc-input"
1403
+ [ngModel]="documentId"
1404
+ (ngModelChange)="documentId = $event"
1405
+ [placeholder]="'previewDocPlaceholder' | pluginTranslate: 'epistola' | async"
1406
+ />
1407
+ <button
1408
+ class="preview__run-btn"
1409
+ (click)="runPreview()"
1410
+ [disabled]="!documentId || !expression || loading"
1411
+ >
1412
+ &#x25B6;
1413
+ </button>
1414
+ </div>
1415
+ </div>
1416
+
1417
+ <div class="preview__panels">
1418
+ <!-- Expected structure -->
1419
+ <div class="preview__panel">
1420
+ <div class="preview__panel-label">
1421
+ {{ 'previewExpected' | pluginTranslate: 'epistola' | async }}
1422
+ </div>
1423
+ <pre class="preview__code">{{ expectedJson }}</pre>
1424
+ </div>
1425
+
1426
+ <!-- Produced output -->
1427
+ <div class="preview__panel">
1428
+ <div class="preview__panel-label">
1429
+ {{ 'previewProduced' | pluginTranslate: 'epistola' | async }}
1430
+ </div>
1431
+ <div *ngIf="loading" class="preview__loading">...</div>
1432
+ <pre *ngIf="!loading && result?.success" class="preview__code">{{
1433
+ result.result | json
1434
+ }}</pre>
1435
+ <div *ngIf="!loading && result && !result.success" class="preview__error">
1436
+ {{ result.error }}
1437
+ </div>
1438
+ <div *ngIf="!loading && !result" class="preview__placeholder">
1439
+ {{ 'previewRunHint' | pluginTranslate: 'epistola' | async }}
1440
+ </div>
1441
+ </div>
1442
+ </div>
1443
+
1444
+ <!-- Missing fields warning -->
1445
+ <div *ngIf="missingRequired.length > 0" class="preview__warnings">
1446
+ <span class="preview__warning-icon">&#x26A0;</span>
1447
+ {{ 'previewMissing' | pluginTranslate: 'epistola' | async }}:
1448
+ <strong>{{ missingRequired.join(', ') }}</strong>
1449
+ </div>
1450
+ </div>
1451
+ `, 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"] }]
1452
+ }], ctorParameters: () => [{ type: EpistolaPluginService }], propDecorators: { expression: [{
588
1453
  type: Input
589
- }], pluginId: [{
1454
+ }], templateFields: [{
590
1455
  type: Input
591
1456
  }], caseDefinitionKey: [{
592
1457
  type: Input
593
- }], processVariables: [{
594
- type: Input
595
- }], disabled: [{
596
- type: Input
597
- }], valueChange: [{
598
- type: Output
599
1458
  }] } });
600
1459
 
1460
+ const FORM_REF_PREFIX$1 = 'form:';
601
1461
  /**
602
- * Top-level wrapper that hosts FieldTreeComponent instances for each top-level template field.
603
- * Manages the full nested mapping object, completeness tracking, and emits mapping changes.
1462
+ * Detect if a string value is a JSONata expression (vs a plain literal).
1463
+ * Checks for characters that indicate JSONata operators: $, &, (, {, ?, [
604
1464
  */
605
- class DataMappingTreeComponent {
606
- pluginId;
607
- templateFields = [];
608
- prefillMapping = {};
609
- disabled = false;
610
- caseDefinitionKey = null;
611
- processVariables = [];
612
- mappingChange = new EventEmitter();
613
- requiredFieldsStatus = new EventEmitter();
614
- mapping = {};
615
- ngOnChanges(changes) {
616
- if (changes['prefillMapping']) {
617
- const mapping = this.prefillMapping;
618
- if (mapping && Object.keys(mapping).length > 0) {
619
- this.mapping = { ...mapping };
1465
+ function isExpression(value) {
1466
+ return /[$&({?\[]/.test(value);
1467
+ }
1468
+ /**
1469
+ * Expand dot-notation keys into nested objects.
1470
+ * e.g. { "beslissing.tekst": "value" } -> { beslissing: { tekst: "value" } }
1471
+ */
1472
+ function expandDotNotation(flat) {
1473
+ const result = {};
1474
+ for (const [key, value] of Object.entries(flat)) {
1475
+ const parts = key.split('.');
1476
+ let current = result;
1477
+ for (let i = 0; i < parts.length - 1; i++) {
1478
+ if (!current[parts[i]] || typeof current[parts[i]] !== 'object') {
1479
+ current[parts[i]] = {};
620
1480
  }
1481
+ current = current[parts[i]];
621
1482
  }
622
- if (changes['templateFields'] || changes['prefillMapping']) {
623
- this.emitRequiredFieldsStatus();
624
- }
1483
+ current[parts[parts.length - 1]] = value;
625
1484
  }
626
- onFieldValueChange(fieldName, value) {
627
- if (value === undefined || value === null || value === '') {
628
- const { [fieldName]: _, ...rest } = this.mapping;
629
- this.mapping = rest;
1485
+ return result;
1486
+ }
1487
+ /**
1488
+ * Given an override mapping (scope -> { inputPath -> "form:<componentKey>" })
1489
+ * and form data, produce the inputOverrides object for the backend.
1490
+ * The "form:" prefix identifies form field references; the remainder is the Formio component key.
1491
+ */
1492
+ function computeInputOverrides(mapping, formData) {
1493
+ const result = {};
1494
+ for (const [scope, fields] of Object.entries(mapping)) {
1495
+ if (scope !== 'doc' && scope !== 'pv')
1496
+ continue;
1497
+ const flatOverrides = {};
1498
+ for (const [inputPath, ref] of Object.entries(fields)) {
1499
+ const formFieldKey = String(ref).startsWith(FORM_REF_PREFIX$1)
1500
+ ? String(ref).substring(FORM_REF_PREFIX$1.length)
1501
+ : String(ref);
1502
+ const value = formData[formFieldKey];
1503
+ if (value !== undefined) {
1504
+ flatOverrides[inputPath] = value;
1505
+ }
630
1506
  }
631
- else {
632
- this.mapping = { ...this.mapping, [fieldName]: value };
1507
+ if (Object.keys(flatOverrides).length > 0) {
1508
+ result[scope] = expandDotNotation(flatOverrides);
633
1509
  }
634
- this.mappingChange.emit(this.mapping);
635
- this.emitRequiredFieldsStatus();
636
- }
637
- getFieldValue(fieldName) {
638
- return this.mapping[fieldName];
639
- }
640
- emitRequiredFieldsStatus() {
641
- const stats = countRequiredMapped(this.templateFields, this.mapping);
642
- this.requiredFieldsStatus.emit(stats);
643
1510
  }
644
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DataMappingTreeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
645
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: DataMappingTreeComponent, isStandalone: true, selector: "epistola-data-mapping-tree", inputs: { pluginId: "pluginId", templateFields: "templateFields", prefillMapping: "prefillMapping", disabled: "disabled", caseDefinitionKey: "caseDefinitionKey", processVariables: "processVariables" }, outputs: { mappingChange: "mappingChange", requiredFieldsStatus: "requiredFieldsStatus" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"data-mapping-tree\">\n <div class=\"mapping-header\">\n <h5>{{ 'dataMappingTitle' | pluginTranslate: pluginId | async }}</h5>\n <p class=\"helper-text\">{{ 'dataMappingDescription' | pluginTranslate: pluginId | async }}</p>\n </div>\n\n <div class=\"field-tree-root\" *ngIf=\"templateFields.length > 0\">\n <epistola-field-tree\n *ngFor=\"let field of templateFields\"\n [field]=\"field\"\n [value]=\"getFieldValue(field.name)\"\n [pluginId]=\"pluginId\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n [processVariables]=\"processVariables\"\n [disabled]=\"disabled\"\n (valueChange)=\"onFieldValueChange(field.name, $event)\"\n ></epistola-field-tree>\n </div>\n\n <div class=\"no-fields\" *ngIf=\"templateFields.length === 0\">\n <p>{{ 'noTemplateFields' | pluginTranslate: pluginId | async }}</p>\n </div>\n</div>\n", styles: [".data-mapping-tree{margin-top:1rem;margin-bottom:1rem}.mapping-header{margin-bottom:.75rem}.mapping-header h5{margin-bottom:.25rem;font-weight:600}.mapping-header .helper-text{color:#6c757d;font-size:.875rem;margin-bottom:0}.field-tree-root{border:1px solid #e0e0e0;border-radius:4px;padding:.5rem .75rem}.no-fields{padding:1rem;text-align:center;color:#6c757d;background-color:#f8f9fa;border:1px solid #dee2e6;border-radius:4px}.no-fields p{margin-bottom:0}\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: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "component", type: FieldTreeComponent, selector: "epistola-field-tree", inputs: ["field", "value", "pluginId", "caseDefinitionKey", "processVariables", "disabled"], outputs: ["valueChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1511
+ return result;
646
1512
  }
647
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DataMappingTreeComponent, decorators: [{
648
- type: Component,
649
- args: [{ selector: 'epistola-data-mapping-tree', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [
650
- CommonModule,
651
- PluginTranslatePipeModule,
652
- FieldTreeComponent
653
- ], template: "<div class=\"data-mapping-tree\">\n <div class=\"mapping-header\">\n <h5>{{ 'dataMappingTitle' | pluginTranslate: pluginId | async }}</h5>\n <p class=\"helper-text\">{{ 'dataMappingDescription' | pluginTranslate: pluginId | async }}</p>\n </div>\n\n <div class=\"field-tree-root\" *ngIf=\"templateFields.length > 0\">\n <epistola-field-tree\n *ngFor=\"let field of templateFields\"\n [field]=\"field\"\n [value]=\"getFieldValue(field.name)\"\n [pluginId]=\"pluginId\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n [processVariables]=\"processVariables\"\n [disabled]=\"disabled\"\n (valueChange)=\"onFieldValueChange(field.name, $event)\"\n ></epistola-field-tree>\n </div>\n\n <div class=\"no-fields\" *ngIf=\"templateFields.length === 0\">\n <p>{{ 'noTemplateFields' | pluginTranslate: pluginId | async }}</p>\n </div>\n</div>\n", styles: [".data-mapping-tree{margin-top:1rem;margin-bottom:1rem}.mapping-header{margin-bottom:.75rem}.mapping-header h5{margin-bottom:.25rem;font-weight:600}.mapping-header .helper-text{color:#6c757d;font-size:.875rem;margin-bottom:0}.field-tree-root{border:1px solid #e0e0e0;border-radius:4px;padding:.5rem .75rem}.no-fields{padding:1rem;text-align:center;color:#6c757d;background-color:#f8f9fa;border:1px solid #dee2e6;border-radius:4px}.no-fields p{margin-bottom:0}\n"] }]
654
- }], propDecorators: { pluginId: [{
655
- type: Input
656
- }], templateFields: [{
657
- type: Input
658
- }], prefillMapping: [{
659
- type: Input
660
- }], disabled: [{
661
- type: Input
662
- }], caseDefinitionKey: [{
663
- type: Input
664
- }], processVariables: [{
665
- type: Input
666
- }], mappingChange: [{
667
- type: Output
668
- }], requiredFieldsStatus: [{
669
- type: Output
670
- }] } });
671
1513
 
672
1514
  class GenerateDocumentConfigurationComponent {
673
1515
  epistolaPluginService;
@@ -686,22 +1528,32 @@ class GenerateDocumentConfigurationComponent {
686
1528
  variants$ = new BehaviorSubject(initialResource([]));
687
1529
  environments$ = new BehaviorSubject(initialResource([]));
688
1530
  templateFields$ = new BehaviorSubject(initialResource([]));
689
- dataMapping$ = new BehaviorSubject({});
1531
+ dataMapping$ = new BehaviorSubject('');
1532
+ mappingMode = 'simple';
1533
+ toolsCollapsed = true;
1534
+ activeToolTab = 'preview';
690
1535
  outputFormatOptions = [
691
1536
  { id: 'PDF', text: 'PDF' },
692
- { id: 'HTML', text: 'HTML' }
1537
+ { id: 'HTML', text: 'HTML' },
693
1538
  ];
694
1539
  selectedCatalogId$ = new BehaviorSubject('');
695
1540
  /** Composite ID: "catalogId/templateId" */
696
1541
  selectedTemplateId$ = new BehaviorSubject('');
697
1542
  selectedVariantId$ = new BehaviorSubject('');
698
1543
  variantSelectionMode = 'explicit';
1544
+ variantIdExpressionMode = false;
1545
+ variantIdExpression = '';
1546
+ filenameExpressionMode = false;
1547
+ filenameExpression = '';
699
1548
  variantAttributeEntries = [];
700
1549
  availableAttributeKeys = [];
701
1550
  caseDefinitionKey = null;
702
1551
  processVariables = [];
1552
+ expressionFunctions = [];
1553
+ variableSuggestions = null;
703
1554
  requiredFieldsStatus = { mapped: 0, total: 0 };
704
1555
  prefillDataMapping = {};
1556
+ validationErrors$ = new BehaviorSubject([]);
705
1557
  destroy$ = new Subject();
706
1558
  saveSubscription;
707
1559
  formValue$ = new BehaviorSubject(null);
@@ -719,6 +1571,7 @@ class GenerateDocumentConfigurationComponent {
719
1571
  this.initContext();
720
1572
  this.initPluginConfiguration();
721
1573
  this.initCascade();
1574
+ this.loadExpressionFunctions();
722
1575
  this.openSaveSubscription();
723
1576
  }
724
1577
  ngOnDestroy() {
@@ -745,8 +1598,8 @@ class GenerateDocumentConfigurationComponent {
745
1598
  }
746
1599
  this.handleValid(formValue);
747
1600
  }
748
- onDataMappingChange(mapping) {
749
- this.dataMapping$.next(mapping);
1601
+ onDataMappingChange(expression) {
1602
+ this.dataMapping$.next(expression);
750
1603
  const currentFormValue = this.formValue$.getValue();
751
1604
  if (currentFormValue) {
752
1605
  this.handleValid(currentFormValue);
@@ -767,7 +1620,10 @@ class GenerateDocumentConfigurationComponent {
767
1620
  this.revalidate();
768
1621
  }
769
1622
  addAttributeEntry() {
770
- this.variantAttributeEntries = [...this.variantAttributeEntries, { key: '', value: '', required: true }];
1623
+ this.variantAttributeEntries = [
1624
+ ...this.variantAttributeEntries,
1625
+ { key: '', value: '', required: true },
1626
+ ];
771
1627
  this.revalidate();
772
1628
  }
773
1629
  removeAttributeEntry(index) {
@@ -777,6 +1633,12 @@ class GenerateDocumentConfigurationComponent {
777
1633
  onAttributeEntryChange() {
778
1634
  this.revalidate();
779
1635
  }
1636
+ onVariantIdExpressionChange() {
1637
+ this.revalidate();
1638
+ }
1639
+ onFilenameExpressionChange() {
1640
+ this.revalidate();
1641
+ }
780
1642
  onKeySelected(entry, value) {
781
1643
  if (value === '__custom__') {
782
1644
  entry._customKey = true;
@@ -817,7 +1679,9 @@ class GenerateDocumentConfigurationComponent {
817
1679
  }
818
1680
  initContext() {
819
1681
  if (this.context$) {
820
- this.context$.pipe(takeUntil(this.destroy$), filter(([context]) => context === 'case')).subscribe(([, params]) => {
1682
+ this.context$
1683
+ .pipe(takeUntil$1(this.destroy$), filter(([context]) => context === 'case'))
1684
+ .subscribe(([, params]) => {
821
1685
  this.caseDefinitionKey = params.caseDefinitionKey;
822
1686
  this.cdr.markForCheck();
823
1687
  });
@@ -826,10 +1690,12 @@ class GenerateDocumentConfigurationComponent {
826
1690
  initPluginConfiguration() {
827
1691
  const sources = [];
828
1692
  if (this.selectedPluginConfigurationData$) {
829
- sources.push(this.selectedPluginConfigurationData$.pipe(filter(config => !!config?.configurationId), map(config => config.configurationId)));
1693
+ sources.push(this.selectedPluginConfigurationData$.pipe(filter((config) => !!config?.configurationId), map((config) => config.configurationId)));
830
1694
  }
831
- sources.push(this.processLinkStateService.selectedProcessLink$.pipe(filter(processLink => !!processLink?.pluginConfigurationId), map(processLink => processLink.pluginConfigurationId)));
832
- merge(...sources).pipe(takeUntil(this.destroy$)).subscribe(configurationId => {
1695
+ sources.push(this.processLinkStateService.selectedProcessLink$.pipe(filter((processLink) => !!processLink?.pluginConfigurationId), map((processLink) => processLink.pluginConfigurationId)));
1696
+ merge(...sources)
1697
+ .pipe(takeUntil$1(this.destroy$))
1698
+ .subscribe((configurationId) => {
833
1699
  this.pluginConfigurationId$.next(configurationId);
834
1700
  });
835
1701
  }
@@ -844,87 +1710,146 @@ class GenerateDocumentConfigurationComponent {
844
1710
  * prefill + templateFields loaded → seed dataMapping
845
1711
  */
846
1712
  initCascade() {
847
- const configId$ = this.pluginConfigurationId$.pipe(filter(id => !!id), distinctUntilChanged());
1713
+ const configId$ = this.pluginConfigurationId$.pipe(filter((id) => !!id), distinctUntilChanged());
848
1714
  // ── Catalogs: load when pluginConfigurationId changes ──
849
- configId$.pipe(takeUntil(this.destroy$), tap(() => this.catalogs$.next(loadingResource(this.catalogs$.getValue().data))), switchMap(configurationId => this.epistolaPluginService.getCatalogs(configurationId).pipe(map(catalogs => successResource(catalogs.map(c => ({ id: c.id, text: c.name })))), catchError(() => of(errorResource([], 'Failed to load catalogs')))))).subscribe(resource => this.catalogs$.next(resource));
1715
+ configId$
1716
+ .pipe(takeUntil$1(this.destroy$), tap(() => this.catalogs$.next(loadingResource(this.catalogs$.getValue().data))), switchMap((configurationId) => this.epistolaPluginService.getCatalogs(configurationId).pipe(map((catalogs) => successResource(catalogs.map((c) => ({ id: c.id, text: c.name })))), catchError(() => of(errorResource([], 'Failed to load catalogs'))))))
1717
+ .subscribe((resource) => this.catalogs$.next(resource));
850
1718
  // ── Environments: load when pluginConfigurationId changes (independent) ──
851
- configId$.pipe(takeUntil(this.destroy$), tap(() => this.environments$.next(loadingResource(this.environments$.getValue().data))), switchMap(configurationId => this.epistolaPluginService.getEnvironments(configurationId).pipe(map(envs => successResource(envs.map(e => ({ id: e.id, text: e.name })))), catchError(() => of(errorResource([], 'Failed to load environments')))))).subscribe(resource => this.environments$.next(resource));
1719
+ configId$
1720
+ .pipe(takeUntil$1(this.destroy$), tap(() => this.environments$.next(loadingResource(this.environments$.getValue().data))), switchMap((configurationId) => this.epistolaPluginService.getEnvironments(configurationId).pipe(map((envs) => successResource(envs.map((e) => ({ id: e.id, text: e.name })))), catchError(() => of(errorResource([], 'Failed to load environments'))))))
1721
+ .subscribe((resource) => this.environments$.next(resource));
852
1722
  // ── Seed selectedCatalogId$ from prefill once catalogs are loaded ──
853
1723
  combineLatest([
854
- this.prefill$.pipe(filter(config => !!config?.catalogId)),
855
- this.catalogs$.pipe(filter(c => !c.loading && c.data.length > 0))
856
- ]).pipe(takeUntil(this.destroy$), take$1(1)).subscribe(([config]) => {
1724
+ this.prefill$.pipe(filter((config) => !!config?.catalogId)),
1725
+ this.catalogs$.pipe(filter((c) => !c.loading && c.data.length > 0)),
1726
+ ])
1727
+ .pipe(takeUntil$1(this.destroy$), take$1(1))
1728
+ .subscribe(([config]) => {
857
1729
  this.selectedCatalogId$.next(config.catalogId);
858
1730
  });
859
1731
  // ── Templates: load when catalogId changes ──
860
- const catalogId$ = this.selectedCatalogId$.pipe(filter(id => !!id), distinctUntilChanged());
861
- combineLatest([configId$, catalogId$]).pipe(takeUntil(this.destroy$), tap(() => this.templates$.next(loadingResource(this.templates$.getValue().data))), switchMap(([configurationId, catalogId]) => this.epistolaPluginService.getTemplates(configurationId, catalogId).pipe(map(templates => successResource(templates.map(t => ({ id: t.id, text: t.name })))), catchError(() => of(errorResource([], 'Failed to load templates')))))).subscribe(resource => this.templates$.next(resource));
1732
+ const catalogId$ = this.selectedCatalogId$.pipe(filter((id) => !!id), distinctUntilChanged());
1733
+ combineLatest([configId$, catalogId$])
1734
+ .pipe(takeUntil$1(this.destroy$), tap(() => this.templates$.next(loadingResource(this.templates$.getValue().data))), switchMap(([configurationId, catalogId]) => this.epistolaPluginService.getTemplates(configurationId, catalogId).pipe(map((templates) => successResource(templates.map((t) => ({ id: t.id, text: t.name })))), catchError(() => of(errorResource([], 'Failed to load templates'))))))
1735
+ .subscribe((resource) => this.templates$.next(resource));
862
1736
  // ── Attributes: load when catalogId changes ──
863
- combineLatest([configId$, catalogId$]).pipe(takeUntil(this.destroy$), switchMap(([configurationId, catalogId]) => this.epistolaPluginService.getAttributes(configurationId, catalogId).pipe(catchError(() => of([]))))).subscribe(attributes => {
864
- this.availableAttributeKeys = attributes.map(a => a.key).sort();
1737
+ combineLatest([configId$, catalogId$])
1738
+ .pipe(takeUntil$1(this.destroy$), switchMap(([configurationId, catalogId]) => this.epistolaPluginService
1739
+ .getAttributes(configurationId, catalogId)
1740
+ .pipe(catchError(() => of([])))))
1741
+ .subscribe((attributes) => {
1742
+ this.availableAttributeKeys = attributes.map((a) => a.key).sort();
865
1743
  this.cdr.markForCheck();
866
1744
  });
867
1745
  // ── Seed selectedTemplateId$ from prefill once templates are loaded ──
868
1746
  combineLatest([
869
- this.prefill$.pipe(filter(config => !!config?.templateId)),
870
- this.templates$.pipe(filter(t => !t.loading && t.data.length > 0))
871
- ]).pipe(takeUntil(this.destroy$), take$1(1)).subscribe(([config]) => {
1747
+ this.prefill$.pipe(filter((config) => !!config?.templateId)),
1748
+ this.templates$.pipe(filter((t) => !t.loading && t.data.length > 0)),
1749
+ ])
1750
+ .pipe(takeUntil$1(this.destroy$), take$1(1))
1751
+ .subscribe(([config]) => {
872
1752
  this.selectedTemplateId$.next(config.templateId);
873
1753
  });
874
1754
  // ── Variants: load when templateId changes ──
875
- const templateId$ = this.selectedTemplateId$.pipe(filter(id => !!id), distinctUntilChanged());
876
- combineLatest([configId$, catalogId$, templateId$]).pipe(takeUntil(this.destroy$), tap(() => this.variants$.next(loadingResource(this.variants$.getValue().data))), switchMap(([configurationId, catalogId, templateId]) => this.epistolaPluginService.getVariants(configurationId, templateId, catalogId).pipe(map(variants => successResource(variants.map(v => ({ id: v.id, text: v.name + this.formatAttributes(v.attributes) })))), catchError(() => of(errorResource([], 'Failed to load variants')))))).subscribe(resource => this.variants$.next(resource));
1755
+ const templateId$ = this.selectedTemplateId$.pipe(filter((id) => !!id), distinctUntilChanged());
1756
+ combineLatest([configId$, catalogId$, templateId$])
1757
+ .pipe(takeUntil$1(this.destroy$), tap(() => this.variants$.next(loadingResource(this.variants$.getValue().data))), switchMap(([configurationId, catalogId, templateId]) => this.epistolaPluginService.getVariants(configurationId, templateId, catalogId).pipe(map((variants) => successResource(variants.map((v) => ({
1758
+ id: v.id,
1759
+ text: v.name + this.formatAttributes(v.attributes),
1760
+ })))), catchError(() => of(errorResource([], 'Failed to load variants'))))))
1761
+ .subscribe((resource) => this.variants$.next(resource));
877
1762
  // ── Template fields: load when templateId changes ──
878
- combineLatest([configId$, catalogId$, templateId$]).pipe(takeUntil(this.destroy$), tap(() => {
1763
+ combineLatest([configId$, catalogId$, templateId$])
1764
+ .pipe(takeUntil$1(this.destroy$), tap(() => {
879
1765
  this.templateFields$.next(loadingResource(this.templateFields$.getValue().data));
880
1766
  this.loadProcessVariables();
881
- }), switchMap(([configurationId, catalogId, templateId]) => this.epistolaPluginService.getTemplateDetails(configurationId, templateId, catalogId).pipe(map(details => successResource(details.fields || [])), catchError(() => of(errorResource([], 'Failed to load template fields')))))).subscribe(resource => this.templateFields$.next(resource));
1767
+ this.loadVariableSuggestions();
1768
+ }), switchMap(([configurationId, catalogId, templateId]) => this.epistolaPluginService
1769
+ .getTemplateDetails(configurationId, templateId, catalogId)
1770
+ .pipe(map((details) => successResource(details.fields || [])), catchError(() => of(errorResource([], 'Failed to load template fields'))))))
1771
+ .subscribe((resource) => this.templateFields$.next(resource));
882
1772
  // ── Seed variant + dataMapping from prefill once templateFields are loaded ──
883
1773
  combineLatest([
884
- this.prefill$.pipe(filter(config => !!config?.templateId)),
885
- this.templateFields$.pipe(filter(tf => !tf.loading && tf.data.length > 0))
886
- ]).pipe(takeUntil(this.destroy$), take$1(1)).subscribe(([config]) => {
1774
+ this.prefill$.pipe(filter((config) => !!config?.templateId)),
1775
+ this.templateFields$.pipe(filter((tf) => !tf.loading && tf.data.length > 0)),
1776
+ ])
1777
+ .pipe(takeUntil$1(this.destroy$), take$1(1))
1778
+ .subscribe(([config]) => {
887
1779
  if (!config)
888
1780
  return;
889
1781
  // Apply variant prefill
890
- if (config.variantAttributes && (Array.isArray(config.variantAttributes) ? config.variantAttributes.length > 0 : Object.keys(config.variantAttributes).length > 0)) {
1782
+ if (config.variantAttributes &&
1783
+ (Array.isArray(config.variantAttributes)
1784
+ ? config.variantAttributes.length > 0
1785
+ : Object.keys(config.variantAttributes).length > 0)) {
891
1786
  this.variantSelectionMode = 'attributes';
892
1787
  if (Array.isArray(config.variantAttributes)) {
893
- this.variantAttributeEntries = config.variantAttributes
894
- .map(e => ({ key: e.key, value: e.value, required: e.required !== false }));
1788
+ this.variantAttributeEntries = config.variantAttributes.map((e) => ({
1789
+ key: e.key,
1790
+ value: e.value,
1791
+ required: e.required !== false,
1792
+ _expressionMode: isExpression(e.value),
1793
+ }));
895
1794
  }
896
1795
  else {
897
- this.variantAttributeEntries = Object.entries(config.variantAttributes)
898
- .map(([key, value]) => ({ key, value: String(value), required: true }));
1796
+ this.variantAttributeEntries = Object.entries(config.variantAttributes).map(([key, value]) => ({ key, value: String(value), required: true }));
899
1797
  }
900
1798
  }
901
1799
  else if (config.variantId) {
902
1800
  this.variantSelectionMode = 'explicit';
903
- this.selectedVariantId$.next(config.variantId);
1801
+ if (isExpression(config.variantId)) {
1802
+ this.variantIdExpressionMode = true;
1803
+ this.variantIdExpression = config.variantId;
1804
+ }
1805
+ else {
1806
+ this.selectedVariantId$.next(config.variantId);
1807
+ }
904
1808
  }
905
- // Apply dataMapping prefill templateFields are guaranteed loaded at this point.
906
- // Use setTimeout to ensure the tree component exists in the DOM (after *ngIf resolves)
907
- // before setting the prefill, so ngOnChanges fires correctly on the child.
1809
+ // Detect expression mode for filename
1810
+ if (config.filename && isExpression(config.filename)) {
1811
+ this.filenameExpressionMode = true;
1812
+ this.filenameExpression = config.filename;
1813
+ }
1814
+ // Apply dataMapping prefill (JSONata expression string)
908
1815
  if (config.dataMapping) {
909
- this.dataMapping$.next(config.dataMapping);
910
- setTimeout(() => {
911
- this.prefillDataMapping = { ...config.dataMapping };
912
- this.cdr.detectChanges();
913
- });
1816
+ const expr = typeof config.dataMapping === 'string' ? config.dataMapping : '';
1817
+ this.dataMapping$.next(expr);
914
1818
  }
915
1819
  else {
916
1820
  this.cdr.detectChanges();
917
1821
  }
918
1822
  });
919
1823
  }
1824
+ loadExpressionFunctions() {
1825
+ this.epistolaPluginService
1826
+ .getExpressionFunctions()
1827
+ .pipe(takeUntil$1(this.destroy$), catchError(() => of([])))
1828
+ .subscribe((functions) => {
1829
+ this.expressionFunctions = functions;
1830
+ this.cdr.markForCheck();
1831
+ });
1832
+ }
920
1833
  loadProcessVariables() {
921
1834
  if (this.caseDefinitionKey) {
922
- this.epistolaPluginService.getProcessVariables(this.caseDefinitionKey).pipe(takeUntil(this.destroy$), catchError(() => of([]))).subscribe(variables => {
1835
+ this.epistolaPluginService
1836
+ .getProcessVariables(this.caseDefinitionKey)
1837
+ .pipe(takeUntil$1(this.destroy$), catchError(() => of([])))
1838
+ .subscribe((variables) => {
923
1839
  this.processVariables = variables;
924
1840
  this.cdr.markForCheck();
925
1841
  });
926
1842
  }
927
1843
  }
1844
+ loadVariableSuggestions() {
1845
+ this.epistolaPluginService
1846
+ .getVariableSuggestions(this.caseDefinitionKey ?? undefined, this.caseDefinitionKey ?? undefined)
1847
+ .pipe(takeUntil$1(this.destroy$), catchError(() => of({ doc: [], pv: [] })))
1848
+ .subscribe((suggestions) => {
1849
+ this.variableSuggestions = suggestions;
1850
+ this.cdr.markForCheck();
1851
+ });
1852
+ }
928
1853
  handleValid(formValue) {
929
1854
  const baseComplete = !!(this.selectedCatalogId$.getValue() &&
930
1855
  formValue?.templateId &&
@@ -933,7 +1858,7 @@ class GenerateDocumentConfigurationComponent {
933
1858
  formValue?.resultProcessVariable);
934
1859
  let variantValid = true;
935
1860
  if (this.variantSelectionMode === 'attributes' && this.variantAttributeEntries.length > 0) {
936
- variantValid = this.variantAttributeEntries.every(e => !!e.key && !!e.value);
1861
+ variantValid = this.variantAttributeEntries.every((e) => !!e.key && !!e.value);
937
1862
  }
938
1863
  const requiredFieldsMapped = this.requiredFieldsStatus.total === 0 ||
939
1864
  this.requiredFieldsStatus.mapped === this.requiredFieldsStatus.total;
@@ -955,25 +1880,66 @@ class GenerateDocumentConfigurationComponent {
955
1880
  environmentId: formValue.environmentId || undefined,
956
1881
  dataMapping: dataMapping,
957
1882
  outputFormat: formValue.outputFormat,
958
- filename: formValue.filename,
1883
+ filename: this.filenameExpressionMode ? this.filenameExpression : formValue.filename,
959
1884
  correlationId: formValue.correlationId || undefined,
960
- resultProcessVariable: formValue.resultProcessVariable
1885
+ resultProcessVariable: formValue.resultProcessVariable,
961
1886
  };
962
1887
  if (this.variantSelectionMode === 'explicit') {
963
- config.variantId = formValue.variantId;
1888
+ config.variantId = this.variantIdExpressionMode
1889
+ ? this.variantIdExpression
1890
+ : formValue.variantId;
964
1891
  }
965
1892
  else {
966
1893
  config.variantAttributes = this.variantAttributeEntries
967
- .filter(e => e.key && e.value)
968
- .map(e => ({ key: e.key, value: e.value, required: e.required }));
1894
+ .filter((e) => e.key && e.value)
1895
+ .map((e) => ({ key: e.key, value: e.value, required: e.required }));
969
1896
  }
970
- this.configuration.emit(config);
1897
+ this.validateAndEmit(config);
971
1898
  }
972
1899
  });
973
1900
  });
974
1901
  }
975
- 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 });
976
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: GenerateDocumentConfigurationComponent, isStandalone: true, selector: "epistola-generate-document-configuration", inputs: { save$: "save$", disabled$: "disabled$", pluginId: "pluginId", prefillConfiguration$: "prefillConfiguration$", selectedPluginConfigurationData$: "selectedPluginConfigurationData$", context$: "context$" }, outputs: { valid: "valid", configuration: "configuration" }, ngImport: i0, template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: disabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n catalogs: catalogs$ | async,\n templates: templates$ | async,\n variants: variants$ | async,\n environments: environments$ | async,\n templateFields: templateFields$ | async,\n selectedCatalogId: selectedCatalogId$ | async,\n selectedTemplateId: selectedTemplateId$ | async\n } as obs\"\n>\n <v-select\n name=\"catalogId\"\n [title]=\"'catalogId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'catalogIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.catalogs.data\"\n [defaultSelectionId]=\"obs.prefill?.catalogId\"\n [disabled]=\"obs.disabled || obs.catalogs.loading\"\n [required]=\"true\"\n [loading]=\"obs.catalogs.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.catalogs.error\" class=\"loading-error\">{{ obs.catalogs.error }}</div>\n\n <v-select\n name=\"templateId\"\n [title]=\"'templateId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'templateIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.templates.data\"\n [defaultSelectionId]=\"obs.prefill?.templateId\"\n [disabled]=\"obs.disabled || obs.templates.loading || !obs.selectedCatalogId\"\n [required]=\"true\"\n [loading]=\"obs.templates.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.templates.error\" class=\"loading-error\">{{ obs.templates.error }}</div>\n\n <!-- Variant selection mode toggle -->\n <div class=\"variant-mode-toggle\" *ngIf=\"obs.selectedTemplateId\">\n <label class=\"variant-mode-label\">{{ 'variantSelectionMode' | pluginTranslate: pluginId | async }}</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 >{{ 'selectByVariant' | pluginTranslate: pluginId | async }}</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 >{{ 'selectByAttributes' | pluginTranslate: pluginId | async }}</button>\n </div>\n </div>\n\n <!-- Explicit variant selection (dropdown) -->\n <v-select\n *ngIf=\"variantSelectionMode === 'explicit'\"\n name=\"variantId\"\n [title]=\"'variantId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'variantIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.variants.data\"\n [defaultSelectionId]=\"obs.prefill?.variantId\"\n [disabled]=\"obs.disabled || obs.variants.loading || !obs.selectedTemplateId\"\n [required]=\"false\"\n [loading]=\"obs.variants.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.variants.error\" class=\"loading-error\">{{ obs.variants.error }}</div>\n\n <!-- Attribute-based variant selection -->\n <div *ngIf=\"variantSelectionMode === 'attributes' && obs.selectedTemplateId\" class=\"variant-attributes-section\">\n <label class=\"variant-attributes-label\">{{ 'variantAttributes' | pluginTranslate: pluginId | async }}</label>\n <div class=\"variant-attributes-list\">\n <div *ngFor=\"let entry of variantAttributeEntries; let i = index\" class=\"variant-attribute-row\">\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>{{ 'attributeKey' | pluginTranslate: pluginId | async }}</option>\n <option *ngFor=\"let key of availableAttributeKeys\" [value]=\"key\">{{ key }}</option>\n <option value=\"__custom__\">{{ 'attributeKeyCustom' | pluginTranslate: pluginId | async }}</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 type=\"button\" class=\"custom-key-cancel\" (click)=\"cancelCustomKey(entry)\" [disabled]=\"obs.disabled\">&times;</button>\n </div>\n <input\n type=\"text\"\n class=\"variant-attribute-input\"\n [placeholder]=\"'attributeValue' | pluginTranslate: pluginId | async\"\n [(ngModel)]=\"entry.value\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <label class=\"variant-attribute-required-toggle\">\n <input\n type=\"checkbox\"\n [(ngModel)]=\"entry.required\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <span class=\"required-label\">{{ (entry.required ? 'attributeRequired' : 'attributePreferred') | pluginTranslate: pluginId | async }}</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 >&times;</button>\n </div>\n </div>\n <button\n type=\"button\"\n class=\"variant-attribute-add-btn\"\n (click)=\"addAttributeEntry()\"\n [disabled]=\"obs.disabled\"\n >+ {{ 'addAttribute' | pluginTranslate: pluginId | async }}</button>\n </div>\n\n <v-select\n name=\"environmentId\"\n [title]=\"'environmentId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'environmentIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.environments.data\"\n [defaultSelectionId]=\"obs.prefill?.environmentId\"\n [disabled]=\"obs.disabled || obs.environments.loading\"\n [required]=\"false\"\n [loading]=\"obs.environments.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.environments.error\" class=\"loading-error\">{{ obs.environments.error }}</div>\n\n <v-select\n name=\"outputFormat\"\n [title]=\"'outputFormat' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'outputFormatTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"outputFormatOptions\"\n [defaultSelectionId]=\"obs.prefill?.outputFormat || 'PDF'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-select>\n\n <v-input\n name=\"filename\"\n [title]=\"'filename' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'filenameTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.filename\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"correlationId\"\n [title]=\"'correlationId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'correlationIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.correlationId\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"resultProcessVariable\"\n [title]=\"'resultProcessVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'resultProcessVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.resultProcessVariable\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n\n<div *ngIf=\"(templateFields$ | async)?.error as templateFieldsError\" class=\"loading-error\">{{ templateFieldsError }}</div>\n\n<epistola-data-mapping-tree\n *ngIf=\"(selectedTemplateId$ | async)\"\n [pluginId]=\"pluginId\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n [disabled]=\"!!(disabled$ | async)\"\n [prefillMapping]=\"prefillDataMapping\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n [processVariables]=\"processVariables\"\n (mappingChange)=\"onDataMappingChange($event)\"\n (requiredFieldsStatus)=\"onRequiredFieldsStatusChange($event)\"\n></epistola-data-mapping-tree>\n\n<div class=\"validation-summary\" *ngIf=\"(selectedTemplateId$ | async) && requiredFieldsStatus.total > 0\">\n <span *ngIf=\"requiredFieldsStatus.mapped === requiredFieldsStatus.total\" class=\"validation-complete\">\n {{ 'requiredFieldsComplete' | pluginTranslate: pluginId | async }}\n </span>\n <span *ngIf=\"requiredFieldsStatus.mapped < requiredFieldsStatus.total\" class=\"validation-incomplete\">\n {{ requiredFieldsStatus.mapped }} / {{ requiredFieldsStatus.total }}\n {{ 'validationSummary' | pluginTranslate: pluginId | async }}\n </span>\n</div>\n", styles: [".loading-error{padding:.25rem .75rem;font-size:.8125rem;color:#dc3545}.validation-summary{margin-top:.5rem;padding:.5rem .75rem;border-radius:4px;font-size:.875rem}.validation-summary .validation-complete{color:#198754}.validation-summary .validation-incomplete{color:#dc3545;font-weight:500}.variant-mode-toggle{margin-bottom:1rem;padding:0 .75rem}.variant-mode-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-mode-buttons{display:flex;gap:0;border:1px solid #d1d5db;border-radius:4px;overflow:hidden;width:fit-content}.variant-mode-btn{padding:.375rem .75rem;font-size:.8125rem;background:#fff;border:none;border-right:1px solid #d1d5db;cursor:pointer;color:#374151;transition:background-color .15s,color .15s}.variant-mode-btn:last-child{border-right:none}.variant-mode-btn:hover:not([disabled]){background:#f3f4f6}.variant-mode-btn.active{background:#2563eb;color:#fff}.variant-mode-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attributes-section{margin-bottom:1rem;padding:0 .75rem}.variant-attributes-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-attributes-list{display:flex;flex-direction:column;gap:.375rem}.variant-attribute-row{display:flex;gap:.375rem;align-items:center}.variant-attribute-input{flex:1;padding:.375rem .5rem;font-size:.8125rem;border:1px solid #d1d5db;border-radius:4px;outline:none}.variant-attribute-input:focus{border-color:#2563eb;box-shadow:0 0 0 1px #2563eb}.variant-attribute-input[disabled]{opacity:.5;background:#f9fafb}.custom-key-input{display:flex;flex:1;gap:.25rem}.custom-key-input .variant-attribute-input{flex:1}.custom-key-cancel{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.custom-key-cancel:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.custom-key-cancel[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-required-toggle{display:flex;align-items:center;gap:.25rem;font-size:.75rem;color:#374151;white-space:nowrap;cursor:pointer}.variant-attribute-required-toggle input[type=checkbox]{margin:0;cursor:pointer}.variant-attribute-required-toggle .required-label{-webkit-user-select:none;user-select:none}.variant-attribute-remove-btn{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-remove-btn:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.variant-attribute-remove-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-add-btn{margin-top:.375rem;padding:.25rem .5rem;font-size:.8125rem;background:none;border:1px dashed #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-add-btn:hover:not([disabled]){color:#2563eb;border-color:#2563eb}.variant-attribute-add-btn[disabled]{opacity:.5;cursor:not-allowed}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i4.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i4.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i4.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i4.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i4.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: FormModule }, { kind: "component", type: i3.FormComponent, selector: "v-form", inputs: ["className"], outputs: ["valueChange"] }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i3.InputComponent, selector: "v-input", inputs: ["name", "type", "title", "titleTranslationKey", "defaultValue", "widthPx", "fullWidth", "margin", "smallMargin", "disabled", "step", "min", "maxLength", "tooltip", "required", "hideNumberSpinBox", "smallLabel", "rows", "clear$", "carbonTheme", "placeholder", "dataTestId", "trim", "presetsTitle", "presetOptions"], outputs: ["valueChange"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i3.SelectComponent, selector: "v-select", inputs: ["items", "defaultSelection", "defaultSelectionId", "defaultSelectionIds", "disabled", "dropUp", "invalid", "multiple", "margin", "widthInPx", "notFoundText", "clearAllText", "clearText", "clearable", "name", "title", "titleTranslationKey", "clearSelectionSubject$", "tooltip", "required", "loading", "loadingText", "placeholder", "smallMargin", "carbonTheme", "appendInline", "warn", "warnText", "dataTestId"], outputs: ["selectedChange"] }, { kind: "component", type: DataMappingTreeComponent, selector: "epistola-data-mapping-tree", inputs: ["pluginId", "templateFields", "prefillMapping", "disabled", "caseDefinitionKey", "processVariables"], outputs: ["mappingChange", "requiredFieldsStatus"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1902
+ /**
1903
+ * Build a JSONata validation request from the config and call the backend.
1904
+ * Only fields that are JSONata expressions get validated:
1905
+ * - dataMapping is always JSONata
1906
+ * - filename / variantId only when their `fx` toggle is on
1907
+ * - variant attribute values only when isExpression() reports true
1908
+ * On invalid response, surface errors and abort the emit.
1909
+ * If the validator endpoint itself fails (network/server), proceed with the
1910
+ * emit — the validation is a quality-of-life check, not a hard gate.
1911
+ */
1912
+ validateAndEmit(config) {
1913
+ const variantAttributeValues = {};
1914
+ if (config.variantAttributes) {
1915
+ for (const attr of config.variantAttributes) {
1916
+ if (isExpression(attr.value)) {
1917
+ variantAttributeValues[attr.key] = attr.value;
1918
+ }
1919
+ }
1920
+ }
1921
+ const request = {
1922
+ dataMapping: config.dataMapping || null,
1923
+ filename: this.filenameExpressionMode ? config.filename : null,
1924
+ variantId: this.variantIdExpressionMode ? config.variantId || null : null,
1925
+ variantAttributeValues: Object.keys(variantAttributeValues).length > 0 ? variantAttributeValues : null,
1926
+ };
1927
+ this.epistolaPluginService
1928
+ .validateJsonata(request)
1929
+ .pipe(take$1(1), catchError(() => of({ valid: true, errors: [] })))
1930
+ .subscribe((result) => {
1931
+ if (result.valid) {
1932
+ this.validationErrors$.next([]);
1933
+ this.configuration.emit(config);
1934
+ }
1935
+ else {
1936
+ this.validationErrors$.next(result.errors);
1937
+ this.cdr.markForCheck();
1938
+ }
1939
+ });
1940
+ }
1941
+ 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 });
1942
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: GenerateDocumentConfigurationComponent, isStandalone: true, selector: "epistola-generate-document-configuration", inputs: { save$: "save$", disabled$: "disabled$", pluginId: "pluginId", prefillConfiguration$: "prefillConfiguration$", selectedPluginConfigurationData$: "selectedPluginConfigurationData$", context$: "context$" }, outputs: { valid: "valid", configuration: "configuration" }, ngImport: i0, template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: disabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n catalogs: catalogs$ | async,\n templates: templates$ | async,\n variants: variants$ | async,\n environments: environments$ | async,\n templateFields: templateFields$ | async,\n selectedCatalogId: selectedCatalogId$ | async,\n selectedTemplateId: selectedTemplateId$ | async,\n validationErrors: validationErrors$ | async,\n } as obs\"\n>\n <div\n *ngIf=\"obs.validationErrors && obs.validationErrors.length > 0\"\n class=\"jsonata-validation-errors\"\n >\n <strong>{{ 'jsonataValidationErrorsHeading' | pluginTranslate: pluginId | async }}</strong>\n <ul>\n <li *ngFor=\"let err of obs.validationErrors\">\n <code>{{ err.field }}</code\n >: {{ err.message }}\n </li>\n </ul>\n </div>\n <v-select\n name=\"catalogId\"\n [title]=\"'catalogId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'catalogIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.catalogs.data\"\n [defaultSelectionId]=\"obs.prefill?.catalogId\"\n [disabled]=\"obs.disabled || obs.catalogs.loading\"\n [required]=\"true\"\n [loading]=\"obs.catalogs.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.catalogs.error\" class=\"loading-error\">{{ obs.catalogs.error }}</div>\n\n <v-select\n name=\"templateId\"\n [title]=\"'templateId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'templateIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.templates.data\"\n [defaultSelectionId]=\"obs.prefill?.templateId\"\n [disabled]=\"obs.disabled || obs.templates.loading || !obs.selectedCatalogId\"\n [required]=\"true\"\n [loading]=\"obs.templates.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.templates.error\" class=\"loading-error\">{{ obs.templates.error }}</div>\n\n <!-- Variant selection mode toggle -->\n <div class=\"variant-mode-toggle\" *ngIf=\"obs.selectedTemplateId\">\n <label class=\"variant-mode-label\">{{\n 'variantSelectionMode' | pluginTranslate: pluginId | async\n }}</label>\n <div class=\"variant-mode-buttons\">\n <button\n type=\"button\"\n class=\"variant-mode-btn\"\n [class.active]=\"variantSelectionMode === 'explicit'\"\n (click)=\"onVariantSelectionModeChange('explicit')\"\n [disabled]=\"obs.disabled\"\n >\n {{ 'selectByVariant' | pluginTranslate: pluginId | async }}\n </button>\n <button\n type=\"button\"\n class=\"variant-mode-btn\"\n [class.active]=\"variantSelectionMode === 'attributes'\"\n (click)=\"onVariantSelectionModeChange('attributes')\"\n [disabled]=\"obs.disabled\"\n >\n {{ 'selectByAttributes' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n </div>\n\n <!-- Explicit variant selection (dropdown or expression) -->\n <div *ngIf=\"variantSelectionMode === 'explicit'\" class=\"field-with-fx\">\n <v-select\n *ngIf=\"!variantIdExpressionMode\"\n name=\"variantId\"\n [title]=\"'variantId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'variantIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.variants.data\"\n [defaultSelectionId]=\"obs.prefill?.variantId\"\n [disabled]=\"obs.disabled || obs.variants.loading || !obs.selectedTemplateId\"\n [required]=\"false\"\n [loading]=\"obs.variants.loading\"\n >\n </v-select>\n <div *ngIf=\"variantIdExpressionMode\" class=\"fx-input-group\">\n <label class=\"fx-input-label\">{{ 'variantId' | pluginTranslate: pluginId | async }}</label>\n <input\n type=\"text\"\n class=\"fx-input\"\n [ngModel]=\"variantIdExpression\"\n (ngModelChange)=\"variantIdExpression = $event; onVariantIdExpressionChange()\"\n [disabled]=\"obs.disabled\"\n placeholder=\"$pv.letterType\"\n />\n </div>\n <button\n type=\"button\"\n class=\"fx-toggle\"\n (click)=\"variantIdExpressionMode = !variantIdExpressionMode\"\n [disabled]=\"obs.disabled\"\n [title]=\"variantIdExpressionMode ? 'Switch to dropdown' : 'Switch to expression'\"\n >\n {{ variantIdExpressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n <div *ngIf=\"obs.variants.error\" class=\"loading-error\">{{ obs.variants.error }}</div>\n\n <!-- Attribute-based variant selection -->\n <div\n *ngIf=\"variantSelectionMode === 'attributes' && obs.selectedTemplateId\"\n class=\"variant-attributes-section\"\n >\n <label class=\"variant-attributes-label\">{{\n 'variantAttributes' | pluginTranslate: pluginId | async\n }}</label>\n <div class=\"variant-attributes-list\">\n <div\n *ngFor=\"let entry of variantAttributeEntries; let i = index\"\n class=\"variant-attribute-row\"\n >\n <select\n *ngIf=\"!entry._customKey\"\n class=\"variant-attribute-input\"\n [ngModel]=\"entry.key\"\n (ngModelChange)=\"onKeySelected(entry, $event)\"\n [disabled]=\"obs.disabled\"\n >\n <option value=\"\" disabled>\n {{ 'attributeKey' | pluginTranslate: pluginId | async }}\n </option>\n <option *ngFor=\"let key of availableAttributeKeys\" [value]=\"key\">{{ key }}</option>\n <option value=\"__custom__\">\n {{ 'attributeKeyCustom' | pluginTranslate: pluginId | async }}\n </option>\n </select>\n <div *ngIf=\"entry._customKey\" class=\"custom-key-input\">\n <input\n type=\"text\"\n class=\"variant-attribute-input\"\n [placeholder]=\"'attributeKey' | pluginTranslate: pluginId | async\"\n [(ngModel)]=\"entry.key\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <button\n type=\"button\"\n class=\"custom-key-cancel\"\n (click)=\"cancelCustomKey(entry)\"\n [disabled]=\"obs.disabled\"\n >\n &times;\n </button>\n </div>\n <div class=\"attribute-value-with-fx\">\n <input\n type=\"text\"\n class=\"variant-attribute-input\"\n [class.fx-input]=\"entry._expressionMode\"\n [placeholder]=\"\n entry._expressionMode\n ? '$pv.language'\n : ('attributeValue' | pluginTranslate: pluginId | async)\n \"\n [(ngModel)]=\"entry.value\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <button\n type=\"button\"\n class=\"fx-toggle fx-toggle--inline\"\n (click)=\"entry._expressionMode = !entry._expressionMode\"\n [disabled]=\"obs.disabled\"\n [title]=\"entry._expressionMode ? 'Switch to plain value' : 'Switch to expression'\"\n >\n {{ entry._expressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n <label class=\"variant-attribute-required-toggle\">\n <input\n type=\"checkbox\"\n [(ngModel)]=\"entry.required\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <span class=\"required-label\">{{\n (entry.required ? 'attributeRequired' : 'attributePreferred')\n | pluginTranslate: pluginId\n | async\n }}</span>\n </label>\n <button\n type=\"button\"\n class=\"variant-attribute-remove-btn\"\n (click)=\"removeAttributeEntry(i)\"\n [disabled]=\"obs.disabled\"\n title=\"{{ 'removeAttribute' | pluginTranslate: pluginId | async }}\"\n >\n &times;\n </button>\n </div>\n </div>\n <button\n type=\"button\"\n class=\"variant-attribute-add-btn\"\n (click)=\"addAttributeEntry()\"\n [disabled]=\"obs.disabled\"\n >\n + {{ 'addAttribute' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <v-select\n name=\"environmentId\"\n [title]=\"'environmentId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'environmentIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.environments.data\"\n [defaultSelectionId]=\"obs.prefill?.environmentId\"\n [disabled]=\"obs.disabled || obs.environments.loading\"\n [required]=\"false\"\n [loading]=\"obs.environments.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.environments.error\" class=\"loading-error\">{{ obs.environments.error }}</div>\n\n <v-select\n name=\"outputFormat\"\n [title]=\"'outputFormat' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'outputFormatTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"outputFormatOptions\"\n [defaultSelectionId]=\"obs.prefill?.outputFormat || 'PDF'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-select>\n\n <div class=\"field-with-fx\">\n <v-input\n *ngIf=\"!filenameExpressionMode\"\n name=\"filename\"\n [title]=\"'filename' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'filenameTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.filename\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n <div *ngIf=\"filenameExpressionMode\" class=\"fx-input-group\">\n <label class=\"fx-input-label\">{{ 'filename' | pluginTranslate: pluginId | async }}</label>\n <input\n type=\"text\"\n class=\"fx-input\"\n [ngModel]=\"filenameExpression\"\n (ngModelChange)=\"filenameExpression = $event; onFilenameExpressionChange()\"\n [disabled]=\"obs.disabled\"\n placeholder='\"besluit-\" & $doc.name & \".pdf\"'\n />\n </div>\n <button\n type=\"button\"\n class=\"fx-toggle\"\n (click)=\"filenameExpressionMode = !filenameExpressionMode\"\n [disabled]=\"obs.disabled\"\n [title]=\"filenameExpressionMode ? 'Switch to plain input' : 'Switch to expression'\"\n >\n {{ filenameExpressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n\n <v-input\n name=\"correlationId\"\n [title]=\"'correlationId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'correlationIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.correlationId\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"resultProcessVariable\"\n [title]=\"'resultProcessVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'resultProcessVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.resultProcessVariable\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n\n<div *ngIf=\"(templateFields$ | async)?.error as templateFieldsError\" class=\"loading-error\">\n {{ templateFieldsError }}\n</div>\n\n<div *ngIf=\"selectedTemplateId$ | async\" class=\"mapping-section\">\n <h5 class=\"mapping-section__title\">\n {{ 'dataMappingTitle' | pluginTranslate: pluginId | async }}\n </h5>\n <p class=\"mapping-section__description\">\n {{ 'dataMappingDescription' | pluginTranslate: pluginId | async }}\n </p>\n <div class=\"mapping-mode-toggle\">\n <button\n class=\"mapping-mode-toggle__btn\"\n [class.mapping-mode-toggle__btn--active]=\"mappingMode === 'simple'\"\n (click)=\"mappingMode = 'simple'\"\n >\n {{ 'mappingModeSimple' | pluginTranslate: pluginId | async }}\n </button>\n <button\n class=\"mapping-mode-toggle__btn\"\n [class.mapping-mode-toggle__btn--active]=\"mappingMode === 'advanced'\"\n (click)=\"mappingMode = 'advanced'\"\n >\n {{ 'mappingModeAdvanced' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <!-- Editor area (full width) -->\n <epistola-mapping-builder\n *ngIf=\"mappingMode === 'simple'\"\n [expression]=\"dataMapping$ | async\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n [suggestions]=\"variableSuggestions\"\n [disabled]=\"!!(disabled$ | async)\"\n (expressionChange)=\"onDataMappingChange($event)\"\n ></epistola-mapping-builder>\n\n <epistola-jsonata-editor\n *ngIf=\"mappingMode === 'advanced'\"\n [expression]=\"dataMapping$ | async\"\n [disabled]=\"!!(disabled$ | async)\"\n [suggestions]=\"variableSuggestions\"\n [functions]=\"expressionFunctions\"\n (expressionChange)=\"onDataMappingChange($event)\"\n ></epistola-jsonata-editor>\n\n <!-- Bottom tabs: Schema + Preview (collapsible) -->\n <div class=\"mapping-tools\" [class.mapping-tools--collapsed]=\"toolsCollapsed\">\n <div class=\"mapping-tools__header\" (click)=\"toolsCollapsed = !toolsCollapsed\">\n <span class=\"mapping-tools__chevron\">{{ toolsCollapsed ? '&#x25B6;' : '&#x25BC;' }}</span>\n <span>{{ 'mappingTools' | pluginTranslate: pluginId | async }}</span>\n </div>\n <div *ngIf=\"!toolsCollapsed\" class=\"mapping-tools__content\">\n <div class=\"mapping-tools__tabs\">\n <button\n class=\"mapping-tools__tab\"\n [class.mapping-tools__tab--active]=\"activeToolTab === 'schema'\"\n (click)=\"activeToolTab = 'schema'\"\n >\n {{ 'expectedStructure' | pluginTranslate: pluginId | async }}\n </button>\n <button\n class=\"mapping-tools__tab\"\n [class.mapping-tools__tab--active]=\"activeToolTab === 'preview'\"\n (click)=\"activeToolTab = 'preview'\"\n >\n {{ 'previewTitle' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <epistola-expected-structure\n *ngIf=\"activeToolTab === 'schema'\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n ></epistola-expected-structure>\n\n <epistola-mapping-preview\n *ngIf=\"activeToolTab === 'preview'\"\n [expression]=\"dataMapping$ | async\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n ></epistola-mapping-preview>\n </div>\n </div>\n</div>\n", styles: [".loading-error{padding:.25rem .75rem;font-size:.8125rem;color:#dc3545}.jsonata-validation-errors{margin-bottom:1rem;padding:.75rem 1rem;border:1px solid #dc3545;border-radius:4px;background:#fdf3f4;color:#dc3545;font-size:.875rem}.jsonata-validation-errors ul{margin:.5rem 0 0;padding-left:1.25rem}.jsonata-validation-errors code{background:#dc35451a;padding:0 .25rem;border-radius:2px;font-family:monospace}.validation-summary{margin-top:.5rem;padding:.5rem .75rem;border-radius:4px;font-size:.875rem}.validation-summary .validation-complete{color:#198754}.validation-summary .validation-incomplete{color:#dc3545;font-weight:500}.variant-mode-toggle{margin-bottom:1rem;padding:0 .75rem}.variant-mode-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-mode-buttons{display:flex;gap:0;border:1px solid #d1d5db;border-radius:4px;overflow:hidden;width:fit-content}.variant-mode-btn{padding:.375rem .75rem;font-size:.8125rem;background:#fff;border:none;border-right:1px solid #d1d5db;cursor:pointer;color:#374151;transition:background-color .15s,color .15s}.variant-mode-btn:last-child{border-right:none}.variant-mode-btn:hover:not([disabled]){background:#f3f4f6}.variant-mode-btn.active{background:#2563eb;color:#fff}.variant-mode-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attributes-section{margin-bottom:1rem;padding:0 .75rem}.variant-attributes-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-attributes-list{display:flex;flex-direction:column;gap:.375rem}.variant-attribute-row{display:flex;gap:.375rem;align-items:center}.variant-attribute-input{flex:1;padding:.375rem .5rem;font-size:.8125rem;border:1px solid #d1d5db;border-radius:4px;outline:none}.variant-attribute-input:focus{border-color:#2563eb;box-shadow:0 0 0 1px #2563eb}.variant-attribute-input[disabled]{opacity:.5;background:#f9fafb}.custom-key-input{display:flex;flex:1;gap:.25rem}.custom-key-input .variant-attribute-input{flex:1}.custom-key-cancel{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.custom-key-cancel:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.custom-key-cancel[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-required-toggle{display:flex;align-items:center;gap:.25rem;font-size:.75rem;color:#374151;white-space:nowrap;cursor:pointer}.variant-attribute-required-toggle input[type=checkbox]{margin:0;cursor:pointer}.variant-attribute-required-toggle .required-label{-webkit-user-select:none;user-select:none}.variant-attribute-remove-btn{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-remove-btn:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.variant-attribute-remove-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-add-btn{margin-top:.375rem;padding:.25rem .5rem;font-size:.8125rem;background:none;border:1px dashed #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-add-btn:hover:not([disabled]){color:#2563eb;border-color:#2563eb}.variant-attribute-add-btn[disabled]{opacity:.5;cursor:not-allowed}.field-with-fx{display:flex;align-items:flex-start;gap:4px}.field-with-fx>*:first-child{flex:1;min-width:0}.fx-toggle{width:28px;height:28px;margin-top:22px;border:1px solid #e0e0e0;border-radius:4px;background:#fff;cursor:pointer;font-family:monospace;font-size:.8em;display:flex;align-items:center;justify-content:center;flex-shrink:0}.fx-toggle:hover{background:#f4f4f4}.fx-toggle--inline{margin-top:0}.fx-input-group{flex:1;min-width:0;margin-bottom:.75rem}.fx-input-label{display:block;font-size:.875rem;margin-bottom:.25rem;color:#525252}.fx-input{width:100%;border:1px solid #8d8d8d;border-radius:0;padding:.4rem .75rem;font-family:monospace;font-size:.875rem;background:#f4f4f4}.attribute-value-with-fx{display:flex;align-items:center;gap:4px;flex:1;min-width:0}.attribute-value-with-fx>input{flex:1;min-width:0}.mapping-section{margin-top:1rem}.mapping-section__title{font-size:1rem;font-weight:600;margin:0 0 4px}.mapping-section__description{font-size:.85em;color:#6f6f6f;margin:0 0 12px}.mapping-mode-toggle{display:flex;gap:0;margin-bottom:12px}.mapping-mode-toggle__btn{padding:6px 16px;border:1px solid #e0e0e0;background:#fff;font-size:.85em;cursor:pointer}.mapping-mode-toggle__btn:first-child{border-radius:4px 0 0 4px}.mapping-mode-toggle__btn:last-child{border-radius:0 4px 4px 0;border-left:none}.mapping-mode-toggle__btn--active{background:#0f62fe;color:#fff;border-color:#0f62fe}.mapping-tools{margin-top:12px;border:1px solid #e0e0e0;border-radius:4px;overflow:hidden}.mapping-tools__header{display:flex;align-items:center;gap:6px;padding:8px 12px;background:#f4f4f4;cursor:pointer;font-size:.85em;font-weight:500;-webkit-user-select:none;user-select:none}.mapping-tools__header:hover{background:#e8e8e8}.mapping-tools__chevron{font-size:.7em}.mapping-tools__content{border-top:1px solid #e0e0e0}.mapping-tools__tabs{display:flex;border-bottom:1px solid #e0e0e0}.mapping-tools__tab{padding:6px 16px;border:none;background:transparent;font-size:.8em;cursor:pointer;border-bottom:2px solid transparent}.mapping-tools__tab--active{border-bottom-color:#0f62fe;font-weight:500}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2$2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i2$2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: FormModule }, { kind: "component", type: i3.FormComponent, selector: "v-form", inputs: ["className"], outputs: ["valueChange"] }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i3.InputComponent, selector: "v-input", inputs: ["name", "type", "title", "titleTranslationKey", "defaultValue", "widthPx", "fullWidth", "margin", "smallMargin", "disabled", "step", "min", "maxLength", "tooltip", "required", "hideNumberSpinBox", "smallLabel", "rows", "clear$", "carbonTheme", "placeholder", "dataTestId", "trim", "presetsTitle", "presetOptions"], outputs: ["valueChange"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i3.SelectComponent, selector: "v-select", inputs: ["items", "defaultSelection", "defaultSelectionId", "defaultSelectionIds", "disabled", "dropUp", "invalid", "multiple", "margin", "widthInPx", "notFoundText", "clearAllText", "clearText", "clearable", "name", "title", "titleTranslationKey", "clearSelectionSubject$", "tooltip", "required", "loading", "loadingText", "placeholder", "smallMargin", "carbonTheme", "appendInline", "warn", "warnText", "dataTestId"], outputs: ["selectedChange"] }, { kind: "component", type: ExpectedStructureComponent, selector: "epistola-expected-structure", inputs: ["templateFields"] }, { kind: "component", type: JsonataEditorComponent, selector: "epistola-jsonata-editor", inputs: ["expression", "disabled", "suggestions", "functions"], outputs: ["expressionChange", "validChange"] }, { kind: "component", type: MappingBuilderComponent, selector: "epistola-mapping-builder", inputs: ["expression", "templateFields", "suggestions", "disabled"], outputs: ["expressionChange"] }, { kind: "component", type: MappingPreviewComponent, selector: "epistola-mapping-preview", inputs: ["expression", "templateFields", "caseDefinitionKey"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
977
1943
  }
978
1944
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: GenerateDocumentConfigurationComponent, decorators: [{
979
1945
  type: Component,
@@ -984,9 +1950,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
984
1950
  FormModule,
985
1951
  InputModule,
986
1952
  SelectModule,
987
- DataMappingTreeComponent
988
- ], template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: disabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n catalogs: catalogs$ | async,\n templates: templates$ | async,\n variants: variants$ | async,\n environments: environments$ | async,\n templateFields: templateFields$ | async,\n selectedCatalogId: selectedCatalogId$ | async,\n selectedTemplateId: selectedTemplateId$ | async\n } as obs\"\n>\n <v-select\n name=\"catalogId\"\n [title]=\"'catalogId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'catalogIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.catalogs.data\"\n [defaultSelectionId]=\"obs.prefill?.catalogId\"\n [disabled]=\"obs.disabled || obs.catalogs.loading\"\n [required]=\"true\"\n [loading]=\"obs.catalogs.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.catalogs.error\" class=\"loading-error\">{{ obs.catalogs.error }}</div>\n\n <v-select\n name=\"templateId\"\n [title]=\"'templateId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'templateIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.templates.data\"\n [defaultSelectionId]=\"obs.prefill?.templateId\"\n [disabled]=\"obs.disabled || obs.templates.loading || !obs.selectedCatalogId\"\n [required]=\"true\"\n [loading]=\"obs.templates.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.templates.error\" class=\"loading-error\">{{ obs.templates.error }}</div>\n\n <!-- Variant selection mode toggle -->\n <div class=\"variant-mode-toggle\" *ngIf=\"obs.selectedTemplateId\">\n <label class=\"variant-mode-label\">{{ 'variantSelectionMode' | pluginTranslate: pluginId | async }}</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 >{{ 'selectByVariant' | pluginTranslate: pluginId | async }}</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 >{{ 'selectByAttributes' | pluginTranslate: pluginId | async }}</button>\n </div>\n </div>\n\n <!-- Explicit variant selection (dropdown) -->\n <v-select\n *ngIf=\"variantSelectionMode === 'explicit'\"\n name=\"variantId\"\n [title]=\"'variantId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'variantIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.variants.data\"\n [defaultSelectionId]=\"obs.prefill?.variantId\"\n [disabled]=\"obs.disabled || obs.variants.loading || !obs.selectedTemplateId\"\n [required]=\"false\"\n [loading]=\"obs.variants.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.variants.error\" class=\"loading-error\">{{ obs.variants.error }}</div>\n\n <!-- Attribute-based variant selection -->\n <div *ngIf=\"variantSelectionMode === 'attributes' && obs.selectedTemplateId\" class=\"variant-attributes-section\">\n <label class=\"variant-attributes-label\">{{ 'variantAttributes' | pluginTranslate: pluginId | async }}</label>\n <div class=\"variant-attributes-list\">\n <div *ngFor=\"let entry of variantAttributeEntries; let i = index\" class=\"variant-attribute-row\">\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>{{ 'attributeKey' | pluginTranslate: pluginId | async }}</option>\n <option *ngFor=\"let key of availableAttributeKeys\" [value]=\"key\">{{ key }}</option>\n <option value=\"__custom__\">{{ 'attributeKeyCustom' | pluginTranslate: pluginId | async }}</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 type=\"button\" class=\"custom-key-cancel\" (click)=\"cancelCustomKey(entry)\" [disabled]=\"obs.disabled\">&times;</button>\n </div>\n <input\n type=\"text\"\n class=\"variant-attribute-input\"\n [placeholder]=\"'attributeValue' | pluginTranslate: pluginId | async\"\n [(ngModel)]=\"entry.value\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <label class=\"variant-attribute-required-toggle\">\n <input\n type=\"checkbox\"\n [(ngModel)]=\"entry.required\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <span class=\"required-label\">{{ (entry.required ? 'attributeRequired' : 'attributePreferred') | pluginTranslate: pluginId | async }}</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 >&times;</button>\n </div>\n </div>\n <button\n type=\"button\"\n class=\"variant-attribute-add-btn\"\n (click)=\"addAttributeEntry()\"\n [disabled]=\"obs.disabled\"\n >+ {{ 'addAttribute' | pluginTranslate: pluginId | async }}</button>\n </div>\n\n <v-select\n name=\"environmentId\"\n [title]=\"'environmentId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'environmentIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.environments.data\"\n [defaultSelectionId]=\"obs.prefill?.environmentId\"\n [disabled]=\"obs.disabled || obs.environments.loading\"\n [required]=\"false\"\n [loading]=\"obs.environments.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.environments.error\" class=\"loading-error\">{{ obs.environments.error }}</div>\n\n <v-select\n name=\"outputFormat\"\n [title]=\"'outputFormat' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'outputFormatTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"outputFormatOptions\"\n [defaultSelectionId]=\"obs.prefill?.outputFormat || 'PDF'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-select>\n\n <v-input\n name=\"filename\"\n [title]=\"'filename' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'filenameTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.filename\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"correlationId\"\n [title]=\"'correlationId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'correlationIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.correlationId\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"resultProcessVariable\"\n [title]=\"'resultProcessVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'resultProcessVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.resultProcessVariable\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n\n<div *ngIf=\"(templateFields$ | async)?.error as templateFieldsError\" class=\"loading-error\">{{ templateFieldsError }}</div>\n\n<epistola-data-mapping-tree\n *ngIf=\"(selectedTemplateId$ | async)\"\n [pluginId]=\"pluginId\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n [disabled]=\"!!(disabled$ | async)\"\n [prefillMapping]=\"prefillDataMapping\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n [processVariables]=\"processVariables\"\n (mappingChange)=\"onDataMappingChange($event)\"\n (requiredFieldsStatus)=\"onRequiredFieldsStatusChange($event)\"\n></epistola-data-mapping-tree>\n\n<div class=\"validation-summary\" *ngIf=\"(selectedTemplateId$ | async) && requiredFieldsStatus.total > 0\">\n <span *ngIf=\"requiredFieldsStatus.mapped === requiredFieldsStatus.total\" class=\"validation-complete\">\n {{ 'requiredFieldsComplete' | pluginTranslate: pluginId | async }}\n </span>\n <span *ngIf=\"requiredFieldsStatus.mapped < requiredFieldsStatus.total\" class=\"validation-incomplete\">\n {{ requiredFieldsStatus.mapped }} / {{ requiredFieldsStatus.total }}\n {{ 'validationSummary' | pluginTranslate: pluginId | async }}\n </span>\n</div>\n", styles: [".loading-error{padding:.25rem .75rem;font-size:.8125rem;color:#dc3545}.validation-summary{margin-top:.5rem;padding:.5rem .75rem;border-radius:4px;font-size:.875rem}.validation-summary .validation-complete{color:#198754}.validation-summary .validation-incomplete{color:#dc3545;font-weight:500}.variant-mode-toggle{margin-bottom:1rem;padding:0 .75rem}.variant-mode-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-mode-buttons{display:flex;gap:0;border:1px solid #d1d5db;border-radius:4px;overflow:hidden;width:fit-content}.variant-mode-btn{padding:.375rem .75rem;font-size:.8125rem;background:#fff;border:none;border-right:1px solid #d1d5db;cursor:pointer;color:#374151;transition:background-color .15s,color .15s}.variant-mode-btn:last-child{border-right:none}.variant-mode-btn:hover:not([disabled]){background:#f3f4f6}.variant-mode-btn.active{background:#2563eb;color:#fff}.variant-mode-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attributes-section{margin-bottom:1rem;padding:0 .75rem}.variant-attributes-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-attributes-list{display:flex;flex-direction:column;gap:.375rem}.variant-attribute-row{display:flex;gap:.375rem;align-items:center}.variant-attribute-input{flex:1;padding:.375rem .5rem;font-size:.8125rem;border:1px solid #d1d5db;border-radius:4px;outline:none}.variant-attribute-input:focus{border-color:#2563eb;box-shadow:0 0 0 1px #2563eb}.variant-attribute-input[disabled]{opacity:.5;background:#f9fafb}.custom-key-input{display:flex;flex:1;gap:.25rem}.custom-key-input .variant-attribute-input{flex:1}.custom-key-cancel{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.custom-key-cancel:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.custom-key-cancel[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-required-toggle{display:flex;align-items:center;gap:.25rem;font-size:.75rem;color:#374151;white-space:nowrap;cursor:pointer}.variant-attribute-required-toggle input[type=checkbox]{margin:0;cursor:pointer}.variant-attribute-required-toggle .required-label{-webkit-user-select:none;user-select:none}.variant-attribute-remove-btn{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-remove-btn:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.variant-attribute-remove-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-add-btn{margin-top:.375rem;padding:.25rem .5rem;font-size:.8125rem;background:none;border:1px dashed #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-add-btn:hover:not([disabled]){color:#2563eb;border-color:#2563eb}.variant-attribute-add-btn[disabled]{opacity:.5;cursor:not-allowed}\n"] }]
989
- }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$2.ProcessLinkStateService }, { type: i0.ChangeDetectorRef }], propDecorators: { save$: [{
1953
+ ExpectedStructureComponent,
1954
+ JsonataEditorComponent,
1955
+ MappingBuilderComponent,
1956
+ MappingPreviewComponent,
1957
+ ], template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: disabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n catalogs: catalogs$ | async,\n templates: templates$ | async,\n variants: variants$ | async,\n environments: environments$ | async,\n templateFields: templateFields$ | async,\n selectedCatalogId: selectedCatalogId$ | async,\n selectedTemplateId: selectedTemplateId$ | async,\n validationErrors: validationErrors$ | async,\n } as obs\"\n>\n <div\n *ngIf=\"obs.validationErrors && obs.validationErrors.length > 0\"\n class=\"jsonata-validation-errors\"\n >\n <strong>{{ 'jsonataValidationErrorsHeading' | pluginTranslate: pluginId | async }}</strong>\n <ul>\n <li *ngFor=\"let err of obs.validationErrors\">\n <code>{{ err.field }}</code\n >: {{ err.message }}\n </li>\n </ul>\n </div>\n <v-select\n name=\"catalogId\"\n [title]=\"'catalogId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'catalogIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.catalogs.data\"\n [defaultSelectionId]=\"obs.prefill?.catalogId\"\n [disabled]=\"obs.disabled || obs.catalogs.loading\"\n [required]=\"true\"\n [loading]=\"obs.catalogs.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.catalogs.error\" class=\"loading-error\">{{ obs.catalogs.error }}</div>\n\n <v-select\n name=\"templateId\"\n [title]=\"'templateId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'templateIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.templates.data\"\n [defaultSelectionId]=\"obs.prefill?.templateId\"\n [disabled]=\"obs.disabled || obs.templates.loading || !obs.selectedCatalogId\"\n [required]=\"true\"\n [loading]=\"obs.templates.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.templates.error\" class=\"loading-error\">{{ obs.templates.error }}</div>\n\n <!-- Variant selection mode toggle -->\n <div class=\"variant-mode-toggle\" *ngIf=\"obs.selectedTemplateId\">\n <label class=\"variant-mode-label\">{{\n 'variantSelectionMode' | pluginTranslate: pluginId | async\n }}</label>\n <div class=\"variant-mode-buttons\">\n <button\n type=\"button\"\n class=\"variant-mode-btn\"\n [class.active]=\"variantSelectionMode === 'explicit'\"\n (click)=\"onVariantSelectionModeChange('explicit')\"\n [disabled]=\"obs.disabled\"\n >\n {{ 'selectByVariant' | pluginTranslate: pluginId | async }}\n </button>\n <button\n type=\"button\"\n class=\"variant-mode-btn\"\n [class.active]=\"variantSelectionMode === 'attributes'\"\n (click)=\"onVariantSelectionModeChange('attributes')\"\n [disabled]=\"obs.disabled\"\n >\n {{ 'selectByAttributes' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n </div>\n\n <!-- Explicit variant selection (dropdown or expression) -->\n <div *ngIf=\"variantSelectionMode === 'explicit'\" class=\"field-with-fx\">\n <v-select\n *ngIf=\"!variantIdExpressionMode\"\n name=\"variantId\"\n [title]=\"'variantId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'variantIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.variants.data\"\n [defaultSelectionId]=\"obs.prefill?.variantId\"\n [disabled]=\"obs.disabled || obs.variants.loading || !obs.selectedTemplateId\"\n [required]=\"false\"\n [loading]=\"obs.variants.loading\"\n >\n </v-select>\n <div *ngIf=\"variantIdExpressionMode\" class=\"fx-input-group\">\n <label class=\"fx-input-label\">{{ 'variantId' | pluginTranslate: pluginId | async }}</label>\n <input\n type=\"text\"\n class=\"fx-input\"\n [ngModel]=\"variantIdExpression\"\n (ngModelChange)=\"variantIdExpression = $event; onVariantIdExpressionChange()\"\n [disabled]=\"obs.disabled\"\n placeholder=\"$pv.letterType\"\n />\n </div>\n <button\n type=\"button\"\n class=\"fx-toggle\"\n (click)=\"variantIdExpressionMode = !variantIdExpressionMode\"\n [disabled]=\"obs.disabled\"\n [title]=\"variantIdExpressionMode ? 'Switch to dropdown' : 'Switch to expression'\"\n >\n {{ variantIdExpressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n <div *ngIf=\"obs.variants.error\" class=\"loading-error\">{{ obs.variants.error }}</div>\n\n <!-- Attribute-based variant selection -->\n <div\n *ngIf=\"variantSelectionMode === 'attributes' && obs.selectedTemplateId\"\n class=\"variant-attributes-section\"\n >\n <label class=\"variant-attributes-label\">{{\n 'variantAttributes' | pluginTranslate: pluginId | async\n }}</label>\n <div class=\"variant-attributes-list\">\n <div\n *ngFor=\"let entry of variantAttributeEntries; let i = index\"\n class=\"variant-attribute-row\"\n >\n <select\n *ngIf=\"!entry._customKey\"\n class=\"variant-attribute-input\"\n [ngModel]=\"entry.key\"\n (ngModelChange)=\"onKeySelected(entry, $event)\"\n [disabled]=\"obs.disabled\"\n >\n <option value=\"\" disabled>\n {{ 'attributeKey' | pluginTranslate: pluginId | async }}\n </option>\n <option *ngFor=\"let key of availableAttributeKeys\" [value]=\"key\">{{ key }}</option>\n <option value=\"__custom__\">\n {{ 'attributeKeyCustom' | pluginTranslate: pluginId | async }}\n </option>\n </select>\n <div *ngIf=\"entry._customKey\" class=\"custom-key-input\">\n <input\n type=\"text\"\n class=\"variant-attribute-input\"\n [placeholder]=\"'attributeKey' | pluginTranslate: pluginId | async\"\n [(ngModel)]=\"entry.key\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <button\n type=\"button\"\n class=\"custom-key-cancel\"\n (click)=\"cancelCustomKey(entry)\"\n [disabled]=\"obs.disabled\"\n >\n &times;\n </button>\n </div>\n <div class=\"attribute-value-with-fx\">\n <input\n type=\"text\"\n class=\"variant-attribute-input\"\n [class.fx-input]=\"entry._expressionMode\"\n [placeholder]=\"\n entry._expressionMode\n ? '$pv.language'\n : ('attributeValue' | pluginTranslate: pluginId | async)\n \"\n [(ngModel)]=\"entry.value\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <button\n type=\"button\"\n class=\"fx-toggle fx-toggle--inline\"\n (click)=\"entry._expressionMode = !entry._expressionMode\"\n [disabled]=\"obs.disabled\"\n [title]=\"entry._expressionMode ? 'Switch to plain value' : 'Switch to expression'\"\n >\n {{ entry._expressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n <label class=\"variant-attribute-required-toggle\">\n <input\n type=\"checkbox\"\n [(ngModel)]=\"entry.required\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <span class=\"required-label\">{{\n (entry.required ? 'attributeRequired' : 'attributePreferred')\n | pluginTranslate: pluginId\n | async\n }}</span>\n </label>\n <button\n type=\"button\"\n class=\"variant-attribute-remove-btn\"\n (click)=\"removeAttributeEntry(i)\"\n [disabled]=\"obs.disabled\"\n title=\"{{ 'removeAttribute' | pluginTranslate: pluginId | async }}\"\n >\n &times;\n </button>\n </div>\n </div>\n <button\n type=\"button\"\n class=\"variant-attribute-add-btn\"\n (click)=\"addAttributeEntry()\"\n [disabled]=\"obs.disabled\"\n >\n + {{ 'addAttribute' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <v-select\n name=\"environmentId\"\n [title]=\"'environmentId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'environmentIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.environments.data\"\n [defaultSelectionId]=\"obs.prefill?.environmentId\"\n [disabled]=\"obs.disabled || obs.environments.loading\"\n [required]=\"false\"\n [loading]=\"obs.environments.loading\"\n >\n </v-select>\n <div *ngIf=\"obs.environments.error\" class=\"loading-error\">{{ obs.environments.error }}</div>\n\n <v-select\n name=\"outputFormat\"\n [title]=\"'outputFormat' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'outputFormatTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"outputFormatOptions\"\n [defaultSelectionId]=\"obs.prefill?.outputFormat || 'PDF'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-select>\n\n <div class=\"field-with-fx\">\n <v-input\n *ngIf=\"!filenameExpressionMode\"\n name=\"filename\"\n [title]=\"'filename' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'filenameTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.filename\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n <div *ngIf=\"filenameExpressionMode\" class=\"fx-input-group\">\n <label class=\"fx-input-label\">{{ 'filename' | pluginTranslate: pluginId | async }}</label>\n <input\n type=\"text\"\n class=\"fx-input\"\n [ngModel]=\"filenameExpression\"\n (ngModelChange)=\"filenameExpression = $event; onFilenameExpressionChange()\"\n [disabled]=\"obs.disabled\"\n placeholder='\"besluit-\" & $doc.name & \".pdf\"'\n />\n </div>\n <button\n type=\"button\"\n class=\"fx-toggle\"\n (click)=\"filenameExpressionMode = !filenameExpressionMode\"\n [disabled]=\"obs.disabled\"\n [title]=\"filenameExpressionMode ? 'Switch to plain input' : 'Switch to expression'\"\n >\n {{ filenameExpressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n\n <v-input\n name=\"correlationId\"\n [title]=\"'correlationId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'correlationIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.correlationId\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"resultProcessVariable\"\n [title]=\"'resultProcessVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'resultProcessVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.resultProcessVariable\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n\n<div *ngIf=\"(templateFields$ | async)?.error as templateFieldsError\" class=\"loading-error\">\n {{ templateFieldsError }}\n</div>\n\n<div *ngIf=\"selectedTemplateId$ | async\" class=\"mapping-section\">\n <h5 class=\"mapping-section__title\">\n {{ 'dataMappingTitle' | pluginTranslate: pluginId | async }}\n </h5>\n <p class=\"mapping-section__description\">\n {{ 'dataMappingDescription' | pluginTranslate: pluginId | async }}\n </p>\n <div class=\"mapping-mode-toggle\">\n <button\n class=\"mapping-mode-toggle__btn\"\n [class.mapping-mode-toggle__btn--active]=\"mappingMode === 'simple'\"\n (click)=\"mappingMode = 'simple'\"\n >\n {{ 'mappingModeSimple' | pluginTranslate: pluginId | async }}\n </button>\n <button\n class=\"mapping-mode-toggle__btn\"\n [class.mapping-mode-toggle__btn--active]=\"mappingMode === 'advanced'\"\n (click)=\"mappingMode = 'advanced'\"\n >\n {{ 'mappingModeAdvanced' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <!-- Editor area (full width) -->\n <epistola-mapping-builder\n *ngIf=\"mappingMode === 'simple'\"\n [expression]=\"dataMapping$ | async\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n [suggestions]=\"variableSuggestions\"\n [disabled]=\"!!(disabled$ | async)\"\n (expressionChange)=\"onDataMappingChange($event)\"\n ></epistola-mapping-builder>\n\n <epistola-jsonata-editor\n *ngIf=\"mappingMode === 'advanced'\"\n [expression]=\"dataMapping$ | async\"\n [disabled]=\"!!(disabled$ | async)\"\n [suggestions]=\"variableSuggestions\"\n [functions]=\"expressionFunctions\"\n (expressionChange)=\"onDataMappingChange($event)\"\n ></epistola-jsonata-editor>\n\n <!-- Bottom tabs: Schema + Preview (collapsible) -->\n <div class=\"mapping-tools\" [class.mapping-tools--collapsed]=\"toolsCollapsed\">\n <div class=\"mapping-tools__header\" (click)=\"toolsCollapsed = !toolsCollapsed\">\n <span class=\"mapping-tools__chevron\">{{ toolsCollapsed ? '&#x25B6;' : '&#x25BC;' }}</span>\n <span>{{ 'mappingTools' | pluginTranslate: pluginId | async }}</span>\n </div>\n <div *ngIf=\"!toolsCollapsed\" class=\"mapping-tools__content\">\n <div class=\"mapping-tools__tabs\">\n <button\n class=\"mapping-tools__tab\"\n [class.mapping-tools__tab--active]=\"activeToolTab === 'schema'\"\n (click)=\"activeToolTab = 'schema'\"\n >\n {{ 'expectedStructure' | pluginTranslate: pluginId | async }}\n </button>\n <button\n class=\"mapping-tools__tab\"\n [class.mapping-tools__tab--active]=\"activeToolTab === 'preview'\"\n (click)=\"activeToolTab = 'preview'\"\n >\n {{ 'previewTitle' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <epistola-expected-structure\n *ngIf=\"activeToolTab === 'schema'\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n ></epistola-expected-structure>\n\n <epistola-mapping-preview\n *ngIf=\"activeToolTab === 'preview'\"\n [expression]=\"dataMapping$ | async\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n ></epistola-mapping-preview>\n </div>\n </div>\n</div>\n", styles: [".loading-error{padding:.25rem .75rem;font-size:.8125rem;color:#dc3545}.jsonata-validation-errors{margin-bottom:1rem;padding:.75rem 1rem;border:1px solid #dc3545;border-radius:4px;background:#fdf3f4;color:#dc3545;font-size:.875rem}.jsonata-validation-errors ul{margin:.5rem 0 0;padding-left:1.25rem}.jsonata-validation-errors code{background:#dc35451a;padding:0 .25rem;border-radius:2px;font-family:monospace}.validation-summary{margin-top:.5rem;padding:.5rem .75rem;border-radius:4px;font-size:.875rem}.validation-summary .validation-complete{color:#198754}.validation-summary .validation-incomplete{color:#dc3545;font-weight:500}.variant-mode-toggle{margin-bottom:1rem;padding:0 .75rem}.variant-mode-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-mode-buttons{display:flex;gap:0;border:1px solid #d1d5db;border-radius:4px;overflow:hidden;width:fit-content}.variant-mode-btn{padding:.375rem .75rem;font-size:.8125rem;background:#fff;border:none;border-right:1px solid #d1d5db;cursor:pointer;color:#374151;transition:background-color .15s,color .15s}.variant-mode-btn:last-child{border-right:none}.variant-mode-btn:hover:not([disabled]){background:#f3f4f6}.variant-mode-btn.active{background:#2563eb;color:#fff}.variant-mode-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attributes-section{margin-bottom:1rem;padding:0 .75rem}.variant-attributes-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-attributes-list{display:flex;flex-direction:column;gap:.375rem}.variant-attribute-row{display:flex;gap:.375rem;align-items:center}.variant-attribute-input{flex:1;padding:.375rem .5rem;font-size:.8125rem;border:1px solid #d1d5db;border-radius:4px;outline:none}.variant-attribute-input:focus{border-color:#2563eb;box-shadow:0 0 0 1px #2563eb}.variant-attribute-input[disabled]{opacity:.5;background:#f9fafb}.custom-key-input{display:flex;flex:1;gap:.25rem}.custom-key-input .variant-attribute-input{flex:1}.custom-key-cancel{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.custom-key-cancel:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.custom-key-cancel[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-required-toggle{display:flex;align-items:center;gap:.25rem;font-size:.75rem;color:#374151;white-space:nowrap;cursor:pointer}.variant-attribute-required-toggle input[type=checkbox]{margin:0;cursor:pointer}.variant-attribute-required-toggle .required-label{-webkit-user-select:none;user-select:none}.variant-attribute-remove-btn{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-remove-btn:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.variant-attribute-remove-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-add-btn{margin-top:.375rem;padding:.25rem .5rem;font-size:.8125rem;background:none;border:1px dashed #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-add-btn:hover:not([disabled]){color:#2563eb;border-color:#2563eb}.variant-attribute-add-btn[disabled]{opacity:.5;cursor:not-allowed}.field-with-fx{display:flex;align-items:flex-start;gap:4px}.field-with-fx>*:first-child{flex:1;min-width:0}.fx-toggle{width:28px;height:28px;margin-top:22px;border:1px solid #e0e0e0;border-radius:4px;background:#fff;cursor:pointer;font-family:monospace;font-size:.8em;display:flex;align-items:center;justify-content:center;flex-shrink:0}.fx-toggle:hover{background:#f4f4f4}.fx-toggle--inline{margin-top:0}.fx-input-group{flex:1;min-width:0;margin-bottom:.75rem}.fx-input-label{display:block;font-size:.875rem;margin-bottom:.25rem;color:#525252}.fx-input{width:100%;border:1px solid #8d8d8d;border-radius:0;padding:.4rem .75rem;font-family:monospace;font-size:.875rem;background:#f4f4f4}.attribute-value-with-fx{display:flex;align-items:center;gap:4px;flex:1;min-width:0}.attribute-value-with-fx>input{flex:1;min-width:0}.mapping-section{margin-top:1rem}.mapping-section__title{font-size:1rem;font-weight:600;margin:0 0 4px}.mapping-section__description{font-size:.85em;color:#6f6f6f;margin:0 0 12px}.mapping-mode-toggle{display:flex;gap:0;margin-bottom:12px}.mapping-mode-toggle__btn{padding:6px 16px;border:1px solid #e0e0e0;background:#fff;font-size:.85em;cursor:pointer}.mapping-mode-toggle__btn:first-child{border-radius:4px 0 0 4px}.mapping-mode-toggle__btn:last-child{border-radius:0 4px 4px 0;border-left:none}.mapping-mode-toggle__btn--active{background:#0f62fe;color:#fff;border-color:#0f62fe}.mapping-tools{margin-top:12px;border:1px solid #e0e0e0;border-radius:4px;overflow:hidden}.mapping-tools__header{display:flex;align-items:center;gap:6px;padding:8px 12px;background:#f4f4f4;cursor:pointer;font-size:.85em;font-weight:500;-webkit-user-select:none;user-select:none}.mapping-tools__header:hover{background:#e8e8e8}.mapping-tools__chevron{font-size:.7em}.mapping-tools__content{border-top:1px solid #e0e0e0}.mapping-tools__tabs{display:flex;border-bottom:1px solid #e0e0e0}.mapping-tools__tab{padding:6px 16px;border:none;background:transparent;font-size:.8em;cursor:pointer;border-bottom:2px solid transparent}.mapping-tools__tab--active{border-bottom-color:#0f62fe;font-weight:500}\n"] }]
1958
+ }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$3.ProcessLinkStateService }, { type: i0.ChangeDetectorRef }], propDecorators: { save$: [{
990
1959
  type: Input
991
1960
  }], disabled$: [{
992
1961
  type: Input
@@ -1028,8 +1997,7 @@ class CheckJobStatusConfigurationComponent {
1028
1997
  this.handleValid(formValue);
1029
1998
  }
1030
1999
  handleValid(formValue) {
1031
- const valid = !!(formValue?.requestIdVariable &&
1032
- formValue?.statusVariable);
2000
+ const valid = !!(formValue?.requestIdVariable && formValue?.statusVariable);
1033
2001
  this.valid$.next(valid);
1034
2002
  this.valid.emit(valid);
1035
2003
  }
@@ -1045,11 +2013,11 @@ class CheckJobStatusConfigurationComponent {
1045
2013
  });
1046
2014
  }
1047
2015
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: CheckJobStatusConfigurationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1048
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: CheckJobStatusConfigurationComponent, isStandalone: true, selector: "epistola-check-job-status-configuration", inputs: { save$: "save$", disabled$: "disabled$", pluginId: "pluginId", prefillConfiguration$: "prefillConfiguration$" }, outputs: { valid: "valid", configuration: "configuration" }, ngImport: i0, template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: safeDisabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null\n } as obs\"\n>\n <v-input\n name=\"requestIdVariable\"\n [title]=\"'requestIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'requestIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.requestIdVariable || 'epistolaRequestId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"statusVariable\"\n [title]=\"'statusVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'statusVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.statusVariable || 'epistolaStatus'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"documentIdVariable\"\n [title]=\"'documentIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.documentIdVariable || 'epistolaDocumentId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"errorMessageVariable\"\n [title]=\"'errorMessageVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'errorMessageVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.errorMessageVariable || 'epistolaErrorMessage'\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n</v-form>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: FormModule }, { kind: "component", type: i3.FormComponent, selector: "v-form", inputs: ["className"], outputs: ["valueChange"] }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i3.InputComponent, selector: "v-input", inputs: ["name", "type", "title", "titleTranslationKey", "defaultValue", "widthPx", "fullWidth", "margin", "smallMargin", "disabled", "step", "min", "maxLength", "tooltip", "required", "hideNumberSpinBox", "smallLabel", "rows", "clear$", "carbonTheme", "placeholder", "dataTestId", "trim", "presetsTitle", "presetOptions"], outputs: ["valueChange"] }] });
2016
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: CheckJobStatusConfigurationComponent, isStandalone: true, selector: "epistola-check-job-status-configuration", inputs: { save$: "save$", disabled$: "disabled$", pluginId: "pluginId", prefillConfiguration$: "prefillConfiguration$" }, outputs: { valid: "valid", configuration: "configuration" }, ngImport: i0, template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: safeDisabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n } as obs\"\n>\n <v-input\n name=\"requestIdVariable\"\n [title]=\"'requestIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'requestIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.requestIdVariable || 'epistolaRequestId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"statusVariable\"\n [title]=\"'statusVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'statusVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.statusVariable || 'epistolaStatus'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"documentIdVariable\"\n [title]=\"'documentIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.documentIdVariable || 'epistolaDocumentId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"errorMessageVariable\"\n [title]=\"'errorMessageVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'errorMessageVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.errorMessageVariable || 'epistolaErrorMessage'\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n</v-form>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: FormModule }, { kind: "component", type: i3.FormComponent, selector: "v-form", inputs: ["className"], outputs: ["valueChange"] }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i3.InputComponent, selector: "v-input", inputs: ["name", "type", "title", "titleTranslationKey", "defaultValue", "widthPx", "fullWidth", "margin", "smallMargin", "disabled", "step", "min", "maxLength", "tooltip", "required", "hideNumberSpinBox", "smallLabel", "rows", "clear$", "carbonTheme", "placeholder", "dataTestId", "trim", "presetsTitle", "presetOptions"], outputs: ["valueChange"] }] });
1049
2017
  }
1050
2018
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: CheckJobStatusConfigurationComponent, decorators: [{
1051
2019
  type: Component,
1052
- args: [{ selector: 'epistola-check-job-status-configuration', standalone: true, imports: [CommonModule, PluginTranslatePipeModule, FormModule, InputModule], template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: safeDisabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null\n } as obs\"\n>\n <v-input\n name=\"requestIdVariable\"\n [title]=\"'requestIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'requestIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.requestIdVariable || 'epistolaRequestId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"statusVariable\"\n [title]=\"'statusVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'statusVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.statusVariable || 'epistolaStatus'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"documentIdVariable\"\n [title]=\"'documentIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.documentIdVariable || 'epistolaDocumentId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"errorMessageVariable\"\n [title]=\"'errorMessageVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'errorMessageVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.errorMessageVariable || 'epistolaErrorMessage'\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n</v-form>\n" }]
2020
+ args: [{ selector: 'epistola-check-job-status-configuration', standalone: true, imports: [CommonModule, PluginTranslatePipeModule, FormModule, InputModule], template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: safeDisabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n } as obs\"\n>\n <v-input\n name=\"requestIdVariable\"\n [title]=\"'requestIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'requestIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.requestIdVariable || 'epistolaRequestId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"statusVariable\"\n [title]=\"'statusVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'statusVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.statusVariable || 'epistolaStatus'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"documentIdVariable\"\n [title]=\"'documentIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.documentIdVariable || 'epistolaDocumentId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"errorMessageVariable\"\n [title]=\"'errorMessageVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'errorMessageVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.errorMessageVariable || 'epistolaErrorMessage'\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n</v-form>\n" }]
1053
2021
  }], propDecorators: { save$: [{
1054
2022
  type: Input
1055
2023
  }], disabled$: [{
@@ -1088,8 +2056,7 @@ class DownloadDocumentConfigurationComponent {
1088
2056
  this.handleValid(formValue);
1089
2057
  }
1090
2058
  handleValid(formValue) {
1091
- const valid = !!(formValue?.documentIdVariable &&
1092
- formValue?.contentVariable);
2059
+ const valid = !!(formValue?.documentIdVariable && formValue?.contentVariable);
1093
2060
  this.valid$.next(valid);
1094
2061
  this.valid.emit(valid);
1095
2062
  }
@@ -1105,11 +2072,11 @@ class DownloadDocumentConfigurationComponent {
1105
2072
  });
1106
2073
  }
1107
2074
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DownloadDocumentConfigurationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1108
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: DownloadDocumentConfigurationComponent, isStandalone: true, selector: "epistola-download-document-configuration", inputs: { save$: "save$", disabled$: "disabled$", pluginId: "pluginId", prefillConfiguration$: "prefillConfiguration$" }, outputs: { valid: "valid", configuration: "configuration" }, ngImport: i0, template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: safeDisabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null\n } as obs\"\n>\n <v-input\n name=\"documentIdVariable\"\n [title]=\"'documentIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.documentIdVariable || 'epistolaDocumentId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"contentVariable\"\n [title]=\"'contentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'contentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.contentVariable || 'documentContent'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: FormModule }, { kind: "component", type: i3.FormComponent, selector: "v-form", inputs: ["className"], outputs: ["valueChange"] }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i3.InputComponent, selector: "v-input", inputs: ["name", "type", "title", "titleTranslationKey", "defaultValue", "widthPx", "fullWidth", "margin", "smallMargin", "disabled", "step", "min", "maxLength", "tooltip", "required", "hideNumberSpinBox", "smallLabel", "rows", "clear$", "carbonTheme", "placeholder", "dataTestId", "trim", "presetsTitle", "presetOptions"], outputs: ["valueChange"] }] });
2075
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: DownloadDocumentConfigurationComponent, isStandalone: true, selector: "epistola-download-document-configuration", inputs: { save$: "save$", disabled$: "disabled$", pluginId: "pluginId", prefillConfiguration$: "prefillConfiguration$" }, outputs: { valid: "valid", configuration: "configuration" }, ngImport: i0, template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: safeDisabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n } as obs\"\n>\n <v-input\n name=\"documentIdVariable\"\n [title]=\"'documentIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.documentIdVariable || 'epistolaDocumentId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"contentVariable\"\n [title]=\"'contentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'contentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.contentVariable || 'documentContent'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: FormModule }, { kind: "component", type: i3.FormComponent, selector: "v-form", inputs: ["className"], outputs: ["valueChange"] }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i3.InputComponent, selector: "v-input", inputs: ["name", "type", "title", "titleTranslationKey", "defaultValue", "widthPx", "fullWidth", "margin", "smallMargin", "disabled", "step", "min", "maxLength", "tooltip", "required", "hideNumberSpinBox", "smallLabel", "rows", "clear$", "carbonTheme", "placeholder", "dataTestId", "trim", "presetsTitle", "presetOptions"], outputs: ["valueChange"] }] });
1109
2076
  }
1110
2077
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DownloadDocumentConfigurationComponent, decorators: [{
1111
2078
  type: Component,
1112
- args: [{ selector: 'epistola-download-document-configuration', standalone: true, imports: [CommonModule, PluginTranslatePipeModule, FormModule, InputModule], template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: safeDisabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null\n } as obs\"\n>\n <v-input\n name=\"documentIdVariable\"\n [title]=\"'documentIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.documentIdVariable || 'epistolaDocumentId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"contentVariable\"\n [title]=\"'contentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'contentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.contentVariable || 'documentContent'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n" }]
2079
+ args: [{ selector: 'epistola-download-document-configuration', standalone: true, imports: [CommonModule, PluginTranslatePipeModule, FormModule, InputModule], template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: safeDisabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n } as obs\"\n>\n <v-input\n name=\"documentIdVariable\"\n [title]=\"'documentIdVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'documentIdVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.documentIdVariable || 'epistolaDocumentId'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n\n <v-input\n name=\"contentVariable\"\n [title]=\"'contentVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'contentVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.contentVariable || 'documentContent'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n" }]
1113
2080
  }], propDecorators: { save$: [{
1114
2081
  type: Input
1115
2082
  }], disabled$: [{
@@ -1149,9 +2116,9 @@ class EpistolaDownloadComponent {
1149
2116
  this.downloading = true;
1150
2117
  this.error = null;
1151
2118
  const { documentId, tenantId } = this.value;
1152
- const url = `/api/v1/plugin/epistola/documents/${encodeURIComponent(documentId)}/download`
1153
- + `?tenantId=${encodeURIComponent(tenantId)}`
1154
- + `&filename=${encodeURIComponent(this.filename)}`;
2119
+ const url = `/api/v1/plugin/epistola/documents/${encodeURIComponent(documentId)}/download` +
2120
+ `?tenantId=${encodeURIComponent(tenantId)}` +
2121
+ `&filename=${encodeURIComponent(this.filename)}`;
1155
2122
  this.http.get(url, { responseType: 'blob' }).subscribe({
1156
2123
  next: (blob) => {
1157
2124
  const objectUrl = URL.createObjectURL(blob);
@@ -1244,7 +2211,7 @@ class EpistolaRetryFormComponent {
1244
2211
  apiEndpoint;
1245
2212
  formOptions = {
1246
2213
  noAlerts: true,
1247
- buttonSettings: { showCancel: false, showSubmit: false, showPrevious: false, showNext: false }
2214
+ buttonSettings: { showCancel: false, showSubmit: false, showPrevious: false, showNext: false },
1248
2215
  };
1249
2216
  constructor(epistolaPluginService, formIoStateService, cdr, http, sanitizer, configService) {
1250
2217
  this.epistolaPluginService = epistolaPluginService;
@@ -1255,7 +2222,7 @@ class EpistolaRetryFormComponent {
1255
2222
  this.configService = configService;
1256
2223
  this.apiEndpoint = `${this.configService.config.valtimoApi.endpointUri}v1/plugin/epistola`;
1257
2224
  // Debounce preview calls
1258
- this.previewSubscription = this.previewSubject.pipe(debounceTime(1500)).subscribe(data => {
2225
+ this.previewSubscription = this.previewSubject.pipe(debounceTime$1(1500)).subscribe((data) => {
1259
2226
  this.loadPreview(data);
1260
2227
  });
1261
2228
  }
@@ -1298,12 +2265,14 @@ class EpistolaRetryFormComponent {
1298
2265
  URL.revokeObjectURL(this.currentBlobUrl);
1299
2266
  this.currentBlobUrl = null;
1300
2267
  }
1301
- this.http.post(`${this.apiEndpoint}/preview`, {
2268
+ this.http
2269
+ .post(`${this.apiEndpoint}/preview`, {
1302
2270
  documentId,
1303
2271
  processInstanceId,
1304
2272
  sourceActivityId: this.sourceActivityId || null,
1305
- overrides: formData
1306
- }, { responseType: 'blob', headers: new HttpHeaders().set('X-Skip-Interceptor', '422') }).subscribe({
2273
+ overrides: formData,
2274
+ }, { responseType: 'blob', headers: new HttpHeaders().set('X-Skip-Interceptor', '422') })
2275
+ .subscribe({
1307
2276
  next: (blob) => {
1308
2277
  this.currentBlobUrl = URL.createObjectURL(blob);
1309
2278
  this.previewUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.currentBlobUrl);
@@ -1332,7 +2301,7 @@ class EpistolaRetryFormComponent {
1332
2301
  this.previewLoading = false;
1333
2302
  this.cdr.markForCheck();
1334
2303
  }
1335
- }
2304
+ },
1336
2305
  });
1337
2306
  }
1338
2307
  loadForm() {
@@ -1344,7 +2313,9 @@ class EpistolaRetryFormComponent {
1344
2313
  this.cdr.markForCheck();
1345
2314
  return;
1346
2315
  }
1347
- this.loadSubscription = this.epistolaPluginService.getRetryForm(processInstanceId, documentId ?? undefined, this.sourceActivityId).subscribe({
2316
+ this.loadSubscription = this.epistolaPluginService
2317
+ .getRetryForm(processInstanceId, documentId ?? undefined, this.sourceActivityId)
2318
+ .subscribe({
1348
2319
  next: (form) => {
1349
2320
  this.formDefinition = form;
1350
2321
  if (this.value) {
@@ -1363,14 +2334,18 @@ class EpistolaRetryFormComponent {
1363
2334
  this.error = 'Failed to load the retry form. Please try again.';
1364
2335
  this.loading = false;
1365
2336
  this.cdr.markForCheck();
1366
- }
2337
+ },
1367
2338
  });
1368
2339
  }
1369
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaRetryFormComponent, deps: [{ token: EpistolaPluginService }, { token: i3.FormIoStateService }, { token: i0.ChangeDetectorRef }, { token: i1.HttpClient }, { token: i4$1.DomSanitizer }, { token: i2.ConfigService }], target: i0.ɵɵFactoryTarget.Component });
2340
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaRetryFormComponent, deps: [{ token: EpistolaPluginService }, { token: i3.FormIoStateService }, { token: i0.ChangeDetectorRef }, { token: i1.HttpClient }, { token: i4.DomSanitizer }, { token: i2.ConfigService }], target: i0.ɵɵFactoryTarget.Component });
1370
2341
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaRetryFormComponent, isStandalone: true, selector: "epistola-retry-form-component", inputs: { value: "value", disabled: "disabled", label: "label", sourceActivityId: "sourceActivityId" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
1371
2342
  <div *ngIf="loading" class="epistola-retry-loading">Loading form...</div>
1372
2343
  <div *ngIf="error" class="epistola-retry-error">{{ error }}</div>
1373
- <div *ngIf="formDefinition && !loading" class="epistola-retry-container" [class.preview-expanded]="previewExpanded">
2344
+ <div
2345
+ *ngIf="formDefinition && !loading"
2346
+ class="epistola-retry-container"
2347
+ [class.preview-expanded]="previewExpanded"
2348
+ >
1374
2349
  <div class="epistola-retry-form" [hidden]="previewExpanded">
1375
2350
  <formio
1376
2351
  [form]="formDefinition"
@@ -1387,7 +2362,12 @@ class EpistolaRetryFormComponent {
1387
2362
  </button>
1388
2363
  </div>
1389
2364
  <div *ngIf="previewLoading" class="preview-loading">Generating preview...</div>
1390
- <object *ngIf="previewUrl && !previewLoading" [data]="previewUrl" type="application/pdf" class="preview-pdf">
2365
+ <object
2366
+ *ngIf="previewUrl && !previewLoading"
2367
+ [data]="previewUrl"
2368
+ type="application/pdf"
2369
+ class="preview-pdf"
2370
+ >
1391
2371
  PDF preview not supported in this browser.
1392
2372
  </object>
1393
2373
  <div *ngIf="previewError" class="preview-error">{{ previewError }}</div>
@@ -1403,7 +2383,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
1403
2383
  args: [{ standalone: true, imports: [CommonModule, FormioModule], selector: 'epistola-retry-form-component', changeDetection: ChangeDetectionStrategy.OnPush, template: `
1404
2384
  <div *ngIf="loading" class="epistola-retry-loading">Loading form...</div>
1405
2385
  <div *ngIf="error" class="epistola-retry-error">{{ error }}</div>
1406
- <div *ngIf="formDefinition && !loading" class="epistola-retry-container" [class.preview-expanded]="previewExpanded">
2386
+ <div
2387
+ *ngIf="formDefinition && !loading"
2388
+ class="epistola-retry-container"
2389
+ [class.preview-expanded]="previewExpanded"
2390
+ >
1407
2391
  <div class="epistola-retry-form" [hidden]="previewExpanded">
1408
2392
  <formio
1409
2393
  [form]="formDefinition"
@@ -1420,7 +2404,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
1420
2404
  </button>
1421
2405
  </div>
1422
2406
  <div *ngIf="previewLoading" class="preview-loading">Generating preview...</div>
1423
- <object *ngIf="previewUrl && !previewLoading" [data]="previewUrl" type="application/pdf" class="preview-pdf">
2407
+ <object
2408
+ *ngIf="previewUrl && !previewLoading"
2409
+ [data]="previewUrl"
2410
+ type="application/pdf"
2411
+ class="preview-pdf"
2412
+ >
1424
2413
  PDF preview not supported in this browser.
1425
2414
  </object>
1426
2415
  <div *ngIf="previewError" class="preview-error">{{ previewError }}</div>
@@ -1430,7 +2419,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
1430
2419
  </div>
1431
2420
  </div>
1432
2421
  `, 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"] }]
1433
- }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }, { type: i1.HttpClient }, { type: i4$1.DomSanitizer }, { type: i2.ConfigService }], propDecorators: { value: [{
2422
+ }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }, { type: i1.HttpClient }, { type: i4.DomSanitizer }, { type: i2.ConfigService }], propDecorators: { value: [{
1434
2423
  type: Input
1435
2424
  }], valueChange: [{
1436
2425
  type: Output
@@ -1480,9 +2469,9 @@ class EpistolaPreviewButtonComponent {
1480
2469
  this.previewError = null;
1481
2470
  this.revokeBlobUrl();
1482
2471
  const { documentId, tenantId } = this.value;
1483
- const url = `${this.apiEndpoint}/documents/${encodeURIComponent(documentId)}/download`
1484
- + `?tenantId=${encodeURIComponent(tenantId)}`
1485
- + `&filename=preview.pdf`;
2472
+ const url = `${this.apiEndpoint}/documents/${encodeURIComponent(documentId)}/download` +
2473
+ `?tenantId=${encodeURIComponent(tenantId)}` +
2474
+ `&filename=preview.pdf`;
1486
2475
  this.http.get(url, { responseType: 'blob' }).subscribe({
1487
2476
  next: (blob) => {
1488
2477
  this.currentBlobUrl = URL.createObjectURL(blob);
@@ -1492,7 +2481,7 @@ class EpistolaPreviewButtonComponent {
1492
2481
  error: () => {
1493
2482
  this.previewError = 'Could not load the document.';
1494
2483
  this.previewLoading = false;
1495
- }
2484
+ },
1496
2485
  });
1497
2486
  }
1498
2487
  closePreview() {
@@ -1507,7 +2496,7 @@ class EpistolaPreviewButtonComponent {
1507
2496
  this.currentBlobUrl = null;
1508
2497
  }
1509
2498
  }
1510
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaPreviewButtonComponent, deps: [{ token: i1.HttpClient }, { token: i4$1.DomSanitizer }, { token: i2.ConfigService }], target: i0.ɵɵFactoryTarget.Component });
2499
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaPreviewButtonComponent, deps: [{ token: i1.HttpClient }, { token: i4.DomSanitizer }, { token: i2.ConfigService }], target: i0.ɵɵFactoryTarget.Component });
1511
2500
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaPreviewButtonComponent, isStandalone: true, selector: "epistola-preview-button-component", inputs: { value: "value", disabled: "disabled", label: "label" }, outputs: { valueChange: "valueChange" }, ngImport: i0, template: `
1512
2501
  <button
1513
2502
  type="button"
@@ -1523,7 +2512,9 @@ class EpistolaPreviewButtonComponent {
1523
2512
  <div class="preview-modal-content" (click)="$event.stopPropagation()">
1524
2513
  <div class="preview-modal-header">
1525
2514
  <span>Document Preview</span>
1526
- <button type="button" class="preview-modal-close" (click)="closePreview()">&times;</button>
2515
+ <button type="button" class="preview-modal-close" (click)="closePreview()">
2516
+ &times;
2517
+ </button>
1527
2518
  </div>
1528
2519
  <div class="preview-modal-body">
1529
2520
  <div *ngIf="previewLoading" class="preview-loading">Generating preview...</div>
@@ -1558,7 +2549,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
1558
2549
  <div class="preview-modal-content" (click)="$event.stopPropagation()">
1559
2550
  <div class="preview-modal-header">
1560
2551
  <span>Document Preview</span>
1561
- <button type="button" class="preview-modal-close" (click)="closePreview()">&times;</button>
2552
+ <button type="button" class="preview-modal-close" (click)="closePreview()">
2553
+ &times;
2554
+ </button>
1562
2555
  </div>
1563
2556
  <div class="preview-modal-body">
1564
2557
  <div *ngIf="previewLoading" class="preview-loading">Generating preview...</div>
@@ -1575,7 +2568,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
1575
2568
  </div>
1576
2569
  </div>
1577
2570
  `, styles: [".preview-modal-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:10000}.preview-modal-content{background:#fff;border-radius:8px;width:90vw;height:90vh;max-width:1200px;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 8px 32px #0000004d}.preview-modal-header{display:flex;justify-content:space-between;align-items:center;padding:.75rem 1rem;border-bottom:1px solid #dee2e6;font-weight:700;font-size:1rem}.preview-modal-close{background:none;border:none;font-size:1.5rem;cursor:pointer;color:#6c757d;line-height:1;padding:0 .25rem}.preview-modal-close:hover{color:#333}.preview-modal-body{flex:1;overflow:hidden;display:flex;flex-direction:column}.preview-loading,.preview-error{padding:2rem;text-align:center}.preview-error{color:#dc3545}.preview-pdf{width:100%;flex:1}\n"] }]
1578
- }], ctorParameters: () => [{ type: i1.HttpClient }, { type: i4$1.DomSanitizer }, { type: i2.ConfigService }], propDecorators: { value: [{
2571
+ }], ctorParameters: () => [{ type: i1.HttpClient }, { type: i4.DomSanitizer }, { type: i2.ConfigService }], propDecorators: { value: [{
1579
2572
  type: Input
1580
2573
  }], valueChange: [{
1581
2574
  type: Output
@@ -1596,17 +2589,25 @@ class EpistolaDocumentPreviewComponent {
1596
2589
  valueChange = new EventEmitter();
1597
2590
  disabled = false;
1598
2591
  label = 'Document Preview';
2592
+ processDefinitionKey;
2593
+ sourceActivityId;
2594
+ overrideMapping;
1599
2595
  sources = [];
1600
2596
  selectedIndex = 0;
1601
2597
  discovering = false;
1602
2598
  loading = false;
1603
2599
  error = null;
1604
2600
  previewUrl = null;
2601
+ designMode = false;
1605
2602
  initialized = false;
1606
2603
  currentBlobUrl = null;
1607
2604
  discoverSubscription;
1608
2605
  previewSubscription;
1609
2606
  apiEndpoint;
2607
+ /** Whether the component is in configured mode (explicit process link) vs auto-discover mode */
2608
+ get configuredMode() {
2609
+ return !!this.sourceActivityId;
2610
+ }
1610
2611
  constructor(epistolaPluginService, http, sanitizer, configService, formIoStateService, cdr) {
1611
2612
  this.epistolaPluginService = epistolaPluginService;
1612
2613
  this.http = http;
@@ -1616,10 +2617,36 @@ class EpistolaDocumentPreviewComponent {
1616
2617
  this.cdr = cdr;
1617
2618
  this.apiEndpoint = `${this.configService.config.valtimoApi.endpointUri}v1/plugin/epistola`;
1618
2619
  }
2620
+ get overrideMappingScopes() {
2621
+ return this.overrideMapping ? Object.keys(this.overrideMapping) : [];
2622
+ }
2623
+ overrideMappingEntries(scope) {
2624
+ const fields = this.overrideMapping?.[scope];
2625
+ if (!fields || typeof fields !== 'object')
2626
+ return [];
2627
+ return Object.entries(fields).map(([path, field]) => ({ path, field: String(field) }));
2628
+ }
1619
2629
  ngOnChanges(changes) {
1620
2630
  if (!this.initialized) {
1621
2631
  this.initialized = true;
1622
- this.discoverSources();
2632
+ // Detect design mode: no runtime context (Formio builder)
2633
+ const documentId = this.formIoStateService.documentId;
2634
+ if (!documentId) {
2635
+ this.designMode = true;
2636
+ this.cdr.markForCheck();
2637
+ return;
2638
+ }
2639
+ if (this.configuredMode) {
2640
+ this.loadConfiguredPreview();
2641
+ }
2642
+ else {
2643
+ this.discoverSources();
2644
+ }
2645
+ return;
2646
+ }
2647
+ // In configured mode, react to value changes (input overrides from Formio wrapper)
2648
+ if (this.configuredMode && changes['value']) {
2649
+ this.loadConfiguredPreview();
1623
2650
  }
1624
2651
  }
1625
2652
  ngOnDestroy() {
@@ -1629,11 +2656,51 @@ class EpistolaDocumentPreviewComponent {
1629
2656
  }
1630
2657
  onSourceChange(event) {
1631
2658
  this.selectedIndex = +event.target.value;
1632
- this.loadPreview();
2659
+ this.loadDiscoveredPreview();
1633
2660
  }
1634
2661
  refresh() {
1635
- this.loadPreview();
2662
+ if (this.configuredMode) {
2663
+ this.loadConfiguredPreview();
2664
+ }
2665
+ else {
2666
+ this.loadDiscoveredPreview();
2667
+ }
1636
2668
  }
2669
+ /**
2670
+ * Configured mode: preview using the explicitly configured process link + input overrides.
2671
+ */
2672
+ loadConfiguredPreview() {
2673
+ const documentId = this.formIoStateService.documentId;
2674
+ if (!documentId) {
2675
+ this.error = 'Could not determine document ID from context.';
2676
+ this.cdr.markForCheck();
2677
+ return;
2678
+ }
2679
+ this.loading = true;
2680
+ this.error = null;
2681
+ this.cdr.markForCheck();
2682
+ this.revokeBlobUrl();
2683
+ this.previewSubscription?.unsubscribe();
2684
+ this.previewSubscription = this.http
2685
+ .post(`${this.apiEndpoint}/preview`, {
2686
+ documentId,
2687
+ processDefinitionKey: this.processDefinitionKey || null,
2688
+ processInstanceId: this.formIoStateService.processInstanceId || null,
2689
+ sourceActivityId: this.sourceActivityId,
2690
+ inputOverrides: this.value || null,
2691
+ overrides: null,
2692
+ }, {
2693
+ responseType: 'blob',
2694
+ headers: new HttpHeaders().set('X-Skip-Interceptor', '422'),
2695
+ })
2696
+ .subscribe({
2697
+ next: (blob) => this.handlePreviewSuccess(blob),
2698
+ error: (err) => this.handlePreviewError(err),
2699
+ });
2700
+ }
2701
+ /**
2702
+ * Auto-discover mode: discover sources from running process instances.
2703
+ */
1637
2704
  discoverSources() {
1638
2705
  const documentId = this.formIoStateService.documentId;
1639
2706
  if (!documentId) {
@@ -1651,17 +2718,20 @@ class EpistolaDocumentPreviewComponent {
1651
2718
  this.cdr.markForCheck();
1652
2719
  if (sources.length > 0) {
1653
2720
  this.selectedIndex = 0;
1654
- this.loadPreview();
2721
+ this.loadDiscoveredPreview();
1655
2722
  }
1656
2723
  },
1657
2724
  error: (err) => {
1658
2725
  this.error = err.error?.error || 'Failed to discover preview sources';
1659
2726
  this.discovering = false;
1660
2727
  this.cdr.markForCheck();
1661
- }
2728
+ },
1662
2729
  });
1663
2730
  }
1664
- loadPreview() {
2731
+ /**
2732
+ * Auto-discover mode: load preview for the selected discovered source.
2733
+ */
2734
+ loadDiscoveredPreview() {
1665
2735
  const source = this.sources[this.selectedIndex];
1666
2736
  if (!source)
1667
2737
  return;
@@ -1673,44 +2743,48 @@ class EpistolaDocumentPreviewComponent {
1673
2743
  this.cdr.markForCheck();
1674
2744
  this.revokeBlobUrl();
1675
2745
  this.previewSubscription?.unsubscribe();
1676
- this.previewSubscription = this.http.post(`${this.apiEndpoint}/preview`, {
2746
+ this.previewSubscription = this.http
2747
+ .post(`${this.apiEndpoint}/preview`, {
1677
2748
  documentId,
1678
2749
  processInstanceId: source.processInstanceId,
1679
2750
  sourceActivityId: source.activityId,
1680
- overrides: null
2751
+ overrides: null,
1681
2752
  }, {
1682
2753
  responseType: 'blob',
1683
- headers: new HttpHeaders().set('X-Skip-Interceptor', '422')
1684
- }).subscribe({
1685
- next: (blob) => {
1686
- this.currentBlobUrl = URL.createObjectURL(blob);
1687
- this.previewUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.currentBlobUrl);
1688
- this.error = null;
1689
- this.loading = false;
1690
- this.cdr.markForCheck();
1691
- },
1692
- error: (err) => {
1693
- this.previewUrl = null;
1694
- if (err.error instanceof Blob) {
1695
- err.error.text().then((text) => {
1696
- try {
1697
- const body = JSON.parse(text);
1698
- this.error = body.details || body.error || 'Preview could not be generated';
1699
- }
1700
- catch {
1701
- this.error = 'Preview could not be generated';
1702
- }
1703
- this.loading = false;
1704
- this.cdr.markForCheck();
1705
- });
2754
+ headers: new HttpHeaders().set('X-Skip-Interceptor', '422'),
2755
+ })
2756
+ .subscribe({
2757
+ next: (blob) => this.handlePreviewSuccess(blob),
2758
+ error: (err) => this.handlePreviewError(err),
2759
+ });
2760
+ }
2761
+ handlePreviewSuccess(blob) {
2762
+ this.currentBlobUrl = URL.createObjectURL(blob);
2763
+ this.previewUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.currentBlobUrl);
2764
+ this.error = null;
2765
+ this.loading = false;
2766
+ this.cdr.markForCheck();
2767
+ }
2768
+ handlePreviewError(err) {
2769
+ this.previewUrl = null;
2770
+ if (err.error instanceof Blob) {
2771
+ err.error.text().then((text) => {
2772
+ try {
2773
+ const body = JSON.parse(text);
2774
+ this.error = body.details || body.error || 'Preview could not be generated';
1706
2775
  }
1707
- else {
1708
- this.error = err.error?.error || 'Preview could not be generated';
1709
- this.loading = false;
1710
- this.cdr.markForCheck();
2776
+ catch {
2777
+ this.error = 'Preview could not be generated';
1711
2778
  }
1712
- }
1713
- });
2779
+ this.loading = false;
2780
+ this.cdr.markForCheck();
2781
+ });
2782
+ }
2783
+ else {
2784
+ this.error = err.error?.error || 'Preview could not be generated';
2785
+ this.loading = false;
2786
+ this.cdr.markForCheck();
2787
+ }
1714
2788
  }
1715
2789
  revokeBlobUrl() {
1716
2790
  if (this.currentBlobUrl) {
@@ -1719,14 +2793,44 @@ class EpistolaDocumentPreviewComponent {
1719
2793
  this.previewUrl = null;
1720
2794
  }
1721
2795
  }
1722
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDocumentPreviewComponent, deps: [{ token: EpistolaPluginService }, { token: i1.HttpClient }, { token: i4$1.DomSanitizer }, { token: i2.ConfigService }, { token: i3.FormIoStateService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
1723
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaDocumentPreviewComponent, isStandalone: true, selector: "epistola-document-preview-component", inputs: { value: "value", disabled: "disabled", label: "label" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
1724
- <div class="epistola-preview-panel">
2796
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDocumentPreviewComponent, deps: [{ token: EpistolaPluginService }, { token: i1.HttpClient }, { token: i4.DomSanitizer }, { token: i2.ConfigService }, { token: i3.FormIoStateService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
2797
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaDocumentPreviewComponent, isStandalone: true, selector: "epistola-document-preview-component", inputs: { value: "value", disabled: "disabled", label: "label", processDefinitionKey: "processDefinitionKey", sourceActivityId: "sourceActivityId", overrideMapping: "overrideMapping" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
2798
+ <!-- Design-time view: show configuration summary when no runtime context -->
2799
+ <div *ngIf="designMode" class="epistola-preview-panel">
2800
+ <div class="preview-header">
2801
+ <span>{{ label || 'Document Preview' }}</span>
2802
+ </div>
2803
+ <div class="preview-body design-info">
2804
+ <div class="design-section" *ngIf="sourceActivityId">
2805
+ <div class="design-label">Process</div>
2806
+ <div class="design-value">{{ processDefinitionKey || '(any)' }}</div>
2807
+ <div class="design-label">Activity</div>
2808
+ <div class="design-value">{{ sourceActivityId }}</div>
2809
+ </div>
2810
+ <div class="design-section" *ngIf="overrideMapping">
2811
+ <div class="design-label">Input Overrides</div>
2812
+ <div *ngFor="let scope of overrideMappingScopes" class="design-mapping">
2813
+ <div *ngFor="let entry of overrideMappingEntries(scope)" class="design-entry">
2814
+ <span class="design-scope">{{ scope }}</span
2815
+ >.{{ entry.path }}
2816
+ <i class="mdi mdi-arrow-left"></i>
2817
+ <span class="design-field">{{ entry.field }}</span>
2818
+ </div>
2819
+ </div>
2820
+ </div>
2821
+ <div *ngIf="!sourceActivityId" class="design-unconfigured">
2822
+ Auto-discover mode (no process link configured)
2823
+ </div>
2824
+ </div>
2825
+ </div>
2826
+
2827
+ <!-- Runtime view: actual preview -->
2828
+ <div *ngIf="!designMode" class="epistola-preview-panel">
1725
2829
  <div class="preview-header">
1726
2830
  <span>{{ label || 'Document Preview' }}</span>
1727
2831
  <div class="preview-controls">
1728
2832
  <select
1729
- *ngIf="sources.length > 1"
2833
+ *ngIf="!sourceActivityId && sources.length > 1"
1730
2834
  class="preview-select"
1731
2835
  [value]="selectedIndex"
1732
2836
  (change)="onSourceChange($event)"
@@ -1735,19 +2839,20 @@ class EpistolaDocumentPreviewComponent {
1735
2839
  {{ source.templateName }} ({{ source.activityId }})
1736
2840
  </option>
1737
2841
  </select>
1738
- <button type="button" class="preview-refresh" [disabled]="loading || discovering" (click)="refresh()">
2842
+ <button
2843
+ type="button"
2844
+ class="preview-refresh"
2845
+ [disabled]="loading || discovering"
2846
+ (click)="refresh()"
2847
+ >
1739
2848
  <i class="mdi mdi-refresh mr-1"></i>
1740
2849
  {{ loading ? 'Generating...' : 'Refresh' }}
1741
2850
  </button>
1742
2851
  </div>
1743
2852
  </div>
1744
2853
  <div class="preview-body">
1745
- <div *ngIf="discovering" class="preview-loading">
1746
- Discovering documents...
1747
- </div>
1748
- <div *ngIf="loading && !discovering" class="preview-loading">
1749
- Generating preview...
1750
- </div>
2854
+ <div *ngIf="discovering" class="preview-loading">Discovering documents...</div>
2855
+ <div *ngIf="loading && !discovering" class="preview-loading">Generating preview...</div>
1751
2856
  <div *ngIf="error && !loading && !discovering" class="preview-unavailable">
1752
2857
  <i class="mdi mdi-information-outline"></i>
1753
2858
  Preview is niet beschikbaar — niet alle gegevens zijn al ingevuld.
@@ -1760,22 +2865,62 @@ class EpistolaDocumentPreviewComponent {
1760
2865
  >
1761
2866
  PDF preview is not supported in this browser.
1762
2867
  </object>
1763
- <div *ngIf="!previewUrl && !loading && !discovering && !error && sources.length === 0" class="preview-empty">
2868
+ <div
2869
+ *ngIf="
2870
+ !previewUrl &&
2871
+ !loading &&
2872
+ !discovering &&
2873
+ !error &&
2874
+ !sourceActivityId &&
2875
+ sources.length === 0
2876
+ "
2877
+ class="preview-empty"
2878
+ >
1764
2879
  No previewable documents found
1765
2880
  </div>
1766
2881
  </div>
1767
2882
  </div>
1768
- `, isInline: true, styles: [".epistola-preview-panel{border:1px solid #dee2e6;border-radius:4px;background:#f8f9fa;display:flex;flex-direction:column}.preview-header{display:flex;justify-content:space-between;align-items:center;padding:.5rem 1rem;border-bottom:1px solid #dee2e6;font-weight:700;color:#495057;flex-wrap:wrap;gap:.5rem}.preview-controls{display:flex;align-items:center;gap:.5rem}.preview-select{border:1px solid #ced4da;border-radius:4px;padding:.25rem .5rem;font-size:.8rem;background:#fff;max-width:300px}.preview-refresh{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.25rem .75rem;font-size:.8rem;cursor:pointer;display:flex;align-items:center;white-space:nowrap}.preview-refresh:hover:not(:disabled){background:#e9ecef}.preview-refresh:disabled{opacity:.5;cursor:not-allowed}.preview-body{display:flex;flex-direction:column;min-height:500px}.preview-loading{padding:2rem;text-align:center;color:#6c757d;font-style:italic}.preview-unavailable{padding:1.5rem;text-align:center;color:#6c757d;font-style:italic}.preview-unavailable i{margin-right:.25rem}.preview-pdf{width:100%;flex:1;min-height:500px}.preview-empty{padding:2rem;text-align:center;color:#6c757d;font-style:italic}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2883
+ `, 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 });
1769
2884
  }
1770
2885
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaDocumentPreviewComponent, decorators: [{
1771
2886
  type: Component,
1772
2887
  args: [{ standalone: true, imports: [CommonModule], selector: 'epistola-document-preview-component', changeDetection: ChangeDetectionStrategy.OnPush, template: `
1773
- <div class="epistola-preview-panel">
2888
+ <!-- Design-time view: show configuration summary when no runtime context -->
2889
+ <div *ngIf="designMode" class="epistola-preview-panel">
2890
+ <div class="preview-header">
2891
+ <span>{{ label || 'Document Preview' }}</span>
2892
+ </div>
2893
+ <div class="preview-body design-info">
2894
+ <div class="design-section" *ngIf="sourceActivityId">
2895
+ <div class="design-label">Process</div>
2896
+ <div class="design-value">{{ processDefinitionKey || '(any)' }}</div>
2897
+ <div class="design-label">Activity</div>
2898
+ <div class="design-value">{{ sourceActivityId }}</div>
2899
+ </div>
2900
+ <div class="design-section" *ngIf="overrideMapping">
2901
+ <div class="design-label">Input Overrides</div>
2902
+ <div *ngFor="let scope of overrideMappingScopes" class="design-mapping">
2903
+ <div *ngFor="let entry of overrideMappingEntries(scope)" class="design-entry">
2904
+ <span class="design-scope">{{ scope }}</span
2905
+ >.{{ entry.path }}
2906
+ <i class="mdi mdi-arrow-left"></i>
2907
+ <span class="design-field">{{ entry.field }}</span>
2908
+ </div>
2909
+ </div>
2910
+ </div>
2911
+ <div *ngIf="!sourceActivityId" class="design-unconfigured">
2912
+ Auto-discover mode (no process link configured)
2913
+ </div>
2914
+ </div>
2915
+ </div>
2916
+
2917
+ <!-- Runtime view: actual preview -->
2918
+ <div *ngIf="!designMode" class="epistola-preview-panel">
1774
2919
  <div class="preview-header">
1775
2920
  <span>{{ label || 'Document Preview' }}</span>
1776
2921
  <div class="preview-controls">
1777
2922
  <select
1778
- *ngIf="sources.length > 1"
2923
+ *ngIf="!sourceActivityId && sources.length > 1"
1779
2924
  class="preview-select"
1780
2925
  [value]="selectedIndex"
1781
2926
  (change)="onSourceChange($event)"
@@ -1784,19 +2929,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
1784
2929
  {{ source.templateName }} ({{ source.activityId }})
1785
2930
  </option>
1786
2931
  </select>
1787
- <button type="button" class="preview-refresh" [disabled]="loading || discovering" (click)="refresh()">
2932
+ <button
2933
+ type="button"
2934
+ class="preview-refresh"
2935
+ [disabled]="loading || discovering"
2936
+ (click)="refresh()"
2937
+ >
1788
2938
  <i class="mdi mdi-refresh mr-1"></i>
1789
2939
  {{ loading ? 'Generating...' : 'Refresh' }}
1790
2940
  </button>
1791
2941
  </div>
1792
2942
  </div>
1793
2943
  <div class="preview-body">
1794
- <div *ngIf="discovering" class="preview-loading">
1795
- Discovering documents...
1796
- </div>
1797
- <div *ngIf="loading && !discovering" class="preview-loading">
1798
- Generating preview...
1799
- </div>
2944
+ <div *ngIf="discovering" class="preview-loading">Discovering documents...</div>
2945
+ <div *ngIf="loading && !discovering" class="preview-loading">Generating preview...</div>
1800
2946
  <div *ngIf="error && !loading && !discovering" class="preview-unavailable">
1801
2947
  <i class="mdi mdi-information-outline"></i>
1802
2948
  Preview is niet beschikbaar — niet alle gegevens zijn al ingevuld.
@@ -1809,13 +2955,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
1809
2955
  >
1810
2956
  PDF preview is not supported in this browser.
1811
2957
  </object>
1812
- <div *ngIf="!previewUrl && !loading && !discovering && !error && sources.length === 0" class="preview-empty">
2958
+ <div
2959
+ *ngIf="
2960
+ !previewUrl &&
2961
+ !loading &&
2962
+ !discovering &&
2963
+ !error &&
2964
+ !sourceActivityId &&
2965
+ sources.length === 0
2966
+ "
2967
+ class="preview-empty"
2968
+ >
1813
2969
  No previewable documents found
1814
2970
  </div>
1815
2971
  </div>
1816
2972
  </div>
1817
- `, styles: [".epistola-preview-panel{border:1px solid #dee2e6;border-radius:4px;background:#f8f9fa;display:flex;flex-direction:column}.preview-header{display:flex;justify-content:space-between;align-items:center;padding:.5rem 1rem;border-bottom:1px solid #dee2e6;font-weight:700;color:#495057;flex-wrap:wrap;gap:.5rem}.preview-controls{display:flex;align-items:center;gap:.5rem}.preview-select{border:1px solid #ced4da;border-radius:4px;padding:.25rem .5rem;font-size:.8rem;background:#fff;max-width:300px}.preview-refresh{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.25rem .75rem;font-size:.8rem;cursor:pointer;display:flex;align-items:center;white-space:nowrap}.preview-refresh:hover:not(:disabled){background:#e9ecef}.preview-refresh:disabled{opacity:.5;cursor:not-allowed}.preview-body{display:flex;flex-direction:column;min-height:500px}.preview-loading{padding:2rem;text-align:center;color:#6c757d;font-style:italic}.preview-unavailable{padding:1.5rem;text-align:center;color:#6c757d;font-style:italic}.preview-unavailable i{margin-right:.25rem}.preview-pdf{width:100%;flex:1;min-height:500px}.preview-empty{padding:2rem;text-align:center;color:#6c757d;font-style:italic}\n"] }]
1818
- }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i1.HttpClient }, { type: i4$1.DomSanitizer }, { type: i2.ConfigService }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }], propDecorators: { value: [{
2973
+ `, styles: [".epistola-preview-panel{border:1px solid #dee2e6;border-radius:4px;background:#f8f9fa;display:flex;flex-direction:column}.preview-header{display:flex;justify-content:space-between;align-items:center;padding:.5rem 1rem;border-bottom:1px solid #dee2e6;font-weight:700;color:#495057;flex-wrap:wrap;gap:.5rem}.preview-controls{display:flex;align-items:center;gap:.5rem}.preview-select{border:1px solid #ced4da;border-radius:4px;padding:.25rem .5rem;font-size:.8rem;background:#fff;max-width:300px}.preview-refresh{background:none;border:1px solid #6c757d;border-radius:4px;color:#6c757d;padding:.25rem .75rem;font-size:.8rem;cursor:pointer;display:flex;align-items:center;white-space:nowrap}.preview-refresh:hover:not(:disabled){background:#e9ecef}.preview-refresh:disabled{opacity:.5;cursor:not-allowed}.preview-body{display:flex;flex-direction:column;min-height:500px}.preview-loading{padding:2rem;text-align:center;color:#6c757d;font-style:italic}.preview-unavailable{padding:1.5rem;text-align:center;color:#6c757d;font-style:italic}.preview-unavailable i{margin-right:.25rem}.preview-pdf{width:100%;flex:1;min-height:500px}.preview-empty{padding:2rem;text-align:center;color:#6c757d;font-style:italic}.design-info{padding:1rem;min-height:auto}.design-section{margin-bottom:.75rem}.design-label{font-size:.7rem;text-transform:uppercase;color:#868e96;font-weight:600;letter-spacing:.05em}.design-value{font-family:monospace;font-size:.85rem;color:#212529;margin-bottom:.25rem}.design-mapping{margin-top:.25rem}.design-entry{font-family:monospace;font-size:.8rem;color:#495057;padding:.15rem 0}.design-scope{color:#0d6efd}.design-field{color:#198754}.design-entry i{font-size:.7rem;margin:0 .25rem;color:#adb5bd}.design-unconfigured{color:#6c757d;font-style:italic;font-size:.85rem}\n"] }]
2974
+ }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i1.HttpClient }, { type: i4.DomSanitizer }, { type: i2.ConfigService }, { type: i3.FormIoStateService }, { type: i0.ChangeDetectorRef }], propDecorators: { value: [{
1819
2975
  type: Input
1820
2976
  }], valueChange: [{
1821
2977
  type: Output
@@ -1823,8 +2979,229 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
1823
2979
  type: Input
1824
2980
  }], label: [{
1825
2981
  type: Input
2982
+ }], processDefinitionKey: [{
2983
+ type: Input
2984
+ }], sourceActivityId: [{
2985
+ type: Input
2986
+ }], overrideMapping: [{
2987
+ type: Input
1826
2988
  }] } });
1827
2989
 
2990
+ class EpistolaAdminPageComponent {
2991
+ adminService;
2992
+ route;
2993
+ router;
2994
+ cards = [];
2995
+ selectedCard = null;
2996
+ activeTab = 'actions';
2997
+ loading = false;
2998
+ pluginVersion = null;
2999
+ connectionStatuses = [];
3000
+ usageEntries = [];
3001
+ pendingJobs = [];
3002
+ connectionLoaded = false;
3003
+ usageLoaded = false;
3004
+ pendingLoaded = false;
3005
+ deepLinkConfigId = null;
3006
+ constructor(adminService, route, router) {
3007
+ this.adminService = adminService;
3008
+ this.route = route;
3009
+ this.router = router;
3010
+ }
3011
+ ngOnInit() {
3012
+ this.deepLinkConfigId = this.route.snapshot.queryParamMap.get('configurationId');
3013
+ const tab = this.route.snapshot.queryParamMap.get('tab');
3014
+ if (tab === 'pending' || tab === 'actions') {
3015
+ this.activeTab = tab;
3016
+ }
3017
+ this.loadData();
3018
+ this.loadPluginVersion();
3019
+ }
3020
+ selectConfiguration(card) {
3021
+ this.selectedCard = card;
3022
+ this.activeTab = 'actions';
3023
+ this.updateUrl(card.configurationId, this.activeTab);
3024
+ }
3025
+ backToOverview() {
3026
+ this.selectedCard = null;
3027
+ this.activeTab = 'actions';
3028
+ this.updateUrl(null, null);
3029
+ }
3030
+ setActiveTab(tab) {
3031
+ this.activeTab = tab;
3032
+ this.updateUrl(this.selectedCard?.configurationId ?? null, tab);
3033
+ }
3034
+ refresh() {
3035
+ this.selectedCard = null;
3036
+ this.loadData();
3037
+ }
3038
+ exportProcessLink(entry) {
3039
+ this.adminService.exportProcessLink(entry.processLinkId).subscribe({
3040
+ next: (blob) => {
3041
+ const url = URL.createObjectURL(blob);
3042
+ const anchor = document.createElement('a');
3043
+ anchor.href = url;
3044
+ anchor.download = `${entry.activityId}.process-link.json`;
3045
+ anchor.click();
3046
+ URL.revokeObjectURL(url);
3047
+ },
3048
+ });
3049
+ }
3050
+ updateUrl(configurationId, tab) {
3051
+ this.router.navigate([], {
3052
+ relativeTo: this.route,
3053
+ queryParams: {
3054
+ configurationId: configurationId ?? null,
3055
+ tab: tab ?? null,
3056
+ },
3057
+ replaceUrl: true,
3058
+ });
3059
+ }
3060
+ loadData() {
3061
+ this.loading = true;
3062
+ this.connectionLoaded = false;
3063
+ this.usageLoaded = false;
3064
+ this.pendingLoaded = false;
3065
+ this.adminService.getConnectionStatus().subscribe({
3066
+ next: (statuses) => {
3067
+ this.connectionStatuses = statuses;
3068
+ this.connectionLoaded = true;
3069
+ this.tryBuildCards();
3070
+ },
3071
+ error: () => {
3072
+ this.connectionStatuses = [];
3073
+ this.connectionLoaded = true;
3074
+ this.tryBuildCards();
3075
+ },
3076
+ });
3077
+ this.adminService.getPluginUsage().subscribe({
3078
+ next: (entries) => {
3079
+ this.usageEntries = entries;
3080
+ this.usageLoaded = true;
3081
+ this.tryBuildCards();
3082
+ },
3083
+ error: () => {
3084
+ this.usageEntries = [];
3085
+ this.usageLoaded = true;
3086
+ this.tryBuildCards();
3087
+ },
3088
+ });
3089
+ this.adminService.getPendingJobs().subscribe({
3090
+ next: (jobs) => {
3091
+ this.pendingJobs = jobs;
3092
+ this.pendingLoaded = true;
3093
+ this.tryBuildCards();
3094
+ },
3095
+ error: () => {
3096
+ this.pendingJobs = [];
3097
+ this.pendingLoaded = true;
3098
+ this.tryBuildCards();
3099
+ },
3100
+ });
3101
+ }
3102
+ tryBuildCards() {
3103
+ if (!this.connectionLoaded || !this.usageLoaded || !this.pendingLoaded) {
3104
+ return;
3105
+ }
3106
+ this.cards = this.connectionStatuses.map((status) => {
3107
+ const entries = this.usageEntries.filter((e) => e.configurationId === status.configurationId);
3108
+ const jobs = this.pendingJobs.filter((j) => j.tenantId === status.tenantId);
3109
+ const problemCount = entries.reduce((sum, e) => sum + e.problems.length, 0);
3110
+ return {
3111
+ configurationId: status.configurationId,
3112
+ configurationTitle: status.configurationTitle,
3113
+ tenantId: status.tenantId,
3114
+ reachable: status.reachable,
3115
+ latencyMs: status.latencyMs,
3116
+ errorMessage: status.errorMessage,
3117
+ serverVersion: status.serverVersion,
3118
+ usageCount: entries.length,
3119
+ problemCount,
3120
+ usageEntries: entries,
3121
+ pendingJobs: jobs,
3122
+ };
3123
+ });
3124
+ // Restore deep link selection
3125
+ if (this.deepLinkConfigId) {
3126
+ const match = this.cards.find((c) => c.configurationId === this.deepLinkConfigId);
3127
+ if (match) {
3128
+ this.selectedCard = match;
3129
+ }
3130
+ this.deepLinkConfigId = null;
3131
+ }
3132
+ this.loading = false;
3133
+ }
3134
+ loadPluginVersion() {
3135
+ this.adminService.getVersions().subscribe({
3136
+ next: (info) => {
3137
+ this.pluginVersion = info.pluginVersion;
3138
+ },
3139
+ });
3140
+ }
3141
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminPageComponent, deps: [{ token: EpistolaAdminService }, { token: i2$4.ActivatedRoute }, { token: i2$4.Router }], target: i0.ɵɵFactoryTarget.Component });
3142
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaAdminPageComponent, isStandalone: true, selector: "epistola-admin-page", ngImport: i0, template: "<div class=\"epistola-admin\">\n <!-- Overview: card grid (no configuration selected) -->\n <ng-container *ngIf=\"!selectedCard\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"d-flex align-items-center\">\n <h5 class=\"mb-0\">{{ 'epistolaAdminOverview' | pluginTranslate: 'epistola' | async }}</h5>\n <span *ngIf=\"pluginVersion\" class=\"version-badge ms-2\">v{{ pluginVersion }}</span>\n </div>\n <button class=\"btn btn-outline-primary btn-sm\" (click)=\"refresh()\" [disabled]=\"loading\">\n {{ 'epistolaAdminRefresh' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div *ngIf=\"loading\" class=\"text-muted\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length === 0\" class=\"text-muted\">\n {{ 'epistolaAdminNoConfigurations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length > 0\" class=\"card-grid\">\n <div\n *ngFor=\"let card of cards\"\n class=\"config-card\"\n [class.config-card--ok]=\"card.reachable && card.problemCount === 0\"\n [class.config-card--warning]=\"card.reachable && card.problemCount > 0\"\n [class.config-card--error]=\"!card.reachable\"\n (click)=\"selectConfiguration(card)\"\n >\n <div class=\"config-card__header\">\n <span\n class=\"status-dot\"\n [class.status-dot--ok]=\"card.reachable\"\n [class.status-dot--error]=\"!card.reachable\"\n >\n </span>\n <h5 class=\"config-card__title\">{{ card.configurationTitle }}</h5>\n </div>\n\n <div class=\"config-card__body\">\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async\n }}</span>\n <code class=\"config-card__value\">{{ card.tenantId }}</code>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" [type]=\"card.reachable ? 'green' : 'red'\">\n {{\n card.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n </div>\n <div *ngIf=\"card.serverVersion\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.serverVersion }}</span>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.usageCount }}</span>\n </div>\n <div *ngIf=\"card.pendingJobs.length > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">{{ card.pendingJobs.length }}</cds-tag>\n </div>\n <div *ngIf=\"card.problemCount > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"red\">{{ card.problemCount }}</cds-tag>\n </div>\n </div>\n\n <div class=\"config-card__footer\">\n <span class=\"config-card__latency\">{{ card.latencyMs }} ms</span>\n </div>\n </div>\n </div>\n </ng-container>\n\n <!-- Detail view: selected configuration -->\n <ng-container *ngIf=\"selectedCard\">\n <div class=\"detail-header mb-3\">\n <button class=\"btn btn-link btn-sm p-0\" (click)=\"backToOverview()\">\n &larr; {{ 'epistolaAdminBackToOverview' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div class=\"detail-summary mb-4\">\n <h4>\n <span\n class=\"status-dot me-2\"\n [class.status-dot--ok]=\"selectedCard.reachable\"\n [class.status-dot--error]=\"!selectedCard.reachable\"\n >\n </span>\n {{ selectedCard.configurationTitle }}\n </h4>\n\n <table class=\"table table-sm detail-info-table\">\n <tbody>\n <tr>\n <th>{{ 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <code>{{ selectedCard.tenantId }}</code>\n </td>\n </tr>\n <tr>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <cds-tag size=\"sm\" [type]=\"selectedCard.reachable ? 'green' : 'red'\">\n {{\n selectedCard.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n <span class=\"text-muted ms-2\">{{ selectedCard.latencyMs }} ms</span>\n </td>\n </tr>\n <tr *ngIf=\"selectedCard.serverVersion\">\n <th>{{ 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async }}</th>\n <td>{{ selectedCard.serverVersion }}</td>\n </tr>\n <tr *ngIf=\"selectedCard.errorMessage\">\n <th>{{ 'epistolaAdminError' | pluginTranslate: 'epistola' | async }}</th>\n <td class=\"text-danger\">{{ selectedCard.errorMessage }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Tabs -->\n <ng-template #actionsHeading>\n {{ 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ selectedCard.usageEntries.length }}</cds-tag>\n </ng-template>\n\n <ng-template #pendingHeading>\n {{ 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n size=\"sm\"\n [type]=\"selectedCard.pendingJobs.length > 0 ? 'blue' : 'gray'\"\n class=\"ms-1\"\n >\n {{ selectedCard.pendingJobs.length }}\n </cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"actionsHeading\"\n [active]=\"activeTab === 'actions'\"\n (selected)=\"setActiveTab('actions')\"\n >\n <div *ngIf=\"selectedCard.usageEntries.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoUsageForConfig' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.usageEntries.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCase' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminAction' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let entry of selectedCard.usageEntries\"\n [class.table-warning]=\"entry.problems.length > 0\"\n >\n <td>{{ entry.caseDefinitionKey || '-' }}</td>\n <td>\n <a\n *ngIf=\"entry.caseDefinitionKey && entry.caseDefinitionVersionTag\"\n [routerLink]=\"[\n '/case-management',\n 'case',\n entry.caseDefinitionKey,\n 'version',\n entry.caseDefinitionVersionTag,\n 'processes',\n entry.processDefinitionKey,\n ]\"\n class=\"usage-link\"\n >\n {{ entry.processDefinitionName }}\n </a>\n <span *ngIf=\"!entry.caseDefinitionKey || !entry.caseDefinitionVersionTag\">\n {{ entry.processDefinitionName }}\n </span>\n </td>\n <td>{{ entry.activityName }}</td>\n <td>\n <code>{{ entry.actionKey }}</code>\n </td>\n <td>\n <cds-tag *ngIf=\"entry.problems.length === 0\" size=\"sm\" type=\"green\">OK</cds-tag>\n <cds-tag\n *ngFor=\"let problem of entry.problems\"\n size=\"sm\"\n type=\"red\"\n class=\"d-block mb-1\"\n >\n {{ problem }}\n </cds-tag>\n </td>\n <td>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"exportProcessLink(entry)\"\n [title]=\"'epistolaAdminExport' | pluginTranslate: 'epistola' | async\"\n >\n &#x2913;\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"pendingHeading\"\n [active]=\"activeTab === 'pending'\"\n (selected)=\"setActiveTab('pending')\"\n >\n <div *ngIf=\"selectedCard.pendingJobs.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoPendingJobs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.pendingJobs.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminRequestId' | pluginTranslate: 'epistola' | async }}</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let job of selectedCard.pendingJobs\">\n <td>{{ job.processDefinitionName }}</td>\n <td>{{ job.activityName }}</td>\n <td>\n <code>{{ job.requestId }}</code>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n</div>\n", styles: [".epistola-admin{padding:1.5rem}.epistola-admin .version-badge{font-size:.75rem;font-weight:500;padding:.2em .6em;border-radius:4px;background-color:#e8e8e8;color:#525252}.epistola-admin .badge{font-size:.85em;padding:.35em .65em}.epistola-admin code{font-size:.9em;color:#525252}.epistola-admin table th{font-weight:600;white-space:nowrap}.epistola-admin .status-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:.5rem;flex-shrink:0;background-color:#adb5bd}.epistola-admin .status-dot--ok{background-color:#198754}.epistola-admin .status-dot--error{background-color:#dc3545}.epistola-admin .card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.epistola-admin .config-card{border:1px solid #dee2e6;border-radius:8px;padding:1.25rem;cursor:pointer;transition:box-shadow .15s ease,border-color .15s ease;background:#fff}.epistola-admin .config-card:hover{box-shadow:0 2px 8px #0000001a}.epistola-admin .config-card--ok{border-left:4px solid #198754}.epistola-admin .config-card--warning{border-left:4px solid #ffc107}.epistola-admin .config-card--error{border-left:4px solid #dc3545}.epistola-admin .config-card__header{display:flex;align-items:center;margin-bottom:1rem}.epistola-admin .config-card__title{margin:0;font-size:1.05rem;font-weight:600;color:#161616}.epistola-admin .config-card__body{display:flex;flex-direction:column;gap:.5rem}.epistola-admin .config-card__field{display:flex;justify-content:space-between;align-items:center}.epistola-admin .config-card__label{font-size:.875rem;color:#6c757d}.epistola-admin .config-card__value{font-size:.875rem;color:#161616}.epistola-admin .config-card__footer{margin-top:1rem;padding-top:.75rem;border-top:1px solid #f0f0f0;text-align:right}.epistola-admin .config-card__latency{font-size:.8rem;color:#adb5bd}.epistola-admin .usage-link{color:#0f62fe;text-decoration:none}.epistola-admin .usage-link:hover{text-decoration:underline}.epistola-admin .detail-info-table{max-width:500px}.epistola-admin .detail-info-table th{width:140px}.epistola-admin .detail-summary{padding:1rem 0;border-bottom:1px solid #dee2e6}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2$4.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: TabsModule }, { kind: "component", type: i5.Tabs, selector: "cds-tabs, ibm-tabs", inputs: ["position", "cacheActive", "followFocus", "isNavigation", "ariaLabel", "ariaLabelledby", "type", "theme", "skeleton"] }, { kind: "component", type: i5.Tab, selector: "cds-tab, ibm-tab", inputs: ["heading", "title", "context", "active", "disabled", "tabIndex", "id", "cacheActive", "tabContent", "templateContext"], outputs: ["selected"] }, { kind: "ngmodule", type: TagModule }, { kind: "component", type: i6.Tag, selector: "cds-tag, ibm-tag", inputs: ["type", "size", "class", "skeleton"] }] });
3143
+ }
3144
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminPageComponent, decorators: [{
3145
+ type: Component,
3146
+ args: [{ selector: 'epistola-admin-page', standalone: true, imports: [CommonModule, RouterModule, PluginTranslatePipeModule, TabsModule, TagModule], template: "<div class=\"epistola-admin\">\n <!-- Overview: card grid (no configuration selected) -->\n <ng-container *ngIf=\"!selectedCard\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"d-flex align-items-center\">\n <h5 class=\"mb-0\">{{ 'epistolaAdminOverview' | pluginTranslate: 'epistola' | async }}</h5>\n <span *ngIf=\"pluginVersion\" class=\"version-badge ms-2\">v{{ pluginVersion }}</span>\n </div>\n <button class=\"btn btn-outline-primary btn-sm\" (click)=\"refresh()\" [disabled]=\"loading\">\n {{ 'epistolaAdminRefresh' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div *ngIf=\"loading\" class=\"text-muted\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length === 0\" class=\"text-muted\">\n {{ 'epistolaAdminNoConfigurations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!loading && cards.length > 0\" class=\"card-grid\">\n <div\n *ngFor=\"let card of cards\"\n class=\"config-card\"\n [class.config-card--ok]=\"card.reachable && card.problemCount === 0\"\n [class.config-card--warning]=\"card.reachable && card.problemCount > 0\"\n [class.config-card--error]=\"!card.reachable\"\n (click)=\"selectConfiguration(card)\"\n >\n <div class=\"config-card__header\">\n <span\n class=\"status-dot\"\n [class.status-dot--ok]=\"card.reachable\"\n [class.status-dot--error]=\"!card.reachable\"\n >\n </span>\n <h5 class=\"config-card__title\">{{ card.configurationTitle }}</h5>\n </div>\n\n <div class=\"config-card__body\">\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async\n }}</span>\n <code class=\"config-card__value\">{{ card.tenantId }}</code>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" [type]=\"card.reachable ? 'green' : 'red'\">\n {{\n card.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n </div>\n <div *ngIf=\"card.serverVersion\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.serverVersion }}</span>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.usageCount }}</span>\n </div>\n <div *ngIf=\"card.pendingJobs.length > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">{{ card.pendingJobs.length }}</cds-tag>\n </div>\n <div *ngIf=\"card.problemCount > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"red\">{{ card.problemCount }}</cds-tag>\n </div>\n </div>\n\n <div class=\"config-card__footer\">\n <span class=\"config-card__latency\">{{ card.latencyMs }} ms</span>\n </div>\n </div>\n </div>\n </ng-container>\n\n <!-- Detail view: selected configuration -->\n <ng-container *ngIf=\"selectedCard\">\n <div class=\"detail-header mb-3\">\n <button class=\"btn btn-link btn-sm p-0\" (click)=\"backToOverview()\">\n &larr; {{ 'epistolaAdminBackToOverview' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div class=\"detail-summary mb-4\">\n <h4>\n <span\n class=\"status-dot me-2\"\n [class.status-dot--ok]=\"selectedCard.reachable\"\n [class.status-dot--error]=\"!selectedCard.reachable\"\n >\n </span>\n {{ selectedCard.configurationTitle }}\n </h4>\n\n <table class=\"table table-sm detail-info-table\">\n <tbody>\n <tr>\n <th>{{ 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <code>{{ selectedCard.tenantId }}</code>\n </td>\n </tr>\n <tr>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <cds-tag size=\"sm\" [type]=\"selectedCard.reachable ? 'green' : 'red'\">\n {{\n selectedCard.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n <span class=\"text-muted ms-2\">{{ selectedCard.latencyMs }} ms</span>\n </td>\n </tr>\n <tr *ngIf=\"selectedCard.serverVersion\">\n <th>{{ 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async }}</th>\n <td>{{ selectedCard.serverVersion }}</td>\n </tr>\n <tr *ngIf=\"selectedCard.errorMessage\">\n <th>{{ 'epistolaAdminError' | pluginTranslate: 'epistola' | async }}</th>\n <td class=\"text-danger\">{{ selectedCard.errorMessage }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Tabs -->\n <ng-template #actionsHeading>\n {{ 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ selectedCard.usageEntries.length }}</cds-tag>\n </ng-template>\n\n <ng-template #pendingHeading>\n {{ 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n size=\"sm\"\n [type]=\"selectedCard.pendingJobs.length > 0 ? 'blue' : 'gray'\"\n class=\"ms-1\"\n >\n {{ selectedCard.pendingJobs.length }}\n </cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\">\n <cds-tab\n [heading]=\"actionsHeading\"\n [active]=\"activeTab === 'actions'\"\n (selected)=\"setActiveTab('actions')\"\n >\n <div *ngIf=\"selectedCard.usageEntries.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoUsageForConfig' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.usageEntries.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCase' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminAction' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let entry of selectedCard.usageEntries\"\n [class.table-warning]=\"entry.problems.length > 0\"\n >\n <td>{{ entry.caseDefinitionKey || '-' }}</td>\n <td>\n <a\n *ngIf=\"entry.caseDefinitionKey && entry.caseDefinitionVersionTag\"\n [routerLink]=\"[\n '/case-management',\n 'case',\n entry.caseDefinitionKey,\n 'version',\n entry.caseDefinitionVersionTag,\n 'processes',\n entry.processDefinitionKey,\n ]\"\n class=\"usage-link\"\n >\n {{ entry.processDefinitionName }}\n </a>\n <span *ngIf=\"!entry.caseDefinitionKey || !entry.caseDefinitionVersionTag\">\n {{ entry.processDefinitionName }}\n </span>\n </td>\n <td>{{ entry.activityName }}</td>\n <td>\n <code>{{ entry.actionKey }}</code>\n </td>\n <td>\n <cds-tag *ngIf=\"entry.problems.length === 0\" size=\"sm\" type=\"green\">OK</cds-tag>\n <cds-tag\n *ngFor=\"let problem of entry.problems\"\n size=\"sm\"\n type=\"red\"\n class=\"d-block mb-1\"\n >\n {{ problem }}\n </cds-tag>\n </td>\n <td>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"exportProcessLink(entry)\"\n [title]=\"'epistolaAdminExport' | pluginTranslate: 'epistola' | async\"\n >\n &#x2913;\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"pendingHeading\"\n [active]=\"activeTab === 'pending'\"\n (selected)=\"setActiveTab('pending')\"\n >\n <div *ngIf=\"selectedCard.pendingJobs.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoPendingJobs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.pendingJobs.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminRequestId' | pluginTranslate: 'epistola' | async }}</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let job of selectedCard.pendingJobs\">\n <td>{{ job.processDefinitionName }}</td>\n <td>{{ job.activityName }}</td>\n <td>\n <code>{{ job.requestId }}</code>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n</div>\n", styles: [".epistola-admin{padding:1.5rem}.epistola-admin .version-badge{font-size:.75rem;font-weight:500;padding:.2em .6em;border-radius:4px;background-color:#e8e8e8;color:#525252}.epistola-admin .badge{font-size:.85em;padding:.35em .65em}.epistola-admin code{font-size:.9em;color:#525252}.epistola-admin table th{font-weight:600;white-space:nowrap}.epistola-admin .status-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:.5rem;flex-shrink:0;background-color:#adb5bd}.epistola-admin .status-dot--ok{background-color:#198754}.epistola-admin .status-dot--error{background-color:#dc3545}.epistola-admin .card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.epistola-admin .config-card{border:1px solid #dee2e6;border-radius:8px;padding:1.25rem;cursor:pointer;transition:box-shadow .15s ease,border-color .15s ease;background:#fff}.epistola-admin .config-card:hover{box-shadow:0 2px 8px #0000001a}.epistola-admin .config-card--ok{border-left:4px solid #198754}.epistola-admin .config-card--warning{border-left:4px solid #ffc107}.epistola-admin .config-card--error{border-left:4px solid #dc3545}.epistola-admin .config-card__header{display:flex;align-items:center;margin-bottom:1rem}.epistola-admin .config-card__title{margin:0;font-size:1.05rem;font-weight:600;color:#161616}.epistola-admin .config-card__body{display:flex;flex-direction:column;gap:.5rem}.epistola-admin .config-card__field{display:flex;justify-content:space-between;align-items:center}.epistola-admin .config-card__label{font-size:.875rem;color:#6c757d}.epistola-admin .config-card__value{font-size:.875rem;color:#161616}.epistola-admin .config-card__footer{margin-top:1rem;padding-top:.75rem;border-top:1px solid #f0f0f0;text-align:right}.epistola-admin .config-card__latency{font-size:.8rem;color:#adb5bd}.epistola-admin .usage-link{color:#0f62fe;text-decoration:none}.epistola-admin .usage-link:hover{text-decoration:underline}.epistola-admin .detail-info-table{max-width:500px}.epistola-admin .detail-info-table th{width:140px}.epistola-admin .detail-summary{padding:1rem 0;border-bottom:1px solid #dee2e6}\n"] }]
3147
+ }], ctorParameters: () => [{ type: EpistolaAdminService }, { type: i2$4.ActivatedRoute }, { type: i2$4.Router }] });
3148
+
3149
+ function isRuntimeWindow(value) {
3150
+ return typeof value === 'object' && value !== null;
3151
+ }
3152
+ /**
3153
+ * Reads the runtime feature flag that decides whether the Epistola plugin
3154
+ * surfaces (admin menu, /epistola route, plugin specification, Formio
3155
+ * components) should activate in the host Valtimo app.
3156
+ *
3157
+ * The flag is sourced from `window['env']['epistolaEnabled']`, populated at
3158
+ * container start by `envsubst` against `assets/config.template.js` (the
3159
+ * standard Valtimo runtime-config pattern). Defaults to enabled — only the
3160
+ * literal `false` or string `'false'` disables the plugin, matching the
3161
+ * backend's `epistola.enabled` `matchIfMissing = true` semantics.
3162
+ *
3163
+ * Exposed as a runtime helper rather than evaluated directly in `@NgModule`
3164
+ * decorator metadata because Angular's AOT compiler cannot statically resolve
3165
+ * `window` accesses (NG1010). Read from runtime code such as specification
3166
+ * property getters, route guards, or the environment initializer instead.
3167
+ */
3168
+ function isEpistolaEnabled() {
3169
+ const runtimeWindow = Reflect.get(globalThis, 'window');
3170
+ if (!runtimeWindow)
3171
+ return true;
3172
+ if (!isRuntimeWindow(runtimeWindow))
3173
+ return true;
3174
+ const flag = runtimeWindow.env ? runtimeWindow.env.epistolaEnabled : undefined;
3175
+ return flag !== false && flag !== 'false';
3176
+ }
3177
+
3178
+ const epistolaEnabledGuard = () => {
3179
+ if (isEpistolaEnabled())
3180
+ return true;
3181
+ return inject(Router).parseUrl('/');
3182
+ };
3183
+
3184
+ const routes = [
3185
+ {
3186
+ path: 'epistola',
3187
+ component: EpistolaAdminPageComponent,
3188
+ canActivate: [epistolaEnabledGuard, AuthGuardService],
3189
+ data: { title: 'Epistola', roles: ['ROLE_ADMIN'] },
3190
+ },
3191
+ ];
3192
+ class EpistolaAdminRoutingModule {
3193
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
3194
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, imports: [i2$4.RouterModule], exports: [RouterModule] });
3195
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, imports: [RouterModule.forChild(routes), RouterModule] });
3196
+ }
3197
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminRoutingModule, decorators: [{
3198
+ type: NgModule,
3199
+ args: [{
3200
+ imports: [RouterModule.forChild(routes)],
3201
+ exports: [RouterModule],
3202
+ }]
3203
+ }] });
3204
+
1828
3205
  const EPISTOLA_DOWNLOAD_OPTIONS = {
1829
3206
  type: 'epistola-download',
1830
3207
  selector: 'epistola-download-button',
@@ -1877,11 +3254,541 @@ const EPISTOLA_DOCUMENT_PREVIEW_OPTIONS = {
1877
3254
  group: 'basic',
1878
3255
  icon: 'file-pdf-o',
1879
3256
  emptyValue: null,
1880
- fieldOptions: ['label'],
3257
+ fieldOptions: ['label', 'processDefinitionKey', 'sourceActivityId', 'overrideMapping'],
3258
+ editForm: () => ({
3259
+ components: [
3260
+ {
3261
+ type: 'epistola-process-link-selector',
3262
+ key: 'processLinkSelection',
3263
+ label: 'Process Link',
3264
+ weight: 10,
3265
+ validate: { required: true },
3266
+ },
3267
+ {
3268
+ type: 'epistola-override-builder',
3269
+ key: 'overrideMapping',
3270
+ label: 'Input Overrides',
3271
+ weight: 20,
3272
+ },
3273
+ ],
3274
+ }),
1881
3275
  };
1882
3276
  function registerEpistolaDocumentPreviewComponent(injector) {
1883
- if (!customElements.get(EPISTOLA_DOCUMENT_PREVIEW_OPTIONS.selector)) {
1884
- registerCustomFormioComponent(EPISTOLA_DOCUMENT_PREVIEW_OPTIONS, EpistolaDocumentPreviewComponent, injector);
3277
+ if (customElements.get(EPISTOLA_DOCUMENT_PREVIEW_OPTIONS.selector)) {
3278
+ return;
3279
+ }
3280
+ // Register the base component (Angular element + Formio component class)
3281
+ registerCustomFormioComponent(EPISTOLA_DOCUMENT_PREVIEW_OPTIONS, EpistolaDocumentPreviewComponent, injector);
3282
+ // Get the Formio Components registry and the registered base class
3283
+ const Formio = window.Formio;
3284
+ if (!Formio?.Components)
3285
+ return;
3286
+ const BasePreviewComponent = Formio.Components.components[EPISTOLA_DOCUMENT_PREVIEW_OPTIONS.type];
3287
+ if (!BasePreviewComponent)
3288
+ return;
3289
+ // Extend the base class to listen for form data changes and compute input overrides
3290
+ class PreviewWithOverrides extends BasePreviewComponent {
3291
+ _debounceTimer = null;
3292
+ _changeListenerAttached = false;
3293
+ attach(element) {
3294
+ // Bidirectional sync between processLinkSelection object and separate properties.
3295
+ // The editForm uses processLinkSelection (single field), while the component
3296
+ // config and Angular inputs use processDefinitionKey + sourceActivityId.
3297
+ if (this.component?.processLinkSelection) {
3298
+ const sel = this.component.processLinkSelection;
3299
+ this.component.processDefinitionKey = sel.processDefinitionKey || '';
3300
+ this.component.sourceActivityId = sel.sourceActivityId || '';
3301
+ }
3302
+ else if (this.component?.processDefinitionKey && this.component?.sourceActivityId) {
3303
+ this.component.processLinkSelection = {
3304
+ processDefinitionKey: this.component.processDefinitionKey,
3305
+ sourceActivityId: this.component.sourceActivityId,
3306
+ };
3307
+ }
3308
+ const result = super.attach(element);
3309
+ if (this._customAngularElement) {
3310
+ this._customAngularElement['processDefinitionKey'] =
3311
+ this.component.processDefinitionKey || '';
3312
+ this._customAngularElement['sourceActivityId'] = this.component.sourceActivityId || '';
3313
+ }
3314
+ // Listen to form changes and compute input overrides from the mapping
3315
+ if (this.root && this.component?.overrideMapping && !this._changeListenerAttached) {
3316
+ this._changeListenerAttached = true;
3317
+ this.root.on('change', () => {
3318
+ this._computeAndSetOverrides();
3319
+ });
3320
+ // Compute initial value
3321
+ this._computeAndSetOverrides();
3322
+ }
3323
+ return result;
3324
+ }
3325
+ _computeAndSetOverrides() {
3326
+ if (this._debounceTimer) {
3327
+ clearTimeout(this._debounceTimer);
3328
+ }
3329
+ this._debounceTimer = setTimeout(() => {
3330
+ const mapping = this.component?.overrideMapping;
3331
+ const formData = this.root?.data;
3332
+ if (mapping && formData) {
3333
+ const overrides = computeInputOverrides(mapping, formData);
3334
+ if (Object.keys(overrides).length > 0) {
3335
+ this.setValue(overrides);
3336
+ }
3337
+ }
3338
+ }, 1500);
3339
+ }
3340
+ }
3341
+ // Re-register with the extended class
3342
+ Formio.Components.setComponent(EPISTOLA_DOCUMENT_PREVIEW_OPTIONS.type, PreviewWithOverrides);
3343
+ }
3344
+
3345
+ const FORM_REF_PREFIX = 'form:';
3346
+ class EpistolaOverrideBuilderComponent {
3347
+ cdr;
3348
+ value;
3349
+ valueChange = new EventEmitter();
3350
+ disabled = false;
3351
+ label = 'Input Overrides';
3352
+ availableFields = [];
3353
+ rows = [];
3354
+ advancedMode = false;
3355
+ jsonText = '';
3356
+ jsonError = null;
3357
+ initialized = false;
3358
+ constructor(cdr) {
3359
+ this.cdr = cdr;
3360
+ }
3361
+ ngOnChanges() {
3362
+ if (!this.initialized && this.value) {
3363
+ this.initialized = true;
3364
+ this.rows = this.mappingToRows(this.value);
3365
+ this.jsonText = JSON.stringify(this.value, null, 2);
3366
+ }
3367
+ this.cdr.markForCheck();
3368
+ }
3369
+ toggleMode() {
3370
+ this.advancedMode = !this.advancedMode;
3371
+ if (this.advancedMode) {
3372
+ const mapping = this.rowsToMapping();
3373
+ this.jsonText = Object.keys(mapping).length > 0 ? JSON.stringify(mapping, null, 2) : '';
3374
+ this.jsonError = null;
3375
+ }
3376
+ else {
3377
+ try {
3378
+ const parsed = this.jsonText.trim() ? JSON.parse(this.jsonText) : {};
3379
+ this.rows = this.mappingToRows(parsed);
3380
+ this.jsonError = null;
3381
+ }
3382
+ catch {
3383
+ // Keep current rows if JSON is invalid
3384
+ }
3385
+ }
3386
+ }
3387
+ addRow() {
3388
+ this.rows.push({ scope: 'doc', inputPath: '', formFieldKey: '' });
3389
+ }
3390
+ removeRow(index) {
3391
+ this.rows.splice(index, 1);
3392
+ this.emitChange();
3393
+ }
3394
+ emitChange() {
3395
+ const mapping = this.rowsToMapping();
3396
+ this.value = Object.keys(mapping).length > 0 ? mapping : null;
3397
+ this.valueChange.emit(this.value);
3398
+ }
3399
+ onJsonChange(text) {
3400
+ this.jsonText = text;
3401
+ if (!text.trim()) {
3402
+ this.jsonError = null;
3403
+ this.value = null;
3404
+ this.valueChange.emit(null);
3405
+ return;
3406
+ }
3407
+ try {
3408
+ const parsed = JSON.parse(text);
3409
+ this.jsonError = null;
3410
+ this.value = parsed;
3411
+ this.valueChange.emit(parsed);
3412
+ }
3413
+ catch (e) {
3414
+ this.jsonError = 'Invalid JSON';
3415
+ }
3416
+ }
3417
+ rowsToMapping() {
3418
+ const mapping = {};
3419
+ for (const row of this.rows) {
3420
+ if (row.inputPath && row.formFieldKey) {
3421
+ if (!mapping[row.scope]) {
3422
+ mapping[row.scope] = {};
3423
+ }
3424
+ mapping[row.scope][row.inputPath] = FORM_REF_PREFIX + row.formFieldKey;
3425
+ }
3426
+ }
3427
+ return mapping;
3428
+ }
3429
+ mappingToRows(mapping) {
3430
+ const rows = [];
3431
+ for (const [scope, fields] of Object.entries(mapping)) {
3432
+ if (scope === 'doc' || scope === 'pv') {
3433
+ for (const [path, ref] of Object.entries(fields)) {
3434
+ const formFieldKey = String(ref).startsWith(FORM_REF_PREFIX)
3435
+ ? String(ref).substring(FORM_REF_PREFIX.length)
3436
+ : String(ref);
3437
+ rows.push({ scope, inputPath: path, formFieldKey });
3438
+ }
3439
+ }
3440
+ }
3441
+ return rows;
3442
+ }
3443
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaOverrideBuilderComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
3444
+ 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: `
3445
+ <div class="override-builder">
3446
+ <div class="builder-header">
3447
+ <span class="builder-label">{{ label || 'Input Overrides' }}</span>
3448
+ <button type="button" class="mode-toggle" (click)="toggleMode()">
3449
+ {{ advancedMode ? 'Simple' : 'Advanced' }}
3450
+ </button>
3451
+ </div>
3452
+
3453
+ <!-- Simple mode: table -->
3454
+ <div *ngIf="!advancedMode" class="builder-table">
3455
+ <div *ngIf="rows.length > 0" class="table-header">
3456
+ <span class="col-scope">Scope</span>
3457
+ <span class="col-path">Input Path</span>
3458
+ <span class="col-field">Form Field</span>
3459
+ <span class="col-action"></span>
3460
+ </div>
3461
+ <div *ngFor="let row of rows; let i = index" class="table-row">
3462
+ <select class="col-scope" [(ngModel)]="row.scope" (ngModelChange)="emitChange()">
3463
+ <option value="doc">doc</option>
3464
+ <option value="pv">pv</option>
3465
+ </select>
3466
+ <input
3467
+ class="col-path"
3468
+ type="text"
3469
+ [(ngModel)]="row.inputPath"
3470
+ (ngModelChange)="emitChange()"
3471
+ placeholder="e.g. beslissing.tekst"
3472
+ />
3473
+ <!-- Dropdown when form fields are available, text input as fallback -->
3474
+ <select
3475
+ *ngIf="availableFields.length > 0"
3476
+ class="col-field"
3477
+ [(ngModel)]="row.formFieldKey"
3478
+ (ngModelChange)="emitChange()"
3479
+ >
3480
+ <option value="">-- Select field --</option>
3481
+ <option *ngFor="let field of availableFields" [value]="field.key">
3482
+ {{ field.label }}
3483
+ </option>
3484
+ </select>
3485
+ <input
3486
+ *ngIf="availableFields.length === 0"
3487
+ class="col-field"
3488
+ type="text"
3489
+ [(ngModel)]="row.formFieldKey"
3490
+ (ngModelChange)="emitChange()"
3491
+ placeholder="form field key"
3492
+ />
3493
+ <button type="button" class="col-action remove-btn" (click)="removeRow(i)">
3494
+ <i class="mdi mdi-close"></i>
3495
+ </button>
3496
+ </div>
3497
+ <button type="button" class="add-btn" (click)="addRow()">
3498
+ <i class="mdi mdi-plus mr-1"></i> Add override
3499
+ </button>
3500
+ </div>
3501
+
3502
+ <!-- Advanced mode: JSON editor -->
3503
+ <div *ngIf="advancedMode" class="builder-advanced">
3504
+ <textarea
3505
+ class="json-editor"
3506
+ [ngModel]="jsonText"
3507
+ (ngModelChange)="onJsonChange($event)"
3508
+ placeholder='{ "pv": { "motivation": "form:pv:motivation" } }'
3509
+ rows="6"
3510
+ ></textarea>
3511
+ <div *ngIf="jsonError" class="json-error">{{ jsonError }}</div>
3512
+ </div>
3513
+ </div>
3514
+ `, 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 });
3515
+ }
3516
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaOverrideBuilderComponent, decorators: [{
3517
+ type: Component,
3518
+ args: [{ standalone: true, imports: [CommonModule, FormsModule], selector: 'epistola-override-builder-component', changeDetection: ChangeDetectionStrategy.OnPush, template: `
3519
+ <div class="override-builder">
3520
+ <div class="builder-header">
3521
+ <span class="builder-label">{{ label || 'Input Overrides' }}</span>
3522
+ <button type="button" class="mode-toggle" (click)="toggleMode()">
3523
+ {{ advancedMode ? 'Simple' : 'Advanced' }}
3524
+ </button>
3525
+ </div>
3526
+
3527
+ <!-- Simple mode: table -->
3528
+ <div *ngIf="!advancedMode" class="builder-table">
3529
+ <div *ngIf="rows.length > 0" class="table-header">
3530
+ <span class="col-scope">Scope</span>
3531
+ <span class="col-path">Input Path</span>
3532
+ <span class="col-field">Form Field</span>
3533
+ <span class="col-action"></span>
3534
+ </div>
3535
+ <div *ngFor="let row of rows; let i = index" class="table-row">
3536
+ <select class="col-scope" [(ngModel)]="row.scope" (ngModelChange)="emitChange()">
3537
+ <option value="doc">doc</option>
3538
+ <option value="pv">pv</option>
3539
+ </select>
3540
+ <input
3541
+ class="col-path"
3542
+ type="text"
3543
+ [(ngModel)]="row.inputPath"
3544
+ (ngModelChange)="emitChange()"
3545
+ placeholder="e.g. beslissing.tekst"
3546
+ />
3547
+ <!-- Dropdown when form fields are available, text input as fallback -->
3548
+ <select
3549
+ *ngIf="availableFields.length > 0"
3550
+ class="col-field"
3551
+ [(ngModel)]="row.formFieldKey"
3552
+ (ngModelChange)="emitChange()"
3553
+ >
3554
+ <option value="">-- Select field --</option>
3555
+ <option *ngFor="let field of availableFields" [value]="field.key">
3556
+ {{ field.label }}
3557
+ </option>
3558
+ </select>
3559
+ <input
3560
+ *ngIf="availableFields.length === 0"
3561
+ class="col-field"
3562
+ type="text"
3563
+ [(ngModel)]="row.formFieldKey"
3564
+ (ngModelChange)="emitChange()"
3565
+ placeholder="form field key"
3566
+ />
3567
+ <button type="button" class="col-action remove-btn" (click)="removeRow(i)">
3568
+ <i class="mdi mdi-close"></i>
3569
+ </button>
3570
+ </div>
3571
+ <button type="button" class="add-btn" (click)="addRow()">
3572
+ <i class="mdi mdi-plus mr-1"></i> Add override
3573
+ </button>
3574
+ </div>
3575
+
3576
+ <!-- Advanced mode: JSON editor -->
3577
+ <div *ngIf="advancedMode" class="builder-advanced">
3578
+ <textarea
3579
+ class="json-editor"
3580
+ [ngModel]="jsonText"
3581
+ (ngModelChange)="onJsonChange($event)"
3582
+ placeholder='{ "pv": { "motivation": "form:pv:motivation" } }'
3583
+ rows="6"
3584
+ ></textarea>
3585
+ <div *ngIf="jsonError" class="json-error">{{ jsonError }}</div>
3586
+ </div>
3587
+ </div>
3588
+ `, 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"] }]
3589
+ }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { value: [{
3590
+ type: Input
3591
+ }], valueChange: [{
3592
+ type: Output
3593
+ }], disabled: [{
3594
+ type: Input
3595
+ }], label: [{
3596
+ type: Input
3597
+ }], availableFields: [{
3598
+ type: Input
3599
+ }] } });
3600
+
3601
+ const EPISTOLA_OVERRIDE_BUILDER_OPTIONS = {
3602
+ type: 'epistola-override-builder',
3603
+ selector: 'epistola-override-builder-element',
3604
+ title: 'Epistola Override Builder',
3605
+ group: 'basic',
3606
+ icon: 'list',
3607
+ emptyValue: null,
3608
+ fieldOptions: ['label', 'availableFields'],
3609
+ };
3610
+ /**
3611
+ * Recursively collect input field keys and labels from a Formio component tree.
3612
+ * Skips epistola custom components (which are builder UI, not form fields).
3613
+ */
3614
+ function collectFormFields(components) {
3615
+ const fields = [];
3616
+ for (const comp of components) {
3617
+ if (comp.input && comp.key && comp.type !== 'button' && !comp.type?.startsWith('epistola-')) {
3618
+ fields.push({ key: comp.key, label: comp.label || comp.key });
3619
+ }
3620
+ if (comp.components) {
3621
+ fields.push(...collectFormFields(comp.components));
3622
+ }
3623
+ if (comp.columns) {
3624
+ for (const col of comp.columns) {
3625
+ if (col.components) {
3626
+ fields.push(...collectFormFields(col.components));
3627
+ }
3628
+ }
3629
+ }
3630
+ }
3631
+ return fields;
3632
+ }
3633
+ function registerEpistolaOverrideBuilderComponent(injector) {
3634
+ if (customElements.get(EPISTOLA_OVERRIDE_BUILDER_OPTIONS.selector)) {
3635
+ return;
3636
+ }
3637
+ // Register the base component (Angular element + Formio component class)
3638
+ registerCustomFormioComponent(EPISTOLA_OVERRIDE_BUILDER_OPTIONS, EpistolaOverrideBuilderComponent, injector);
3639
+ // Get the Formio Components registry and the registered base class
3640
+ const Formio = window.Formio;
3641
+ if (!Formio?.Components)
3642
+ return;
3643
+ const BaseComponent = Formio.Components.components[EPISTOLA_OVERRIDE_BUILDER_OPTIONS.type];
3644
+ if (!BaseComponent)
3645
+ return;
3646
+ // Extend the base class to pass available form fields to the Angular component
3647
+ class OverrideBuilderWithFields extends BaseComponent {
3648
+ attach(element) {
3649
+ // Set form fields on the component BEFORE super.attach() reads fieldOptions
3650
+ this.component.availableFields = this._extractFormFields();
3651
+ return super.attach(element);
3652
+ }
3653
+ _extractFormFields() {
3654
+ // The Formio builder passes the main form schema as options.editForm
3655
+ // when opening the edit dialog (editFormOptions.editForm = this.form).
3656
+ const components = this.options?.editForm?.components;
3657
+ if (Array.isArray(components)) {
3658
+ return collectFormFields(components);
3659
+ }
3660
+ return [];
3661
+ }
3662
+ }
3663
+ // Re-register with the extended class
3664
+ Formio.Components.setComponent(EPISTOLA_OVERRIDE_BUILDER_OPTIONS.type, OverrideBuilderWithFields);
3665
+ }
3666
+
3667
+ class EpistolaProcessLinkSelectorComponent {
3668
+ adminService;
3669
+ cdr;
3670
+ value;
3671
+ valueChange = new EventEmitter();
3672
+ disabled = false;
3673
+ label = 'Process Link';
3674
+ filteredEntries = [];
3675
+ selectedKey = '';
3676
+ loading = false;
3677
+ error = null;
3678
+ initialized = false;
3679
+ loadSubscription;
3680
+ constructor(adminService, cdr) {
3681
+ this.adminService = adminService;
3682
+ this.cdr = cdr;
3683
+ }
3684
+ ngOnChanges(changes) {
3685
+ if (!this.initialized) {
3686
+ this.initialized = true;
3687
+ this.loadEntries();
3688
+ }
3689
+ // Restore selection whenever value changes (Formio may set it after init)
3690
+ if (changes['value'] && this.value) {
3691
+ this.selectedKey = `${this.value.processDefinitionKey}::${this.value.sourceActivityId}`;
3692
+ this.cdr.markForCheck();
3693
+ }
3694
+ }
3695
+ ngOnDestroy() {
3696
+ this.loadSubscription?.unsubscribe();
3697
+ }
3698
+ onSelect(key) {
3699
+ this.selectedKey = key;
3700
+ if (!key) {
3701
+ this.value = null;
3702
+ this.valueChange.emit(null);
3703
+ return;
3704
+ }
3705
+ const [processDefinitionKey, sourceActivityId] = key.split('::');
3706
+ this.value = { processDefinitionKey, sourceActivityId };
3707
+ this.valueChange.emit(this.value);
3708
+ }
3709
+ entryKey(entry) {
3710
+ return `${entry.processDefinitionKey}::${entry.activityId}`;
3711
+ }
3712
+ loadEntries() {
3713
+ this.loading = true;
3714
+ this.cdr.markForCheck();
3715
+ this.loadSubscription = this.adminService.getPluginUsage().subscribe({
3716
+ next: (entries) => {
3717
+ this.filteredEntries = entries.filter((e) => e.actionKey === 'generate-document');
3718
+ this.loading = false;
3719
+ // Restore selection from value
3720
+ if (this.value) {
3721
+ this.selectedKey = `${this.value.processDefinitionKey}::${this.value.sourceActivityId}`;
3722
+ }
3723
+ this.cdr.markForCheck();
3724
+ },
3725
+ error: (err) => {
3726
+ this.error = 'Failed to load process links';
3727
+ this.loading = false;
3728
+ this.cdr.markForCheck();
3729
+ },
3730
+ });
3731
+ }
3732
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaProcessLinkSelectorComponent, deps: [{ token: EpistolaAdminService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
3733
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaProcessLinkSelectorComponent, isStandalone: true, selector: "epistola-process-link-selector-component", inputs: { value: "value", disabled: "disabled", label: "label" }, outputs: { valueChange: "valueChange" }, usesOnChanges: true, ngImport: i0, template: `
3734
+ <div class="process-link-selector">
3735
+ <label class="selector-label">{{ label || 'Process Link' }}</label>
3736
+ <select
3737
+ class="selector-dropdown"
3738
+ [ngModel]="selectedKey"
3739
+ (ngModelChange)="onSelect($event)"
3740
+ [disabled]="disabled || loading"
3741
+ >
3742
+ <option value="">{{ loading ? 'Loading...' : '-- Select a process link --' }}</option>
3743
+ <option *ngFor="let entry of filteredEntries" [value]="entryKey(entry)">
3744
+ {{ entry.processDefinitionName }} / {{ entry.activityName }} ({{ entry.activityId }})
3745
+ </option>
3746
+ </select>
3747
+ <div *ngIf="error" class="selector-error">{{ error }}</div>
3748
+ </div>
3749
+ `, 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 });
3750
+ }
3751
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaProcessLinkSelectorComponent, decorators: [{
3752
+ type: Component,
3753
+ args: [{ standalone: true, imports: [CommonModule, FormsModule], selector: 'epistola-process-link-selector-component', changeDetection: ChangeDetectionStrategy.OnPush, template: `
3754
+ <div class="process-link-selector">
3755
+ <label class="selector-label">{{ label || 'Process Link' }}</label>
3756
+ <select
3757
+ class="selector-dropdown"
3758
+ [ngModel]="selectedKey"
3759
+ (ngModelChange)="onSelect($event)"
3760
+ [disabled]="disabled || loading"
3761
+ >
3762
+ <option value="">{{ loading ? 'Loading...' : '-- Select a process link --' }}</option>
3763
+ <option *ngFor="let entry of filteredEntries" [value]="entryKey(entry)">
3764
+ {{ entry.processDefinitionName }} / {{ entry.activityName }} ({{ entry.activityId }})
3765
+ </option>
3766
+ </select>
3767
+ <div *ngIf="error" class="selector-error">{{ error }}</div>
3768
+ </div>
3769
+ `, 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"] }]
3770
+ }], ctorParameters: () => [{ type: EpistolaAdminService }, { type: i0.ChangeDetectorRef }], propDecorators: { value: [{
3771
+ type: Input
3772
+ }], valueChange: [{
3773
+ type: Output
3774
+ }], disabled: [{
3775
+ type: Input
3776
+ }], label: [{
3777
+ type: Input
3778
+ }] } });
3779
+
3780
+ const EPISTOLA_PROCESS_LINK_SELECTOR_OPTIONS = {
3781
+ type: 'epistola-process-link-selector',
3782
+ selector: 'epistola-process-link-selector-element',
3783
+ title: 'Epistola Process Link Selector',
3784
+ group: 'basic',
3785
+ icon: 'link',
3786
+ emptyValue: null,
3787
+ fieldOptions: ['label'],
3788
+ };
3789
+ function registerEpistolaProcessLinkSelectorComponent(injector) {
3790
+ if (!customElements.get(EPISTOLA_PROCESS_LINK_SELECTOR_OPTIONS.selector)) {
3791
+ registerCustomFormioComponent(EPISTOLA_PROCESS_LINK_SELECTOR_OPTIONS, EpistolaProcessLinkSelectorComponent, injector);
1885
3792
  }
1886
3793
  }
1887
3794
 
@@ -1890,18 +3797,25 @@ class EpistolaPluginModule {
1890
3797
  return {
1891
3798
  ngModule: EpistolaPluginModule,
1892
3799
  providers: [
3800
+ EpistolaMenuService,
1893
3801
  {
1894
3802
  provide: ENVIRONMENT_INITIALIZER,
1895
3803
  multi: true,
1896
3804
  useValue: () => {
3805
+ if (!isEpistolaEnabled())
3806
+ return;
1897
3807
  const injector = inject(Injector);
1898
3808
  registerEpistolaDownloadComponent(injector);
1899
3809
  registerEpistolaRetryFormComponent(injector);
1900
3810
  registerEpistolaPreviewButtonComponent(injector);
3811
+ registerEpistolaOverrideBuilderComponent(injector);
3812
+ registerEpistolaProcessLinkSelectorComponent(injector);
1901
3813
  registerEpistolaDocumentPreviewComponent(injector);
1902
- }
1903
- }
1904
- ]
3814
+ // Eagerly create EpistolaMenuService to trigger menu registration
3815
+ inject(EpistolaMenuService);
3816
+ },
3817
+ },
3818
+ ],
1905
3819
  };
1906
3820
  }
1907
3821
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaPluginModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
@@ -1911,52 +3825,40 @@ class EpistolaPluginModule {
1911
3825
  FormModule,
1912
3826
  InputModule,
1913
3827
  SelectModule,
3828
+ EpistolaAdminRoutingModule,
1914
3829
  EpistolaConfigurationComponent,
1915
3830
  GenerateDocumentConfigurationComponent,
1916
3831
  CheckJobStatusConfigurationComponent,
1917
3832
  DownloadDocumentConfigurationComponent,
1918
- DataMappingTreeComponent,
1919
- ValueInputComponent,
1920
- ScalarFieldComponent,
1921
- ArrayFieldComponent,
1922
- FieldTreeComponent,
1923
3833
  EpistolaDownloadComponent,
1924
3834
  EpistolaRetryFormComponent,
1925
3835
  EpistolaPreviewButtonComponent,
1926
- EpistolaDocumentPreviewComponent], exports: [EpistolaConfigurationComponent,
3836
+ EpistolaDocumentPreviewComponent,
3837
+ EpistolaAdminPageComponent], exports: [EpistolaConfigurationComponent,
1927
3838
  GenerateDocumentConfigurationComponent,
1928
3839
  CheckJobStatusConfigurationComponent,
1929
3840
  DownloadDocumentConfigurationComponent,
1930
- DataMappingTreeComponent,
1931
- ValueInputComponent,
1932
- ScalarFieldComponent,
1933
- ArrayFieldComponent,
1934
- FieldTreeComponent,
1935
3841
  EpistolaDownloadComponent,
1936
3842
  EpistolaRetryFormComponent,
1937
3843
  EpistolaPreviewButtonComponent,
1938
- EpistolaDocumentPreviewComponent] });
1939
- static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaPluginModule, providers: [
1940
- EpistolaPluginService
1941
- ], imports: [CommonModule,
3844
+ EpistolaDocumentPreviewComponent,
3845
+ EpistolaAdminPageComponent] });
3846
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaPluginModule, providers: [EpistolaPluginService, EpistolaAdminService], imports: [CommonModule,
1942
3847
  HttpClientModule,
1943
3848
  PluginTranslatePipeModule,
1944
3849
  FormModule,
1945
3850
  InputModule,
1946
3851
  SelectModule,
3852
+ EpistolaAdminRoutingModule,
1947
3853
  EpistolaConfigurationComponent,
1948
3854
  GenerateDocumentConfigurationComponent,
1949
3855
  CheckJobStatusConfigurationComponent,
1950
3856
  DownloadDocumentConfigurationComponent,
1951
- DataMappingTreeComponent,
1952
- ValueInputComponent,
1953
- ScalarFieldComponent,
1954
- ArrayFieldComponent,
1955
- FieldTreeComponent,
1956
3857
  EpistolaDownloadComponent,
1957
3858
  EpistolaRetryFormComponent,
1958
3859
  EpistolaPreviewButtonComponent,
1959
- EpistolaDocumentPreviewComponent] });
3860
+ EpistolaDocumentPreviewComponent,
3861
+ EpistolaAdminPageComponent] });
1960
3862
  }
1961
3863
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaPluginModule, decorators: [{
1962
3864
  type: NgModule,
@@ -1968,48 +3870,40 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
1968
3870
  FormModule,
1969
3871
  InputModule,
1970
3872
  SelectModule,
3873
+ EpistolaAdminRoutingModule,
1971
3874
  EpistolaConfigurationComponent,
1972
3875
  GenerateDocumentConfigurationComponent,
1973
3876
  CheckJobStatusConfigurationComponent,
1974
3877
  DownloadDocumentConfigurationComponent,
1975
- DataMappingTreeComponent,
1976
- ValueInputComponent,
1977
- ScalarFieldComponent,
1978
- ArrayFieldComponent,
1979
- FieldTreeComponent,
1980
3878
  EpistolaDownloadComponent,
1981
3879
  EpistolaRetryFormComponent,
1982
3880
  EpistolaPreviewButtonComponent,
1983
- EpistolaDocumentPreviewComponent
3881
+ EpistolaDocumentPreviewComponent,
3882
+ EpistolaAdminPageComponent,
1984
3883
  ],
1985
3884
  exports: [
1986
3885
  EpistolaConfigurationComponent,
1987
3886
  GenerateDocumentConfigurationComponent,
1988
3887
  CheckJobStatusConfigurationComponent,
1989
3888
  DownloadDocumentConfigurationComponent,
1990
- DataMappingTreeComponent,
1991
- ValueInputComponent,
1992
- ScalarFieldComponent,
1993
- ArrayFieldComponent,
1994
- FieldTreeComponent,
1995
3889
  EpistolaDownloadComponent,
1996
3890
  EpistolaRetryFormComponent,
1997
3891
  EpistolaPreviewButtonComponent,
1998
- EpistolaDocumentPreviewComponent
3892
+ EpistolaDocumentPreviewComponent,
3893
+ EpistolaAdminPageComponent,
1999
3894
  ],
2000
- providers: [
2001
- EpistolaPluginService
2002
- ]
3895
+ providers: [EpistolaPluginService, EpistolaAdminService],
2003
3896
  }]
2004
3897
  }] });
2005
3898
 
2006
- // Placeholder logo - a simple document icon in SVG format, base64 encoded
2007
- // TODO: Replace with actual Epistola logo
2008
- const EPISTOLA_PLUGIN_LOGO_BASE64 = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iIzMzNjZjYyI+PHBhdGggZD0iTTE0IDJINmMtMS4xIDAtMiAuOS0yIDJ2MTZjMCAxLjEuOSAyIDIgMmgxMmMxLjEgMCAyLS45IDItMlY4bC02LTZ6bTQgMThINlY0aDd2NWg1djExeiIvPjxwYXRoIGQ9Ik04IDEyaDh2Mkg4em0wIDRoOHYtMkg4em0wLThWNmg0djJ6Ii8+PC9zdmc+';
3899
+ const EPISTOLA_PLUGIN_LOGO_BASE64 = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAgMTIwIiB3aWR0aD0iMTIwIiBoZWlnaHQ9IjEyMCI+CiAgPCEtLSBTdGFjayBiYXNlIC0tPgogIDxyZWN0IHg9IjM2IiB5PSIxNiIgd2lkdGg9IjU0IiBoZWlnaHQ9IjcwIiByeD0iMyIgZmlsbD0iI2U2YzJiMCIgc3Ryb2tlPSIjNGYyZjJiIiBzdHJva2Utd2lkdGg9IjIiIHRyYW5zZm9ybT0icm90YXRlKDUgNjMgNTEpIi8+CiAgPHJlY3QgeD0iMzIiIHk9IjIyIiB3aWR0aD0iNTQiIGhlaWdodD0iNzAiIHJ4PSIzIiBmaWxsPSIjZjBkOGM4IiBzdHJva2U9IiM0ZjJmMmIiIHN0cm9rZS13aWR0aD0iMiIvPgogIDxyZWN0IHg9IjI4IiB5PSIyOCIgd2lkdGg9IjU0IiBoZWlnaHQ9IjcwIiByeD0iMyIgZmlsbD0iI2Y1ZWJlMyIgc3Ryb2tlPSIjNGYyZjJiIiBzdHJva2Utd2lkdGg9IjIuNSIvPgogIDxsaW5lIHgxPSIzOCIgeTE9IjQ0IiB4Mj0iNzIiIHkyPSI0NCIgc3Ryb2tlPSIjYzRhODgyIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgogIDxsaW5lIHgxPSIzOCIgeTE9IjU0IiB4Mj0iNzIiIHkyPSI1NCIgc3Ryb2tlPSIjYzRhODgyIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgogIDxsaW5lIHgxPSIzOCIgeTE9IjY0IiB4Mj0iNTgiIHkyPSI2NCIgc3Ryb2tlPSIjYzRhODgyIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgogIDwhLS0gV2F4IHNlYWwgd2l0aCBjaGVja21hcmsgLS0+CiAgPGNpcmNsZSBjeD0iNTUiIGN5PSI4NCIgcj0iMTUiIGZpbGw9IiNiODVjM2MiIHN0cm9rZT0iIzRmMmYyYiIgc3Ryb2tlLXdpZHRoPSIyIi8+CiAgPGNpcmNsZSBjeD0iNTUiIGN5PSI4NCIgcj0iMTAuNSIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZDQ4MzZhIiBzdHJva2Utd2lkdGg9IjEiLz4KICA8cG9seWxpbmUgcG9pbnRzPSI0OSw4NCA1Myw4OSA2Miw3OCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNGYyZjJiIiBzdHJva2Utd2lkdGg9IjIuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo=';
2009
3900
 
3901
+ const EPISTOLA_PLUGIN_ID = 'epistola';
3902
+ const DISABLED_EPISTOLA_PLUGIN_ID = '__epistola_disabled__';
2010
3903
  const epistolaPluginSpecification = {
2011
- // Must match backend @Plugin(key = "epistola")
2012
- pluginId: 'epistola',
3904
+ get pluginId() {
3905
+ return isEpistolaEnabled() ? EPISTOLA_PLUGIN_ID : DISABLED_EPISTOLA_PLUGIN_ID;
3906
+ },
2013
3907
  // Component for plugin-level configuration (tenantId)
2014
3908
  pluginConfigurationComponent: EpistolaConfigurationComponent,
2015
3909
  // Plugin logo
@@ -2073,6 +3967,18 @@ const epistolaPluginSpecification = {
2073
3967
  // Data mapping builder translations
2074
3968
  dataMappingTitle: 'Data Mapping',
2075
3969
  dataMappingDescription: 'Koppel template velden aan Valtimo data bronnen',
3970
+ jsonataDescription: 'JSONata expressie die de template data genereert. Gebruik $doc, $pv en $case voor toegang tot document-, procesvariabelen- en zaakdata.',
3971
+ mappingModeSimple: 'Eenvoudig',
3972
+ mappingModeAdvanced: 'Geavanceerd',
3973
+ mappingTools: 'Schema & Voorbeeld',
3974
+ expectedStructure: 'Verwacht schema',
3975
+ expectedStructureLoading: 'Schema laden...',
3976
+ previewTitle: 'Voorbeeld',
3977
+ previewDocPlaceholder: 'Document ID',
3978
+ previewExpected: 'Verwacht',
3979
+ previewProduced: 'Geproduceerd',
3980
+ previewRunHint: 'Voer een document ID in en klik ▶ om een voorbeeld te zien',
3981
+ previewMissing: 'Ontbrekende verplichte velden',
2076
3982
  templateField: 'Template veld',
2077
3983
  dataSource: 'Data bron',
2078
3984
  addMapping: 'Mapping toevoegen',
@@ -2087,6 +3993,7 @@ const epistolaPluginSpecification = {
2087
3993
  requiredFieldsMissing: 'Niet alle verplichte velden zijn gekoppeld',
2088
3994
  requiredFieldsComplete: 'Alle verplichte velden zijn gekoppeld',
2089
3995
  validationSummary: 'verplichte velden gekoppeld',
3996
+ jsonataValidationErrorsHeading: 'Ongeldige JSONata-expressies:',
2090
3997
  fieldRequired: 'Verplicht',
2091
3998
  fieldOptional: 'Optioneel',
2092
3999
  mapCollectionTo: 'Koppel collectie aan',
@@ -2094,6 +4001,7 @@ const epistolaPluginSpecification = {
2094
4001
  pvMode: 'Procesvariabele modus',
2095
4002
  pvPlaceholder: 'Naam procesvariabele',
2096
4003
  expressionMode: 'Expressiemodus',
4004
+ availableFunctions: 'Beschikbare functies',
2097
4005
  itemFieldMapping: 'Veldnamen per item koppelen',
2098
4006
  itemFieldMappingTitle: 'Veldkoppeling per item:',
2099
4007
  sourceFieldPlaceholder: 'Bronveldnaam',
@@ -2111,7 +4019,31 @@ const epistolaPluginSpecification = {
2111
4019
  // Download document action
2112
4020
  'download-document': 'Download Document',
2113
4021
  contentVariable: 'Inhoud Variabele',
2114
- contentVariableTooltip: 'Naam van de procesvariabele waarin de documentinhoud (Base64) wordt opgeslagen'
4022
+ contentVariableTooltip: 'Naam van de procesvariabele waarin de documentinhoud (Base64) wordt opgeslagen',
4023
+ // Admin page
4024
+ epistolaAdminOverview: 'Overzicht',
4025
+ epistolaAdminRefresh: 'Vernieuwen',
4026
+ epistolaAdminLoading: 'Laden...',
4027
+ epistolaAdminNoConfigurations: 'Geen Epistola plugin configuraties gevonden.',
4028
+ epistolaAdminTenantId: 'Tenant ID',
4029
+ epistolaAdminStatus: 'Status',
4030
+ epistolaAdminConnected: 'Verbonden',
4031
+ epistolaAdminUnreachable: 'Onbereikbaar',
4032
+ epistolaAdminError: 'Fout',
4033
+ epistolaAdminPluginActions: 'Plugin acties',
4034
+ epistolaAdminProblems: 'Problemen',
4035
+ epistolaAdminBackToOverview: 'Terug naar overzicht',
4036
+ epistolaAdminNoUsageForConfig: 'Geen procesacties geconfigureerd voor deze verbinding.',
4037
+ epistolaAdminCase: 'Zaak',
4038
+ epistolaAdminProcess: 'Proces',
4039
+ epistolaAdminActivity: 'Activiteit',
4040
+ epistolaAdminAction: 'Actie',
4041
+ epistolaAdminServerVersion: 'Server versie',
4042
+ epistolaAdminExport: 'Exporteren',
4043
+ epistolaAdminPendingJobs: 'Wachtende taken',
4044
+ epistolaAdminNoPendingJobs: 'Geen wachtende taken voor deze verbinding.',
4045
+ epistolaAdminConfiguration: 'Configuratie',
4046
+ epistolaAdminRequestId: 'Request ID',
2115
4047
  },
2116
4048
  en: {
2117
4049
  title: 'Epistola Document Suite',
@@ -2164,6 +4096,18 @@ const epistolaPluginSpecification = {
2164
4096
  // Data mapping builder translations
2165
4097
  dataMappingTitle: 'Data Mapping',
2166
4098
  dataMappingDescription: 'Map template fields to Valtimo data sources',
4099
+ mappingModeSimple: 'Simple',
4100
+ mappingModeAdvanced: 'Advanced',
4101
+ mappingTools: 'Schema & Preview',
4102
+ expectedStructure: 'Expected schema',
4103
+ expectedStructureLoading: 'Loading schema...',
4104
+ previewTitle: 'Preview',
4105
+ previewDocPlaceholder: 'Document ID',
4106
+ previewExpected: 'Expected',
4107
+ previewProduced: 'Produced',
4108
+ previewRunHint: 'Enter a document ID and click ▶ to preview the output',
4109
+ previewMissing: 'Missing required fields',
4110
+ jsonataDescription: 'JSONata expression that generates the template data. Use $doc, $pv and $case to access document, process variable and case data.',
2167
4111
  templateField: 'Template field',
2168
4112
  dataSource: 'Data source',
2169
4113
  addMapping: 'Add mapping',
@@ -2178,6 +4122,7 @@ const epistolaPluginSpecification = {
2178
4122
  requiredFieldsMissing: 'Not all required fields are mapped',
2179
4123
  requiredFieldsComplete: 'All required fields are mapped',
2180
4124
  validationSummary: 'required fields mapped',
4125
+ jsonataValidationErrorsHeading: 'Invalid JSONata expressions:',
2181
4126
  fieldRequired: 'Required',
2182
4127
  fieldOptional: 'Optional',
2183
4128
  mapCollectionTo: 'Map collection to',
@@ -2185,6 +4130,7 @@ const epistolaPluginSpecification = {
2185
4130
  pvMode: 'Process variable mode',
2186
4131
  pvPlaceholder: 'Process variable name',
2187
4132
  expressionMode: 'Expression mode',
4133
+ availableFunctions: 'Available functions',
2188
4134
  itemFieldMapping: 'Map field names per item',
2189
4135
  itemFieldMappingTitle: 'Item field mapping:',
2190
4136
  sourceFieldPlaceholder: 'Source field name',
@@ -2202,9 +4148,33 @@ const epistolaPluginSpecification = {
2202
4148
  // Download document action
2203
4149
  'download-document': 'Download Document',
2204
4150
  contentVariable: 'Content Variable',
2205
- contentVariableTooltip: 'Name of the process variable to store the document content (Base64) in'
2206
- }
2207
- }
4151
+ contentVariableTooltip: 'Name of the process variable to store the document content (Base64) in',
4152
+ // Admin page
4153
+ epistolaAdminOverview: 'Overview',
4154
+ epistolaAdminRefresh: 'Refresh',
4155
+ epistolaAdminLoading: 'Loading...',
4156
+ epistolaAdminNoConfigurations: 'No Epistola plugin configurations found.',
4157
+ epistolaAdminTenantId: 'Tenant ID',
4158
+ epistolaAdminStatus: 'Status',
4159
+ epistolaAdminConnected: 'Connected',
4160
+ epistolaAdminUnreachable: 'Unreachable',
4161
+ epistolaAdminError: 'Error',
4162
+ epistolaAdminPluginActions: 'Plugin actions',
4163
+ epistolaAdminProblems: 'Problems',
4164
+ epistolaAdminBackToOverview: 'Back to overview',
4165
+ epistolaAdminNoUsageForConfig: 'No process actions configured for this connection.',
4166
+ epistolaAdminCase: 'Case',
4167
+ epistolaAdminProcess: 'Process',
4168
+ epistolaAdminActivity: 'Activity',
4169
+ epistolaAdminAction: 'Action',
4170
+ epistolaAdminServerVersion: 'Server version',
4171
+ epistolaAdminExport: 'Export',
4172
+ epistolaAdminPendingJobs: 'Pending jobs',
4173
+ epistolaAdminNoPendingJobs: 'No pending jobs for this connection.',
4174
+ epistolaAdminConfiguration: 'Configuration',
4175
+ epistolaAdminRequestId: 'Request ID',
4176
+ },
4177
+ },
2208
4178
  };
2209
4179
 
2210
4180
  /*
@@ -2215,5 +4185,5 @@ const epistolaPluginSpecification = {
2215
4185
  * Generated bundle index. Do not edit.
2216
4186
  */
2217
4187
 
2218
- export { ArrayFieldComponent, CheckJobStatusConfigurationComponent, DataMappingTreeComponent, DownloadDocumentConfigurationComponent, EPISTOLA_DOCUMENT_PREVIEW_OPTIONS, EPISTOLA_DOWNLOAD_OPTIONS, EPISTOLA_PREVIEW_BUTTON_OPTIONS, EPISTOLA_RETRY_FORM_OPTIONS, EpistolaConfigurationComponent, EpistolaDocumentPreviewComponent, EpistolaDownloadComponent, EpistolaPluginModule, EpistolaPluginService, EpistolaPreviewButtonComponent, EpistolaRetryFormComponent, FieldTreeComponent, GenerateDocumentConfigurationComponent, ScalarFieldComponent, ValueInputComponent, epistolaPluginSpecification, errorResource, initialResource, loadingResource, normalizeToDots, registerEpistolaDocumentPreviewComponent, registerEpistolaDownloadComponent, registerEpistolaPreviewButtonComponent, registerEpistolaRetryFormComponent, successResource };
4188
+ export { CheckJobStatusConfigurationComponent, DownloadDocumentConfigurationComponent, EPISTOLA_DOCUMENT_PREVIEW_OPTIONS, EPISTOLA_DOWNLOAD_OPTIONS, EPISTOLA_OVERRIDE_BUILDER_OPTIONS, EPISTOLA_PREVIEW_BUTTON_OPTIONS, EPISTOLA_PROCESS_LINK_SELECTOR_OPTIONS, EPISTOLA_RETRY_FORM_OPTIONS, EpistolaAdminPageComponent, EpistolaAdminRoutingModule, EpistolaAdminService, EpistolaConfigurationComponent, EpistolaDocumentPreviewComponent, EpistolaDownloadComponent, EpistolaMenuService, EpistolaOverrideBuilderComponent, EpistolaPluginModule, EpistolaPluginService, EpistolaPreviewButtonComponent, EpistolaProcessLinkSelectorComponent, EpistolaRetryFormComponent, GenerateDocumentConfigurationComponent, JsonataEditorComponent, MappingBuilderComponent, epistolaPluginSpecification, errorResource, initialResource, isEpistolaEnabled, loadingResource, registerEpistolaDocumentPreviewComponent, registerEpistolaDownloadComponent, registerEpistolaOverrideBuilderComponent, registerEpistolaPreviewButtonComponent, registerEpistolaProcessLinkSelectorComponent, registerEpistolaRetryFormComponent, successResource };
2219
4189
  //# sourceMappingURL=epistola.app-valtimo-plugin.mjs.map