@epistola.app/valtimo-plugin 0.3.1 → 0.3.3

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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, EventEmitter, Output, Input, Component, ChangeDetectionStrategy, NgModule } from '@angular/core';
2
+ import { Injectable, EventEmitter, Output, Input, Component, ChangeDetectionStrategy, forwardRef, ENVIRONMENT_INITIALIZER, inject, Injector, NgModule } 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';
@@ -10,7 +10,7 @@ import { PluginTranslatePipeModule } from '@valtimo/plugin';
10
10
  import * as i3 from '@valtimo/components';
11
11
  import { FormModule, InputModule, ValuePathSelectorPrefix, ValuePathSelectorComponent, SelectModule, registerCustomFormioComponent } from '@valtimo/components';
12
12
  import { BehaviorSubject, combineLatest, take, Subject, merge, of } from 'rxjs';
13
- import { startWith, delay, takeUntil, filter, map, catchError, take as take$1, debounceTime } from 'rxjs/operators';
13
+ import { startWith, delay, takeUntil, filter, map, tap, switchMap, catchError, take as take$1, debounceTime } from 'rxjs/operators';
14
14
  import * as i4 from '@angular/forms';
15
15
  import { FormsModule } from '@angular/forms';
16
16
  import * as i2$2 from '@valtimo/process-link';
@@ -18,6 +18,19 @@ import * as i7 from '@formio/angular';
18
18
  import { FormioModule } from '@formio/angular';
19
19
  import * as i4$1 from '@angular/platform-browser';
20
20
 
21
+ function initialResource(empty) {
22
+ return { data: empty, loading: false, error: null };
23
+ }
24
+ function loadingResource(current) {
25
+ return { data: current, loading: true, error: null };
26
+ }
27
+ function successResource(data) {
28
+ return { data, loading: false, error: null };
29
+ }
30
+ function errorResource(current, error) {
31
+ return { data: current, loading: false, error };
32
+ }
33
+
21
34
  /**
22
35
  * Service for interacting with Epistola plugin API endpoints.
23
36
  * Provides methods to fetch templates, environments, variants,
@@ -44,6 +57,12 @@ class EpistolaPluginService {
44
57
  getTemplateDetails(pluginConfigurationId, templateId) {
45
58
  return this.http.get(`${this.apiEndpoint}/configurations/${pluginConfigurationId}/templates/${templateId}`);
46
59
  }
60
+ /**
61
+ * Get all attribute definitions for a plugin configuration's tenant.
62
+ */
63
+ getAttributes(pluginConfigurationId) {
64
+ return this.http.get(`${this.apiEndpoint}/configurations/${pluginConfigurationId}/attributes`);
65
+ }
47
66
  /**
48
67
  * Get all available environments for a plugin configuration.
49
68
  */
@@ -223,149 +242,203 @@ function countRequiredMapped(fields, mapping) {
223
242
  }
224
243
 
225
244
  /**
226
- * Recursive component that renders a single TemplateField node as part of a tree form.
227
- *
228
- * - SCALAR fields render as a label + input row with a 3-mode selector (browse / pv / expression)
229
- * - OBJECT fields render as a collapsible section with children indented inside
230
- * - ARRAY fields render as a collapsible section with source collection input and optional per-item field mappings
231
- *
232
- * Input modes:
233
- * - Browse (⊞): ValuePathSelector for doc:/case: paths
234
- * - PV (pv): Dropdown of discovered process variables (text fallback when none found)
235
- * - Expression (fx): Free-text input for manual expressions
245
+ * Reusable 3-mode input (browse / pv / expression) for value resolver expressions.
246
+ * Used by both ScalarFieldComponent and ArrayFieldComponent for source mapping.
236
247
  */
237
- class FieldTreeComponent {
238
- field;
239
- value = undefined;
240
- pluginId;
248
+ class ValueInputComponent {
249
+ cdr;
250
+ name = '';
251
+ value = '';
252
+ pluginId = '';
241
253
  caseDefinitionKey = null;
242
254
  processVariables = [];
243
255
  disabled = false;
256
+ placeholder = 'e.g. pv:variableName or doc:path.to.field';
244
257
  valueChange = new EventEmitter();
245
258
  ValuePathSelectorPrefix = ValuePathSelectorPrefix;
246
259
  inputMode = 'browse';
247
- expanded = false;
248
- /** For ARRAY fields: whether to show per-item field mapping */
249
- arrayPerFieldMode = false;
250
- /** Completeness badge for collapsed object/array sections */
251
- mappedCount = 0;
252
- totalRequired = 0;
260
+ selectedPv = '';
261
+ browseDefault = '';
262
+ constructor(cdr) {
263
+ this.cdr = cdr;
264
+ }
253
265
  ngOnChanges(changes) {
254
- if (changes['value'] || changes['field']) {
255
- this.updateCompleteness();
256
- // Auto-expand if there are unmapped required children
257
- if (!this.expanded && this.totalRequired > 0 && this.mappedCount < this.totalRequired) {
258
- this.expanded = true;
259
- }
266
+ if (changes['value']) {
267
+ this.inputMode = this.detectInputMode(this.value);
268
+ this.browseDefault = normalizeToDots(this.value);
269
+ this.selectedPv = extractPvName(this.value);
260
270
  }
261
- // Detect input mode from prefill value
262
- if (changes['value'] && this.value != null) {
263
- if (this.field?.fieldType === 'SCALAR') {
264
- this.inputMode = this.detectInputMode(this.value);
265
- }
266
- else if (this.field?.fieldType === 'ARRAY') {
267
- const sourceValue = this.getSourceValue();
268
- if (sourceValue) {
269
- this.inputMode = this.detectInputMode(sourceValue);
270
- }
271
- // Detect per-field mode from value shape
272
- if (typeof this.value === 'object' && this.value !== null && '_source' in this.value) {
273
- this.arrayPerFieldMode = true;
274
- }
275
- }
271
+ if (changes['processVariables']) {
272
+ this.cdr.markForCheck();
276
273
  }
277
274
  }
278
- toggleExpanded() {
279
- this.expanded = !this.expanded;
280
- }
281
275
  setInputMode(mode) {
282
276
  this.inputMode = mode;
283
277
  }
284
- /** Handle value change from ValuePathSelector (browse mode) */
285
278
  onBrowseValueChange(newValue) {
286
- this.emitScalarValue(newValue);
279
+ this.valueChange.emit(normalizeToDots(newValue));
287
280
  }
288
- /** Handle value change from PV dropdown */
289
281
  onPvChange(newValue) {
290
- if (newValue) {
291
- this.emitScalarValue('pv:' + newValue);
292
- }
293
- else {
294
- this.emitScalarValue('');
295
- }
282
+ this.selectedPv = newValue;
283
+ this.valueChange.emit(newValue ? 'pv:' + newValue : '');
296
284
  }
297
- /** Handle value change from text input (expression mode) */
298
285
  onExpressionValueChange(newValue) {
299
- this.emitScalarValue(newValue);
286
+ this.valueChange.emit(newValue);
300
287
  }
301
- /** Handle child value change for OBJECT fields */
302
- onChildChange(childName, childValue) {
303
- const current = (typeof this.value === 'object' && this.value !== null) ? { ...this.value } : {};
304
- if (childValue === undefined || childValue === null || childValue === '') {
305
- delete current[childName];
306
- }
307
- else {
308
- current[childName] = childValue;
309
- }
310
- this.valueChange.emit(Object.keys(current).length > 0 ? current : undefined);
288
+ detectInputMode(value) {
289
+ if (!value)
290
+ return 'browse';
291
+ if (value.startsWith('doc:') || value.startsWith('case:'))
292
+ return 'browse';
293
+ if (value.startsWith('pv:'))
294
+ return 'pv';
295
+ if (value.length > 0)
296
+ return 'expression';
297
+ return 'browse';
311
298
  }
312
- /** Get the current value for a child field within an OBJECT */
313
- getChildValue(childName) {
314
- if (typeof this.value === 'object' && this.value !== null) {
315
- return this.value[childName];
316
- }
317
- return undefined;
299
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ValueInputComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
300
+ 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 });
301
+ }
302
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ValueInputComponent, decorators: [{
303
+ type: Component,
304
+ args: [{ selector: 'epistola-value-input', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [
305
+ CommonModule,
306
+ FormsModule,
307
+ PluginTranslatePipeModule,
308
+ InputModule,
309
+ ValuePathSelectorComponent
310
+ ], 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"] }]
311
+ }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { name: [{
312
+ type: Input
313
+ }], value: [{
314
+ type: Input
315
+ }], pluginId: [{
316
+ type: Input
317
+ }], caseDefinitionKey: [{
318
+ type: Input
319
+ }], processVariables: [{
320
+ type: Input
321
+ }], disabled: [{
322
+ type: Input
323
+ }], placeholder: [{
324
+ type: Input
325
+ }], valueChange: [{
326
+ type: Output
327
+ }] } });
328
+ /** Convert slash-notation paths (e.g. doc:/a/b) to dot notation (doc:a.b). */
329
+ function normalizeToDots(value) {
330
+ if (typeof value !== 'string')
331
+ return value;
332
+ const colonIndex = value.indexOf(':');
333
+ if (colonIndex < 0)
334
+ return value;
335
+ const prefix = value.substring(0, colonIndex);
336
+ const path = value.substring(colonIndex + 1);
337
+ if (!path.includes('/'))
338
+ return value;
339
+ const normalized = path.split('/').filter(p => p.length > 0).join('.');
340
+ return `${prefix}:${normalized}`;
341
+ }
342
+ function extractPvName(value) {
343
+ if (typeof value === 'string' && value.startsWith('pv:')) {
344
+ return value.substring(3);
318
345
  }
319
- /** Get the string value for display (for SCALAR leaf inputs and direct array mode) */
320
- getStringValue() {
346
+ return '';
347
+ }
348
+
349
+ class ScalarFieldComponent {
350
+ field;
351
+ value = undefined;
352
+ pluginId;
353
+ caseDefinitionKey = null;
354
+ processVariables = [];
355
+ disabled = false;
356
+ valueChange = new EventEmitter();
357
+ get stringValue() {
321
358
  return typeof this.value === 'string' ? this.value : '';
322
359
  }
323
- /** Get the PV name from a pv: prefixed value (for PV dropdown default selection) */
324
- getPvName() {
325
- const str = this.getStringValue();
326
- return str.startsWith('pv:') ? str.substring(3) : '';
360
+ onValueChange(newValue) {
361
+ this.valueChange.emit(newValue || undefined);
362
+ }
363
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ScalarFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
364
+ 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 });
365
+ }
366
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ScalarFieldComponent, decorators: [{
367
+ type: Component,
368
+ 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"] }]
369
+ }], propDecorators: { field: [{
370
+ type: Input
371
+ }], value: [{
372
+ type: Input
373
+ }], pluginId: [{
374
+ type: Input
375
+ }], caseDefinitionKey: [{
376
+ type: Input
377
+ }], processVariables: [{
378
+ type: Input
379
+ }], disabled: [{
380
+ type: Input
381
+ }], valueChange: [{
382
+ type: Output
383
+ }] } });
384
+
385
+ class ArrayFieldComponent {
386
+ field;
387
+ value = undefined;
388
+ pluginId;
389
+ caseDefinitionKey = null;
390
+ processVariables = [];
391
+ disabled = false;
392
+ valueChange = new EventEmitter();
393
+ expanded = false;
394
+ arrayPerFieldMode = false;
395
+ mappedCount = 0;
396
+ totalRequired = 0;
397
+ ngOnChanges(changes) {
398
+ if (changes['value'] || changes['field']) {
399
+ this.updateCompleteness();
400
+ if (!this.expanded && this.totalRequired > 0 && this.mappedCount < this.totalRequired) {
401
+ this.expanded = true;
402
+ }
403
+ // Detect per-field mode from value shape
404
+ if (changes['value'] && typeof this.value === 'object' && this.value !== null && '_source' in this.value) {
405
+ this.arrayPerFieldMode = true;
406
+ }
407
+ }
408
+ }
409
+ toggleExpanded() {
410
+ this.expanded = !this.expanded;
327
411
  }
328
- // --- ARRAY-specific methods ---
329
- /** Get the source collection value (works for both direct string and _source format) */
330
412
  getSourceValue() {
331
413
  if (typeof this.value === 'string') {
332
- return this.value;
414
+ return normalizeToDots(this.value);
333
415
  }
334
416
  if (typeof this.value === 'object' && this.value !== null && '_source' in this.value) {
335
- return this.value['_source'] || '';
417
+ return normalizeToDots(this.value['_source'] || '');
336
418
  }
337
419
  return '';
338
420
  }
339
- /** Get the PV name from the source value */
340
- getSourcePvName() {
341
- const source = this.getSourceValue();
342
- return source.startsWith('pv:') ? source.substring(3) : '';
421
+ onSourceValueChange(newValue) {
422
+ if (this.arrayPerFieldMode) {
423
+ const current = (typeof this.value === 'object' && this.value !== null) ? { ...this.value } : {};
424
+ current['_source'] = newValue || '';
425
+ this.valueChange.emit(current);
426
+ }
427
+ else {
428
+ this.valueChange.emit(newValue || undefined);
429
+ }
343
430
  }
344
431
  toggleArrayPerFieldMode() {
345
432
  this.arrayPerFieldMode = !this.arrayPerFieldMode;
346
433
  if (this.arrayPerFieldMode) {
347
- // Switch from direct to per-field: convert string value to _source object
348
434
  const currentSource = this.getSourceValue();
349
- const obj = { _source: currentSource };
350
- this.valueChange.emit(obj);
435
+ this.valueChange.emit({ _source: currentSource });
351
436
  }
352
437
  else {
353
- // Switch from per-field to direct: extract _source as string value
354
438
  const source = this.getSourceValue();
355
439
  this.valueChange.emit(source || undefined);
356
440
  }
357
441
  }
358
- /** Handle source collection value change (used in ARRAY mode) */
359
- onSourceBrowseChange(newValue) {
360
- this.updateSourceValue(newValue);
361
- }
362
- onSourcePvChange(newValue) {
363
- this.updateSourceValue(newValue ? 'pv:' + newValue : '');
364
- }
365
- onSourceExpressionChange(newValue) {
366
- this.updateSourceValue(newValue);
367
- }
368
- /** Handle per-item field mapping change */
369
442
  onItemFieldChange(childName, sourceFieldName) {
370
443
  const current = (typeof this.value === 'object' && this.value !== null) ? { ...this.value } : { _source: '' };
371
444
  if (sourceFieldName && sourceFieldName.trim().length > 0) {
@@ -376,59 +449,30 @@ class FieldTreeComponent {
376
449
  }
377
450
  this.valueChange.emit(current);
378
451
  }
379
- /** Get the current source field name mapping for a child */
380
452
  getItemFieldValue(childName) {
381
453
  if (typeof this.value === 'object' && this.value !== null) {
382
454
  return this.value[childName] || '';
383
455
  }
384
456
  return '';
385
457
  }
386
- /** Check if the array has any children that can be mapped per-item */
387
458
  hasArrayChildren() {
388
459
  return !!(this.field?.children && this.field.children.length > 0);
389
460
  }
390
- emitScalarValue(newValue) {
391
- this.valueChange.emit(newValue || undefined);
392
- }
393
- updateSourceValue(newValue) {
394
- if (this.arrayPerFieldMode) {
395
- const current = (typeof this.value === 'object' && this.value !== null) ? { ...this.value } : {};
396
- current['_source'] = newValue || '';
397
- this.valueChange.emit(current);
398
- }
399
- else {
400
- this.valueChange.emit(newValue || undefined);
401
- }
402
- }
403
461
  updateCompleteness() {
404
- if (this.field?.fieldType === 'OBJECT' && this.field.children) {
405
- const stats = countRequiredMapped(this.field.children, this.value || {});
406
- this.mappedCount = stats.mapped;
407
- this.totalRequired = stats.total;
408
- }
409
- else if (this.field?.fieldType === 'ARRAY') {
410
- this.updateArrayCompleteness();
411
- }
412
- }
413
- updateArrayCompleteness() {
414
462
  if (!this.field?.children || this.field.children.length === 0) {
415
- // No children: just check if source is set
416
463
  this.totalRequired = this.field?.required ? 1 : 0;
417
464
  this.mappedCount = this.getSourceValue() ? (this.field?.required ? 1 : 0) : 0;
418
465
  return;
419
466
  }
420
467
  if (typeof this.value === 'object' && this.value !== null && '_source' in this.value) {
421
- // Per-field mode: count source + required children
422
468
  let total = 0;
423
469
  let mapped = 0;
424
- // Source counts as 1 required
425
470
  if (this.field?.required) {
426
471
  total++;
427
472
  if (this.value['_source'] && this.value['_source'].trim().length > 0) {
428
473
  mapped++;
429
474
  }
430
475
  }
431
- // Count required children
432
476
  for (const child of this.field.children) {
433
477
  if (child.required) {
434
478
  total++;
@@ -442,37 +486,87 @@ class FieldTreeComponent {
442
486
  this.totalRequired = total;
443
487
  }
444
488
  else {
445
- // Direct mode: just check if source is set
446
489
  this.totalRequired = this.field?.required ? 1 : 0;
447
490
  this.mappedCount = this.getSourceValue() ? (this.field?.required ? 1 : 0) : 0;
448
491
  }
449
492
  }
450
- detectInputMode(value) {
451
- if (typeof value !== 'string')
452
- return 'browse';
453
- if (value.startsWith('doc:') || value.startsWith('case:'))
454
- return 'browse';
455
- if (value.startsWith('pv:'))
456
- return 'pv';
457
- if (value.length > 0)
458
- return 'expression';
459
- return 'browse';
493
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ArrayFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
494
+ 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 });
495
+ }
496
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ArrayFieldComponent, decorators: [{
497
+ type: Component,
498
+ 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"] }]
499
+ }], propDecorators: { field: [{
500
+ type: Input
501
+ }], value: [{
502
+ type: Input
503
+ }], pluginId: [{
504
+ type: Input
505
+ }], caseDefinitionKey: [{
506
+ type: Input
507
+ }], processVariables: [{
508
+ type: Input
509
+ }], disabled: [{
510
+ type: Input
511
+ }], valueChange: [{
512
+ type: Output
513
+ }] } });
514
+
515
+ /**
516
+ * Recursive field tree component.
517
+ * Dispatches SCALAR and ARRAY to dedicated sub-components.
518
+ * Handles OBJECT inline to avoid circular import issues (OBJECT children recurse back to this component).
519
+ * Uses forwardRef(() => FieldTreeComponent) in imports to allow self-referencing in the template.
520
+ */
521
+ class FieldTreeComponent {
522
+ field;
523
+ value = undefined;
524
+ pluginId;
525
+ caseDefinitionKey = null;
526
+ processVariables = [];
527
+ disabled = false;
528
+ valueChange = new EventEmitter();
529
+ // OBJECT-specific state
530
+ expanded = false;
531
+ mappedCount = 0;
532
+ totalRequired = 0;
533
+ ngOnChanges(changes) {
534
+ if (this.field?.fieldType === 'OBJECT' && (changes['value'] || changes['field'])) {
535
+ if (this.field.children) {
536
+ const stats = countRequiredMapped(this.field.children, this.value || {});
537
+ this.mappedCount = stats.mapped;
538
+ this.totalRequired = stats.total;
539
+ }
540
+ if (!this.expanded && this.totalRequired > 0 && this.mappedCount < this.totalRequired) {
541
+ this.expanded = true;
542
+ }
543
+ }
460
544
  }
461
- isResolvableValue(value) {
462
- return value.startsWith('doc:') || value.startsWith('case:') || value.startsWith('pv:') || value.startsWith('template:');
545
+ toggleExpanded() {
546
+ this.expanded = !this.expanded;
547
+ }
548
+ onChildChange(childName, childValue) {
549
+ const current = (typeof this.value === 'object' && this.value !== null) ? { ...this.value } : {};
550
+ if (childValue === undefined || childValue === null || childValue === '') {
551
+ delete current[childName];
552
+ }
553
+ else {
554
+ current[childName] = childValue;
555
+ }
556
+ this.valueChange.emit(Object.keys(current).length > 0 ? current : undefined);
557
+ }
558
+ getChildValue(childName) {
559
+ if (typeof this.value === 'object' && this.value !== null) {
560
+ return this.value[childName];
561
+ }
562
+ return undefined;
463
563
  }
464
564
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: FieldTreeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
465
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.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: "<!-- SCALAR field: label row + input with 3-mode selector -->\n<div *ngIf=\"field.fieldType === 'SCALAR'\" class=\"field-row\" [class.field-required-unmapped]=\"field.required && !getStringValue()\">\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 <div class=\"field-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=\"field-input-control\" *ngIf=\"inputMode === 'browse'\">\n <valtimo-value-path-selector\n [name]=\"'field_' + field.path\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n [prefixes]=\"[ValuePathSelectorPrefix.DOC, ValuePathSelectorPrefix.CASE]\"\n [notation]=\"'dots'\"\n [disabled]=\"disabled\"\n [defaultValue]=\"getStringValue()\"\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=\"field-input-control\" *ngIf=\"inputMode === 'pv'\">\n <select\n *ngIf=\"processVariables.length > 0; else pvFallback\"\n class=\"pv-select\"\n [disabled]=\"disabled\"\n (change)=\"onPvChange($any($event.target).value)\"\n >\n <option value=\"\">{{ 'pvPlaceholder' | pluginTranslate: pluginId | async }}</option>\n <option *ngFor=\"let pv of processVariables\" [value]=\"pv\" [selected]=\"pv === getPvName()\">{{ pv }}</option>\n </select>\n <ng-template #pvFallback>\n <v-input\n [name]=\"'pvfb_' + field.path\"\n [defaultValue]=\"getPvName()\"\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=\"field-input-control\" *ngIf=\"inputMode === 'expression'\">\n <v-input\n [name]=\"'fx_' + field.path\"\n [defaultValue]=\"getStringValue()\"\n [disabled]=\"disabled\"\n [placeholder]=\"'e.g. pv:variableName or doc:path.to.field'\"\n (valueChange)=\"onExpressionValueChange($event)\"\n ></v-input>\n </div>\n </div>\n</div>\n\n<!-- OBJECT field: collapsible section with children -->\n<div *ngIf=\"field.fieldType === 'OBJECT'\" 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<!-- ARRAY field: collapsible section with source collection + optional per-item mapping -->\n<div *ngIf=\"field.fieldType === 'ARRAY'\" 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 <div class=\"field-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 <div class=\"field-input-control\" *ngIf=\"inputMode === 'browse'\">\n <valtimo-value-path-selector\n [name]=\"'field_' + field.path\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n [prefixes]=\"[ValuePathSelectorPrefix.DOC, ValuePathSelectorPrefix.CASE]\"\n [notation]=\"'dots'\"\n [disabled]=\"disabled\"\n [defaultValue]=\"getSourceValue()\"\n [showCaseDefinitionSelector]=\"!caseDefinitionKey\"\n (valueChangeEvent)=\"onSourceBrowseChange($event)\"\n ></valtimo-value-path-selector>\n </div>\n\n <div class=\"field-input-control\" *ngIf=\"inputMode === 'pv'\">\n <select\n *ngIf=\"processVariables.length > 0; else pvSourceFallback\"\n class=\"pv-select\"\n [disabled]=\"disabled\"\n (change)=\"onSourcePvChange($any($event.target).value)\"\n >\n <option value=\"\">{{ 'pvPlaceholder' | pluginTranslate: pluginId | async }}</option>\n <option *ngFor=\"let pv of processVariables\" [value]=\"pv\" [selected]=\"pv === getSourcePvName()\">{{ pv }}</option>\n </select>\n <ng-template #pvSourceFallback>\n <v-input\n [name]=\"'pvfb_' + field.path\"\n [defaultValue]=\"getSourcePvName()\"\n [disabled]=\"disabled\"\n [placeholder]=\"'pvPlaceholder' | pluginTranslate: pluginId | async\"\n (valueChange)=\"onSourcePvChange($event)\"\n ></v-input>\n </ng-template>\n </div>\n\n <div class=\"field-input-control\" *ngIf=\"inputMode === 'expression'\">\n <v-input\n [name]=\"'fx_' + field.path\"\n [defaultValue]=\"getSourceValue()\"\n [disabled]=\"disabled\"\n [placeholder]=\"'e.g. pv:variableName or doc:path.to.field'\"\n (valueChange)=\"onSourceExpressionChange($event)\"\n ></v-input>\n </div>\n </div>\n </div>\n\n <!-- Per-item field mapping toggle (only shown when array has children) -->\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-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}.field-input{flex:1;display:flex;align-items:flex-start;gap:.5rem;min-width:0}.field-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}.field-object{margin:.25rem 0}.field-object-header,.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-object-header:hover,.field-array-header:hover{background:#f4f4f4}.field-object-header.field-required-unmapped,.field-array-header.field-required-unmapped{border-left-color:#dc3545;background-color:#fff5f5}.field-object-header .expand-icon,.field-array-header .expand-icon{flex:0 0 1rem;font-size:.75rem;color:#525252}.field-object-header .field-name,.field-array-header .field-name{font-weight:500}.field-object-header .field-meta,.field-array-header .field-meta{font-size:.8125rem;color:#6c757d}.field-object-header .completeness-badge,.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-object-header .mapped-indicator,.field-array-header .mapped-indicator{margin-left:auto;color:#198754;font-weight:600}.field-object-children{padding-left:1.25rem;border-left:1px solid #e0e0e0;margin-left:.5rem}.field-array{margin:.25rem 0}.field-array-content{padding-left:1.25rem;border-left:1px solid #e0e0e0;margin-left:.5rem}.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: "component", type: FieldTreeComponent, selector: "epistola-field-tree", inputs: ["field", "value", "pluginId", "caseDefinitionKey", "processVariables", "disabled"], outputs: ["valueChange"] }, { 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: "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 });
565
+ 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 });
466
566
  }
467
567
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: FieldTreeComponent, decorators: [{
468
568
  type: Component,
469
- args: [{ selector: 'epistola-field-tree', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [
470
- CommonModule,
471
- FormsModule,
472
- PluginTranslatePipeModule,
473
- InputModule,
474
- ValuePathSelectorComponent
475
- ], template: "<!-- SCALAR field: label row + input with 3-mode selector -->\n<div *ngIf=\"field.fieldType === 'SCALAR'\" class=\"field-row\" [class.field-required-unmapped]=\"field.required && !getStringValue()\">\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 <div class=\"field-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=\"field-input-control\" *ngIf=\"inputMode === 'browse'\">\n <valtimo-value-path-selector\n [name]=\"'field_' + field.path\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n [prefixes]=\"[ValuePathSelectorPrefix.DOC, ValuePathSelectorPrefix.CASE]\"\n [notation]=\"'dots'\"\n [disabled]=\"disabled\"\n [defaultValue]=\"getStringValue()\"\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=\"field-input-control\" *ngIf=\"inputMode === 'pv'\">\n <select\n *ngIf=\"processVariables.length > 0; else pvFallback\"\n class=\"pv-select\"\n [disabled]=\"disabled\"\n (change)=\"onPvChange($any($event.target).value)\"\n >\n <option value=\"\">{{ 'pvPlaceholder' | pluginTranslate: pluginId | async }}</option>\n <option *ngFor=\"let pv of processVariables\" [value]=\"pv\" [selected]=\"pv === getPvName()\">{{ pv }}</option>\n </select>\n <ng-template #pvFallback>\n <v-input\n [name]=\"'pvfb_' + field.path\"\n [defaultValue]=\"getPvName()\"\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=\"field-input-control\" *ngIf=\"inputMode === 'expression'\">\n <v-input\n [name]=\"'fx_' + field.path\"\n [defaultValue]=\"getStringValue()\"\n [disabled]=\"disabled\"\n [placeholder]=\"'e.g. pv:variableName or doc:path.to.field'\"\n (valueChange)=\"onExpressionValueChange($event)\"\n ></v-input>\n </div>\n </div>\n</div>\n\n<!-- OBJECT field: collapsible section with children -->\n<div *ngIf=\"field.fieldType === 'OBJECT'\" 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<!-- ARRAY field: collapsible section with source collection + optional per-item mapping -->\n<div *ngIf=\"field.fieldType === 'ARRAY'\" 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 <div class=\"field-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 <div class=\"field-input-control\" *ngIf=\"inputMode === 'browse'\">\n <valtimo-value-path-selector\n [name]=\"'field_' + field.path\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n [prefixes]=\"[ValuePathSelectorPrefix.DOC, ValuePathSelectorPrefix.CASE]\"\n [notation]=\"'dots'\"\n [disabled]=\"disabled\"\n [defaultValue]=\"getSourceValue()\"\n [showCaseDefinitionSelector]=\"!caseDefinitionKey\"\n (valueChangeEvent)=\"onSourceBrowseChange($event)\"\n ></valtimo-value-path-selector>\n </div>\n\n <div class=\"field-input-control\" *ngIf=\"inputMode === 'pv'\">\n <select\n *ngIf=\"processVariables.length > 0; else pvSourceFallback\"\n class=\"pv-select\"\n [disabled]=\"disabled\"\n (change)=\"onSourcePvChange($any($event.target).value)\"\n >\n <option value=\"\">{{ 'pvPlaceholder' | pluginTranslate: pluginId | async }}</option>\n <option *ngFor=\"let pv of processVariables\" [value]=\"pv\" [selected]=\"pv === getSourcePvName()\">{{ pv }}</option>\n </select>\n <ng-template #pvSourceFallback>\n <v-input\n [name]=\"'pvfb_' + field.path\"\n [defaultValue]=\"getSourcePvName()\"\n [disabled]=\"disabled\"\n [placeholder]=\"'pvPlaceholder' | pluginTranslate: pluginId | async\"\n (valueChange)=\"onSourcePvChange($event)\"\n ></v-input>\n </ng-template>\n </div>\n\n <div class=\"field-input-control\" *ngIf=\"inputMode === 'expression'\">\n <v-input\n [name]=\"'fx_' + field.path\"\n [defaultValue]=\"getSourceValue()\"\n [disabled]=\"disabled\"\n [placeholder]=\"'e.g. pv:variableName or doc:path.to.field'\"\n (valueChange)=\"onSourceExpressionChange($event)\"\n ></v-input>\n </div>\n </div>\n </div>\n\n <!-- Per-item field mapping toggle (only shown when array has children) -->\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-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}.field-input{flex:1;display:flex;align-items:flex-start;gap:.5rem;min-width:0}.field-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}.field-object{margin:.25rem 0}.field-object-header,.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-object-header:hover,.field-array-header:hover{background:#f4f4f4}.field-object-header.field-required-unmapped,.field-array-header.field-required-unmapped{border-left-color:#dc3545;background-color:#fff5f5}.field-object-header .expand-icon,.field-array-header .expand-icon{flex:0 0 1rem;font-size:.75rem;color:#525252}.field-object-header .field-name,.field-array-header .field-name{font-weight:500}.field-object-header .field-meta,.field-array-header .field-meta{font-size:.8125rem;color:#6c757d}.field-object-header .completeness-badge,.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-object-header .mapped-indicator,.field-array-header .mapped-indicator{margin-left:auto;color:#198754;font-weight:600}.field-object-children{padding-left:1.25rem;border-left:1px solid #e0e0e0;margin-left:.5rem}.field-array{margin:.25rem 0}.field-array-content{padding-left:1.25rem;border-left:1px solid #e0e0e0;margin-left:.5rem}.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"] }]
569
+ 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"] }]
476
570
  }], propDecorators: { field: [{
477
571
  type: Input
478
572
  }], value: [{
@@ -495,35 +589,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
495
589
  */
496
590
  class DataMappingTreeComponent {
497
591
  pluginId;
498
- templateFields$;
499
- prefillMapping$;
500
- disabled$;
592
+ templateFields = [];
593
+ prefillMapping = {};
594
+ disabled = false;
501
595
  caseDefinitionKey = null;
502
596
  processVariables = [];
503
597
  mappingChange = new EventEmitter();
504
598
  requiredFieldsStatus = new EventEmitter();
505
- templateFields = [];
506
599
  mapping = {};
507
- disabled = false;
508
- destroy$ = new Subject();
509
- ngOnInit() {
510
- this.templateFields$.pipe(takeUntil(this.destroy$)).subscribe(fields => {
511
- this.templateFields = fields;
512
- this.emitRequiredFieldsStatus();
513
- });
514
- this.prefillMapping$.pipe(takeUntil(this.destroy$)).subscribe(mapping => {
600
+ ngOnChanges(changes) {
601
+ if (changes['prefillMapping']) {
602
+ const mapping = this.prefillMapping;
515
603
  if (mapping && Object.keys(mapping).length > 0) {
516
604
  this.mapping = { ...mapping };
517
605
  }
606
+ }
607
+ if (changes['templateFields'] || changes['prefillMapping']) {
518
608
  this.emitRequiredFieldsStatus();
519
- });
520
- this.disabled$.pipe(takeUntil(this.destroy$)).subscribe(disabled => {
521
- this.disabled = disabled;
522
- });
523
- }
524
- ngOnDestroy() {
525
- this.destroy$.next();
526
- this.destroy$.complete();
609
+ }
527
610
  }
528
611
  onFieldValueChange(fieldName, value) {
529
612
  if (value === undefined || value === null || value === '') {
@@ -544,7 +627,7 @@ class DataMappingTreeComponent {
544
627
  this.requiredFieldsStatus.emit(stats);
545
628
  }
546
629
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DataMappingTreeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
547
- 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" }, 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 });
630
+ 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 });
548
631
  }
549
632
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: DataMappingTreeComponent, decorators: [{
550
633
  type: Component,
@@ -555,11 +638,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
555
638
  ], 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"] }]
556
639
  }], propDecorators: { pluginId: [{
557
640
  type: Input
558
- }], templateFields$: [{
641
+ }], templateFields: [{
559
642
  type: Input
560
- }], prefillMapping$: [{
643
+ }], prefillMapping: [{
561
644
  type: Input
562
- }], disabled$: [{
645
+ }], disabled: [{
563
646
  type: Input
564
647
  }], caseDefinitionKey: [{
565
648
  type: Input
@@ -575,53 +658,32 @@ class GenerateDocumentConfigurationComponent {
575
658
  epistolaPluginService;
576
659
  processLinkStateService;
577
660
  cdr;
578
- // Required inputs from FunctionConfigurationComponent
579
661
  save$;
580
662
  disabled$;
581
663
  pluginId;
582
664
  prefillConfiguration$;
583
- // Optional inputs from FunctionConfigurationComponent
584
665
  selectedPluginConfigurationData$;
585
666
  context$;
586
667
  valid = new EventEmitter();
587
668
  configuration = new EventEmitter();
588
- // Template options loaded from API
589
- templateOptions$ = new BehaviorSubject([]);
590
- templatesLoading$ = new BehaviorSubject(false);
591
- templatesError$ = new BehaviorSubject(null);
592
- // Variant options loaded based on selected template
593
- variantOptions$ = new BehaviorSubject([]);
594
- variantsLoading$ = new BehaviorSubject(false);
595
- variantsError$ = new BehaviorSubject(null);
596
- // Environment options loaded from API
597
- environmentOptions$ = new BehaviorSubject([]);
598
- environmentsLoading$ = new BehaviorSubject(false);
599
- environmentsError$ = new BehaviorSubject(null);
600
- // Template fields for data mapping
601
- templateFields$ = new BehaviorSubject([]);
602
- templateFieldsLoading$ = new BehaviorSubject(false);
603
- templateFieldsError$ = new BehaviorSubject(null);
604
- // Current data mapping (nested structure mirroring template schema)
669
+ templates$ = new BehaviorSubject(initialResource([]));
670
+ variants$ = new BehaviorSubject(initialResource([]));
671
+ environments$ = new BehaviorSubject(initialResource([]));
672
+ templateFields$ = new BehaviorSubject(initialResource([]));
605
673
  dataMapping$ = new BehaviorSubject({});
606
- // Prefill data mapping observable for the tree
607
- prefillDataMapping$;
608
674
  outputFormatOptions = [
609
675
  { id: 'PDF', text: 'PDF' },
610
676
  { id: 'HTML', text: 'HTML' }
611
677
  ];
612
- // Show data mapping builder only when template is selected
613
678
  selectedTemplateId$ = new BehaviorSubject('');
614
679
  selectedVariantId$ = new BehaviorSubject('');
615
- // Variant selection mode: 'explicit' (dropdown) or 'attributes' (key-value pairs)
616
680
  variantSelectionMode = 'explicit';
617
- // Variant attributes for attribute-based selection
618
681
  variantAttributeEntries = [];
619
- // Case definition key from context (for ValuePathSelector)
682
+ availableAttributeKeys = [];
620
683
  caseDefinitionKey = null;
621
- // Discovered process variables
622
684
  processVariables = [];
623
- // Required fields status
624
685
  requiredFieldsStatus = { mapped: 0, total: 0 };
686
+ prefillDataMapping = {};
625
687
  destroy$ = new Subject();
626
688
  saveSubscription;
627
689
  formValue$ = new BehaviorSubject(null);
@@ -634,10 +696,11 @@ class GenerateDocumentConfigurationComponent {
634
696
  }
635
697
  ngOnInit() {
636
698
  this.initContext();
637
- this.initPrefillDataMapping();
699
+ this.initPrefill();
638
700
  this.initPluginConfiguration();
639
701
  this.initTemplatesLoading();
640
702
  this.initEnvironmentsLoading();
703
+ this.initAttributesLoading();
641
704
  this.initVariantsLoading();
642
705
  this.initTemplateFieldsLoading();
643
706
  this.openSaveSubscription();
@@ -650,12 +713,10 @@ class GenerateDocumentConfigurationComponent {
650
713
  formValueChange(formOutput) {
651
714
  const formValue = formOutput;
652
715
  this.formValue$.next(formValue);
653
- // Update selected template if changed (also clears variant selection)
654
716
  if (formValue.templateId && formValue.templateId !== this.selectedTemplateId$.getValue()) {
655
717
  this.selectedTemplateId$.next(formValue.templateId);
656
718
  this.selectedVariantId$.next('');
657
719
  }
658
- // Update selected variant if changed
659
720
  if (formValue.variantId && formValue.variantId !== this.selectedVariantId$.getValue()) {
660
721
  this.selectedVariantId$.next(formValue.variantId);
661
722
  }
@@ -663,7 +724,6 @@ class GenerateDocumentConfigurationComponent {
663
724
  }
664
725
  onDataMappingChange(mapping) {
665
726
  this.dataMapping$.next(mapping);
666
- // Re-validate when data mapping changes
667
727
  const currentFormValue = this.formValue$.getValue();
668
728
  if (currentFormValue) {
669
729
  this.handleValid(currentFormValue);
@@ -671,7 +731,6 @@ class GenerateDocumentConfigurationComponent {
671
731
  }
672
732
  onRequiredFieldsStatusChange(status) {
673
733
  this.requiredFieldsStatus = status;
674
- // Re-validate when required fields status changes
675
734
  const currentFormValue = this.formValue$.getValue();
676
735
  if (currentFormValue) {
677
736
  this.handleValid(currentFormValue);
@@ -680,16 +739,12 @@ class GenerateDocumentConfigurationComponent {
680
739
  onVariantSelectionModeChange(mode) {
681
740
  this.variantSelectionMode = mode;
682
741
  if (mode === 'attributes' && this.variantAttributeEntries.length === 0) {
683
- this.variantAttributeEntries = [{ key: '', value: '' }];
684
- }
685
- // Re-validate
686
- const currentFormValue = this.formValue$.getValue();
687
- if (currentFormValue) {
688
- this.handleValid(currentFormValue);
742
+ this.variantAttributeEntries = [{ key: '', value: '', required: true }];
689
743
  }
744
+ this.revalidate();
690
745
  }
691
746
  addAttributeEntry() {
692
- this.variantAttributeEntries = [...this.variantAttributeEntries, { key: '', value: '' }];
747
+ this.variantAttributeEntries = [...this.variantAttributeEntries, { key: '', value: '', required: true }];
693
748
  this.revalidate();
694
749
  }
695
750
  removeAttributeEntry(index) {
@@ -699,6 +754,21 @@ class GenerateDocumentConfigurationComponent {
699
754
  onAttributeEntryChange() {
700
755
  this.revalidate();
701
756
  }
757
+ onKeySelected(entry, value) {
758
+ if (value === '__custom__') {
759
+ entry._customKey = true;
760
+ entry.key = '';
761
+ }
762
+ else {
763
+ entry.key = value;
764
+ }
765
+ this.onAttributeEntryChange();
766
+ }
767
+ cancelCustomKey(entry) {
768
+ entry._customKey = false;
769
+ entry.key = '';
770
+ this.onAttributeEntryChange();
771
+ }
702
772
  revalidate() {
703
773
  const currentFormValue = this.formValue$.getValue();
704
774
  if (currentFormValue) {
@@ -719,118 +789,93 @@ class GenerateDocumentConfigurationComponent {
719
789
  });
720
790
  }
721
791
  }
722
- initPrefillDataMapping() {
792
+ initPrefill() {
723
793
  if (this.prefillConfiguration$) {
724
- this.prefillDataMapping$ = this.prefillConfiguration$.pipe(map(config => config?.dataMapping || {}));
725
- // Also set initial selected template, variant mode, and variant
726
794
  this.prefillConfiguration$.pipe(takeUntil(this.destroy$), filter(config => !!config?.templateId)).subscribe(config => {
727
795
  this.selectedTemplateId$.next(config.templateId);
728
- if (config.variantAttributes && Object.keys(config.variantAttributes).length > 0) {
796
+ if (config.variantAttributes && (Array.isArray(config.variantAttributes) ? config.variantAttributes.length > 0 : Object.keys(config.variantAttributes).length > 0)) {
729
797
  this.variantSelectionMode = 'attributes';
730
- this.variantAttributeEntries = Object.entries(config.variantAttributes)
731
- .map(([key, value]) => ({ key, value }));
798
+ if (Array.isArray(config.variantAttributes)) {
799
+ // New format: VariantAttributeEntry[]
800
+ this.variantAttributeEntries = config.variantAttributes
801
+ .map(e => ({ key: e.key, value: e.value, required: e.required !== false }));
802
+ }
803
+ else {
804
+ // Old format: Record<string, string> — treat all as required
805
+ this.variantAttributeEntries = Object.entries(config.variantAttributes)
806
+ .map(([key, value]) => ({ key, value: String(value), required: true }));
807
+ }
732
808
  }
733
809
  else if (config.variantId) {
734
810
  this.variantSelectionMode = 'explicit';
735
811
  this.selectedVariantId$.next(config.variantId);
736
812
  }
737
813
  if (config.dataMapping) {
814
+ this.prefillDataMapping = config.dataMapping;
738
815
  this.dataMapping$.next(config.dataMapping);
739
816
  }
740
817
  this.cdr.markForCheck();
741
818
  });
742
819
  }
743
- else {
744
- this.prefillDataMapping$ = new BehaviorSubject({}).asObservable();
745
- }
746
820
  }
747
821
  initPluginConfiguration() {
748
822
  const sources = [];
749
- // Create mode: framework emits when user selects a plugin configuration
750
823
  if (this.selectedPluginConfigurationData$) {
751
824
  sources.push(this.selectedPluginConfigurationData$.pipe(filter(config => !!config?.configurationId), map(config => config.configurationId)));
752
825
  }
753
- // Edit mode: read pluginConfigurationId from the ProcessLink entity
754
- // (selectedPluginConfigurationData$ does not emit in edit mode)
755
826
  sources.push(this.processLinkStateService.selectedProcessLink$.pipe(filter(processLink => !!processLink?.pluginConfigurationId), map(processLink => processLink.pluginConfigurationId)));
756
827
  merge(...sources).pipe(takeUntil(this.destroy$)).subscribe(configurationId => {
757
828
  this.pluginConfigurationId$.next(configurationId);
758
829
  });
759
830
  }
760
831
  initTemplatesLoading() {
761
- this.pluginConfigurationId$.pipe(takeUntil(this.destroy$), filter(id => !!id)).subscribe(configurationId => {
762
- this.templatesLoading$.next(true);
763
- this.templatesError$.next(null);
764
- this.epistolaPluginService.getTemplates(configurationId).pipe(takeUntil(this.destroy$), catchError(() => {
765
- this.templatesError$.next('Failed to load templates');
766
- return of([]);
767
- })).subscribe(templates => {
768
- const options = templates.map(t => ({
769
- id: t.id,
770
- text: t.name
771
- }));
772
- this.templateOptions$.next(options);
773
- this.templatesLoading$.next(false);
774
- });
832
+ this.pluginConfigurationId$.pipe(takeUntil(this.destroy$), filter(id => !!id), tap(() => this.templates$.next(loadingResource(this.templates$.getValue().data))), switchMap(configurationId => this.epistolaPluginService.getTemplates(configurationId).pipe(catchError(() => {
833
+ this.templates$.next(errorResource([], 'Failed to load templates'));
834
+ return of(null);
835
+ }))), filter(result => result !== null)).subscribe(templates => {
836
+ this.templates$.next(successResource(templates.map(t => ({ id: t.id, text: t.name }))));
775
837
  });
776
838
  }
777
839
  initEnvironmentsLoading() {
778
- this.pluginConfigurationId$.pipe(takeUntil(this.destroy$), filter(id => !!id)).subscribe(configurationId => {
779
- this.environmentsLoading$.next(true);
780
- this.environmentsError$.next(null);
781
- this.epistolaPluginService.getEnvironments(configurationId).pipe(takeUntil(this.destroy$), catchError(() => {
782
- this.environmentsError$.next('Failed to load environments');
783
- return of([]);
784
- })).subscribe(environments => {
785
- const options = environments.map(e => ({
786
- id: e.id,
787
- text: e.name
788
- }));
789
- this.environmentOptions$.next(options);
790
- this.environmentsLoading$.next(false);
791
- });
840
+ this.pluginConfigurationId$.pipe(takeUntil(this.destroy$), filter(id => !!id), tap(() => this.environments$.next(loadingResource(this.environments$.getValue().data))), switchMap(configurationId => this.epistolaPluginService.getEnvironments(configurationId).pipe(catchError(() => {
841
+ this.environments$.next(errorResource([], 'Failed to load environments'));
842
+ return of(null);
843
+ }))), filter(result => result !== null)).subscribe(environments => {
844
+ this.environments$.next(successResource(environments.map(e => ({ id: e.id, text: e.name }))));
845
+ });
846
+ }
847
+ initAttributesLoading() {
848
+ this.pluginConfigurationId$.pipe(takeUntil(this.destroy$), filter(id => !!id), switchMap(configurationId => this.epistolaPluginService.getAttributes(configurationId).pipe(catchError(() => of([]))))).subscribe(attributes => {
849
+ this.availableAttributeKeys = attributes.map(a => a.key).sort();
850
+ this.cdr.markForCheck();
792
851
  });
793
852
  }
794
853
  initVariantsLoading() {
795
854
  combineLatest([
796
855
  this.pluginConfigurationId$,
797
856
  this.selectedTemplateId$
798
- ]).pipe(takeUntil(this.destroy$), filter(([configId, templateId]) => !!configId && !!templateId)).subscribe(([configurationId, templateId]) => {
799
- this.variantsLoading$.next(true);
800
- this.variantsError$.next(null);
801
- this.epistolaPluginService.getVariants(configurationId, templateId).pipe(takeUntil(this.destroy$), catchError(() => {
802
- this.variantsError$.next('Failed to load variants');
803
- return of([]);
804
- })).subscribe(variants => {
805
- const options = variants.map(v => ({
806
- id: v.id,
807
- text: v.name + this.formatAttributes(v.attributes)
808
- }));
809
- this.variantOptions$.next(options);
810
- this.variantsLoading$.next(false);
811
- });
857
+ ]).pipe(takeUntil(this.destroy$), filter(([configId, templateId]) => !!configId && !!templateId), tap(() => this.variants$.next(loadingResource(this.variants$.getValue().data))), switchMap(([configurationId, templateId]) => this.epistolaPluginService.getVariants(configurationId, templateId).pipe(catchError(() => {
858
+ this.variants$.next(errorResource([], 'Failed to load variants'));
859
+ return of(null);
860
+ }))), filter(result => result !== null)).subscribe(variants => {
861
+ this.variants$.next(successResource(variants.map(v => ({ id: v.id, text: v.name + this.formatAttributes(v.attributes) }))));
812
862
  });
813
863
  }
814
864
  initTemplateFieldsLoading() {
815
865
  combineLatest([
816
866
  this.pluginConfigurationId$,
817
867
  this.selectedTemplateId$
818
- ]).pipe(takeUntil(this.destroy$), filter(([configId, templateId]) => !!configId && !!templateId)).subscribe(([configurationId, templateId]) => {
819
- this.templateFieldsLoading$.next(true);
820
- this.templateFieldsError$.next(null);
821
- this.epistolaPluginService.getTemplateDetails(configurationId, templateId).pipe(takeUntil(this.destroy$), catchError(() => {
822
- this.templateFieldsError$.next('Failed to load template fields');
823
- return of({ fields: [] });
824
- })).subscribe(details => {
825
- this.templateFields$.next(details.fields || []);
826
- this.templateFieldsLoading$.next(false);
827
- });
828
- // Also load process variables if we have a case context
868
+ ]).pipe(takeUntil(this.destroy$), filter(([configId, templateId]) => !!configId && !!templateId), tap(() => {
869
+ this.templateFields$.next(loadingResource(this.templateFields$.getValue().data));
829
870
  this.loadProcessVariables();
871
+ }), switchMap(([configurationId, templateId]) => this.epistolaPluginService.getTemplateDetails(configurationId, templateId).pipe(catchError(() => {
872
+ this.templateFields$.next(errorResource([], 'Failed to load template fields'));
873
+ return of(null);
874
+ }))), filter(result => result !== null)).subscribe(details => {
875
+ this.templateFields$.next(successResource(details.fields || []));
830
876
  });
831
877
  }
832
878
  loadProcessVariables() {
833
- // Try to discover process variables (best-effort, may not always have context)
834
879
  if (this.caseDefinitionKey) {
835
880
  this.epistolaPluginService.getProcessVariables(this.caseDefinitionKey).pipe(takeUntil(this.destroy$), catchError(() => of([]))).subscribe(variables => {
836
881
  this.processVariables = variables;
@@ -843,13 +888,10 @@ class GenerateDocumentConfigurationComponent {
843
888
  formValue?.outputFormat &&
844
889
  formValue?.filename &&
845
890
  formValue?.resultProcessVariable);
846
- // Variant selection: if attribute mode is used, all entries must have both key and value filled.
847
- // Neither variant nor attributes are required — omitting both uses the template's default variant.
848
891
  let variantValid = true;
849
892
  if (this.variantSelectionMode === 'attributes' && this.variantAttributeEntries.length > 0) {
850
893
  variantValid = this.variantAttributeEntries.every(e => !!e.key && !!e.value);
851
894
  }
852
- // Check if all required template fields are mapped
853
895
  const requiredFieldsMapped = this.requiredFieldsStatus.total === 0 ||
854
896
  this.requiredFieldsStatus.mapped === this.requiredFieldsStatus.total;
855
897
  const valid = baseComplete && variantValid && requiredFieldsMapped;
@@ -875,12 +917,9 @@ class GenerateDocumentConfigurationComponent {
875
917
  config.variantId = formValue.variantId;
876
918
  }
877
919
  else {
878
- config.variantAttributes = {};
879
- for (const entry of this.variantAttributeEntries) {
880
- if (entry.key && entry.value) {
881
- config.variantAttributes[entry.key] = entry.value;
882
- }
883
- }
920
+ config.variantAttributes = this.variantAttributeEntries
921
+ .filter(e => e.key && e.value)
922
+ .map(e => ({ key: e.key, value: e.value, required: e.required }));
884
923
  }
885
924
  this.configuration.emit(config);
886
925
  }
@@ -888,7 +927,7 @@ class GenerateDocumentConfigurationComponent {
888
927
  });
889
928
  }
890
929
  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 });
891
- 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 templateOptions: templateOptions$ | async,\n templatesLoading: templatesLoading$ | async,\n templatesError: templatesError$ | async,\n variantOptions: variantOptions$ | async,\n variantsLoading: variantsLoading$ | async,\n variantsError: variantsError$ | async,\n environmentOptions: environmentOptions$ | async,\n environmentsLoading: environmentsLoading$ | async,\n environmentsError: environmentsError$ | async,\n templateFieldsLoading: templateFieldsLoading$ | async,\n selectedTemplateId: selectedTemplateId$ | async\n } as obs\"\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.templateOptions\"\n [defaultSelectionId]=\"obs.prefill?.templateId\"\n [disabled]=\"obs.disabled || obs.templatesLoading\"\n [required]=\"true\"\n [loading]=\"obs.templatesLoading\"\n >\n </v-select>\n <div *ngIf=\"obs.templatesError\" class=\"loading-error\">{{ obs.templatesError }}</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.variantOptions\"\n [defaultSelectionId]=\"obs.prefill?.variantId\"\n [disabled]=\"obs.disabled || obs.variantsLoading || !obs.selectedTemplateId\"\n [required]=\"false\"\n [loading]=\"obs.variantsLoading\"\n >\n </v-select>\n <div *ngIf=\"obs.variantsError\" class=\"loading-error\">{{ obs.variantsError }}</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 <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 <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 <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.environmentOptions\"\n [defaultSelectionId]=\"obs.prefill?.environmentId\"\n [disabled]=\"obs.disabled || obs.environmentsLoading\"\n [required]=\"false\"\n [loading]=\"obs.environmentsLoading\"\n >\n </v-select>\n <div *ngIf=\"obs.environmentsError\" class=\"loading-error\">{{ obs.environmentsError }}</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=\"templateFieldsError$ | async as templateFieldsError\" class=\"loading-error\">{{ templateFieldsError }}</div>\n\n<epistola-data-mapping-tree\n *ngIf=\"(selectedTemplateId$ | async)\"\n [pluginId]=\"pluginId\"\n [templateFields$]=\"templateFields$\"\n [disabled$]=\"disabled$\"\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}.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.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { 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 });
930
+ 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 templates: templates$ | async,\n variants: variants$ | async,\n environments: environments$ | async,\n templateFields: templateFields$ | async,\n selectedTemplateId: selectedTemplateId$ | async\n } as obs\"\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\"\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 });
892
931
  }
893
932
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: GenerateDocumentConfigurationComponent, decorators: [{
894
933
  type: Component,
@@ -900,7 +939,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
900
939
  InputModule,
901
940
  SelectModule,
902
941
  DataMappingTreeComponent
903
- ], template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: disabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n templateOptions: templateOptions$ | async,\n templatesLoading: templatesLoading$ | async,\n templatesError: templatesError$ | async,\n variantOptions: variantOptions$ | async,\n variantsLoading: variantsLoading$ | async,\n variantsError: variantsError$ | async,\n environmentOptions: environmentOptions$ | async,\n environmentsLoading: environmentsLoading$ | async,\n environmentsError: environmentsError$ | async,\n templateFieldsLoading: templateFieldsLoading$ | async,\n selectedTemplateId: selectedTemplateId$ | async\n } as obs\"\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.templateOptions\"\n [defaultSelectionId]=\"obs.prefill?.templateId\"\n [disabled]=\"obs.disabled || obs.templatesLoading\"\n [required]=\"true\"\n [loading]=\"obs.templatesLoading\"\n >\n </v-select>\n <div *ngIf=\"obs.templatesError\" class=\"loading-error\">{{ obs.templatesError }}</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.variantOptions\"\n [defaultSelectionId]=\"obs.prefill?.variantId\"\n [disabled]=\"obs.disabled || obs.variantsLoading || !obs.selectedTemplateId\"\n [required]=\"false\"\n [loading]=\"obs.variantsLoading\"\n >\n </v-select>\n <div *ngIf=\"obs.variantsError\" class=\"loading-error\">{{ obs.variantsError }}</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 <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 <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 <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.environmentOptions\"\n [defaultSelectionId]=\"obs.prefill?.environmentId\"\n [disabled]=\"obs.disabled || obs.environmentsLoading\"\n [required]=\"false\"\n [loading]=\"obs.environmentsLoading\"\n >\n </v-select>\n <div *ngIf=\"obs.environmentsError\" class=\"loading-error\">{{ obs.environmentsError }}</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=\"templateFieldsError$ | async as templateFieldsError\" class=\"loading-error\">{{ templateFieldsError }}</div>\n\n<epistola-data-mapping-tree\n *ngIf=\"(selectedTemplateId$ | async)\"\n [pluginId]=\"pluginId\"\n [templateFields$]=\"templateFields$\"\n [disabled$]=\"disabled$\"\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}.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"] }]
942
+ ], template: "<v-form\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: disabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n templates: templates$ | async,\n variants: variants$ | async,\n environments: environments$ | async,\n templateFields: templateFields$ | async,\n selectedTemplateId: selectedTemplateId$ | async\n } as obs\"\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\"\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"] }]
904
943
  }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$2.ProcessLinkStateService }, { type: i0.ChangeDetectorRef }], propDecorators: { save$: [{
905
944
  type: Input
906
945
  }], disabled$: [{
@@ -1218,7 +1257,7 @@ class EpistolaRetryFormComponent {
1218
1257
  processInstanceId,
1219
1258
  sourceActivityId: this.sourceActivityId || null,
1220
1259
  overrides: formData
1221
- }, { responseType: 'blob' }).subscribe({
1260
+ }, { responseType: 'blob', headers: new HttpHeaders().set('X-Skip-Interceptor', '422') }).subscribe({
1222
1261
  next: (blob) => {
1223
1262
  this.currentBlobUrl = URL.createObjectURL(blob);
1224
1263
  this.previewUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.currentBlobUrl);
@@ -1740,7 +1779,85 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
1740
1779
  type: Input
1741
1780
  }] } });
1742
1781
 
1782
+ const EPISTOLA_DOWNLOAD_OPTIONS = {
1783
+ type: 'epistola-download',
1784
+ selector: 'epistola-download-button',
1785
+ title: 'Epistola Download',
1786
+ group: 'basic',
1787
+ icon: 'download',
1788
+ emptyValue: null,
1789
+ fieldOptions: ['filename', 'label'],
1790
+ };
1791
+ function registerEpistolaDownloadComponent(injector) {
1792
+ if (!customElements.get(EPISTOLA_DOWNLOAD_OPTIONS.selector)) {
1793
+ registerCustomFormioComponent(EPISTOLA_DOWNLOAD_OPTIONS, EpistolaDownloadComponent, injector);
1794
+ }
1795
+ }
1796
+
1797
+ const EPISTOLA_RETRY_FORM_OPTIONS = {
1798
+ type: 'epistola-retry-form',
1799
+ selector: 'epistola-retry-form-element',
1800
+ title: 'Epistola Retry Form',
1801
+ group: 'basic',
1802
+ icon: 'refresh',
1803
+ emptyValue: null,
1804
+ fieldOptions: ['sourceActivityId', 'label'], // sourceActivityId is optional (set via BPMN input parameter)
1805
+ };
1806
+ function registerEpistolaRetryFormComponent(injector) {
1807
+ if (!customElements.get(EPISTOLA_RETRY_FORM_OPTIONS.selector)) {
1808
+ registerCustomFormioComponent(EPISTOLA_RETRY_FORM_OPTIONS, EpistolaRetryFormComponent, injector);
1809
+ }
1810
+ }
1811
+
1812
+ const EPISTOLA_PREVIEW_BUTTON_OPTIONS = {
1813
+ type: 'epistola-preview-button',
1814
+ selector: 'epistola-preview-button-element',
1815
+ title: 'Epistola Preview',
1816
+ group: 'basic',
1817
+ icon: 'eye',
1818
+ emptyValue: null,
1819
+ fieldOptions: ['label'],
1820
+ };
1821
+ function registerEpistolaPreviewButtonComponent(injector) {
1822
+ if (!customElements.get(EPISTOLA_PREVIEW_BUTTON_OPTIONS.selector)) {
1823
+ registerCustomFormioComponent(EPISTOLA_PREVIEW_BUTTON_OPTIONS, EpistolaPreviewButtonComponent, injector);
1824
+ }
1825
+ }
1826
+
1827
+ const EPISTOLA_DOCUMENT_PREVIEW_OPTIONS = {
1828
+ type: 'epistola-document-preview',
1829
+ selector: 'epistola-document-preview-element',
1830
+ title: 'Epistola Document Preview',
1831
+ group: 'basic',
1832
+ icon: 'file-pdf-o',
1833
+ emptyValue: null,
1834
+ fieldOptions: ['label'],
1835
+ };
1836
+ function registerEpistolaDocumentPreviewComponent(injector) {
1837
+ if (!customElements.get(EPISTOLA_DOCUMENT_PREVIEW_OPTIONS.selector)) {
1838
+ registerCustomFormioComponent(EPISTOLA_DOCUMENT_PREVIEW_OPTIONS, EpistolaDocumentPreviewComponent, injector);
1839
+ }
1840
+ }
1841
+
1743
1842
  class EpistolaPluginModule {
1843
+ static forRoot() {
1844
+ return {
1845
+ ngModule: EpistolaPluginModule,
1846
+ providers: [
1847
+ {
1848
+ provide: ENVIRONMENT_INITIALIZER,
1849
+ multi: true,
1850
+ useValue: () => {
1851
+ const injector = inject(Injector);
1852
+ registerEpistolaDownloadComponent(injector);
1853
+ registerEpistolaRetryFormComponent(injector);
1854
+ registerEpistolaPreviewButtonComponent(injector);
1855
+ registerEpistolaDocumentPreviewComponent(injector);
1856
+ }
1857
+ }
1858
+ ]
1859
+ };
1860
+ }
1744
1861
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaPluginModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
1745
1862
  static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.20", ngImport: i0, type: EpistolaPluginModule, imports: [CommonModule,
1746
1863
  HttpClientModule,
@@ -1753,6 +1870,9 @@ class EpistolaPluginModule {
1753
1870
  CheckJobStatusConfigurationComponent,
1754
1871
  DownloadDocumentConfigurationComponent,
1755
1872
  DataMappingTreeComponent,
1873
+ ValueInputComponent,
1874
+ ScalarFieldComponent,
1875
+ ArrayFieldComponent,
1756
1876
  FieldTreeComponent,
1757
1877
  EpistolaDownloadComponent,
1758
1878
  EpistolaRetryFormComponent,
@@ -1762,6 +1882,9 @@ class EpistolaPluginModule {
1762
1882
  CheckJobStatusConfigurationComponent,
1763
1883
  DownloadDocumentConfigurationComponent,
1764
1884
  DataMappingTreeComponent,
1885
+ ValueInputComponent,
1886
+ ScalarFieldComponent,
1887
+ ArrayFieldComponent,
1765
1888
  FieldTreeComponent,
1766
1889
  EpistolaDownloadComponent,
1767
1890
  EpistolaRetryFormComponent,
@@ -1780,6 +1903,9 @@ class EpistolaPluginModule {
1780
1903
  CheckJobStatusConfigurationComponent,
1781
1904
  DownloadDocumentConfigurationComponent,
1782
1905
  DataMappingTreeComponent,
1906
+ ValueInputComponent,
1907
+ ScalarFieldComponent,
1908
+ ArrayFieldComponent,
1783
1909
  FieldTreeComponent,
1784
1910
  EpistolaDownloadComponent,
1785
1911
  EpistolaRetryFormComponent,
@@ -1801,6 +1927,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
1801
1927
  CheckJobStatusConfigurationComponent,
1802
1928
  DownloadDocumentConfigurationComponent,
1803
1929
  DataMappingTreeComponent,
1930
+ ValueInputComponent,
1931
+ ScalarFieldComponent,
1932
+ ArrayFieldComponent,
1804
1933
  FieldTreeComponent,
1805
1934
  EpistolaDownloadComponent,
1806
1935
  EpistolaRetryFormComponent,
@@ -1813,6 +1942,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
1813
1942
  CheckJobStatusConfigurationComponent,
1814
1943
  DownloadDocumentConfigurationComponent,
1815
1944
  DataMappingTreeComponent,
1945
+ ValueInputComponent,
1946
+ ScalarFieldComponent,
1947
+ ArrayFieldComponent,
1816
1948
  FieldTreeComponent,
1817
1949
  EpistolaDownloadComponent,
1818
1950
  EpistolaRetryFormComponent,
@@ -1870,9 +2002,12 @@ const epistolaPluginSpecification = {
1870
2002
  variantAttributes: 'Variant kenmerken',
1871
2003
  variantAttributesTooltip: 'Kenmerken voor automatische variant selectie. Waarden kunnen expressies zijn (doc:, pv:, case:).',
1872
2004
  attributeKey: 'Kenmerk',
2005
+ attributeKeyCustom: 'Aangepast...',
1873
2006
  attributeValue: 'Waarde',
1874
2007
  addAttribute: 'Kenmerk toevoegen',
1875
2008
  removeAttribute: 'Kenmerk verwijderen',
2009
+ attributeRequired: 'Verplicht',
2010
+ attributePreferred: 'Voorkeur',
1876
2011
  environmentId: 'Omgeving',
1877
2012
  environmentIdTooltip: 'Selecteer de doelomgeving (optioneel)',
1878
2013
  correlationId: 'Correlatie ID',
@@ -1956,9 +2091,12 @@ const epistolaPluginSpecification = {
1956
2091
  variantAttributes: 'Variant attributes',
1957
2092
  variantAttributesTooltip: 'Attributes for automatic variant selection. Values can be expressions (doc:, pv:, case:).',
1958
2093
  attributeKey: 'Attribute',
2094
+ attributeKeyCustom: 'Custom...',
1959
2095
  attributeValue: 'Value',
1960
2096
  addAttribute: 'Add attribute',
1961
2097
  removeAttribute: 'Remove attribute',
2098
+ attributeRequired: 'Required',
2099
+ attributePreferred: 'Preferred',
1962
2100
  environmentId: 'Environment',
1963
2101
  environmentIdTooltip: 'Select the target environment (optional)',
1964
2102
  correlationId: 'Correlation ID',
@@ -2019,66 +2157,6 @@ const epistolaPluginSpecification = {
2019
2157
  }
2020
2158
  };
2021
2159
 
2022
- const EPISTOLA_DOWNLOAD_OPTIONS = {
2023
- type: 'epistola-download',
2024
- selector: 'epistola-download-button',
2025
- title: 'Epistola Download',
2026
- group: 'basic',
2027
- icon: 'download',
2028
- emptyValue: null,
2029
- fieldOptions: ['filename', 'label'],
2030
- };
2031
- function registerEpistolaDownloadComponent(injector) {
2032
- if (!customElements.get(EPISTOLA_DOWNLOAD_OPTIONS.selector)) {
2033
- registerCustomFormioComponent(EPISTOLA_DOWNLOAD_OPTIONS, EpistolaDownloadComponent, injector);
2034
- }
2035
- }
2036
-
2037
- const EPISTOLA_RETRY_FORM_OPTIONS = {
2038
- type: 'epistola-retry-form',
2039
- selector: 'epistola-retry-form-element',
2040
- title: 'Epistola Retry Form',
2041
- group: 'basic',
2042
- icon: 'refresh',
2043
- emptyValue: null,
2044
- fieldOptions: ['sourceActivityId', 'label'], // sourceActivityId is optional (set via BPMN input parameter)
2045
- };
2046
- function registerEpistolaRetryFormComponent(injector) {
2047
- if (!customElements.get(EPISTOLA_RETRY_FORM_OPTIONS.selector)) {
2048
- registerCustomFormioComponent(EPISTOLA_RETRY_FORM_OPTIONS, EpistolaRetryFormComponent, injector);
2049
- }
2050
- }
2051
-
2052
- const EPISTOLA_PREVIEW_BUTTON_OPTIONS = {
2053
- type: 'epistola-preview-button',
2054
- selector: 'epistola-preview-button-element',
2055
- title: 'Epistola Preview',
2056
- group: 'basic',
2057
- icon: 'eye',
2058
- emptyValue: null,
2059
- fieldOptions: ['label'],
2060
- };
2061
- function registerEpistolaPreviewButtonComponent(injector) {
2062
- if (!customElements.get(EPISTOLA_PREVIEW_BUTTON_OPTIONS.selector)) {
2063
- registerCustomFormioComponent(EPISTOLA_PREVIEW_BUTTON_OPTIONS, EpistolaPreviewButtonComponent, injector);
2064
- }
2065
- }
2066
-
2067
- const EPISTOLA_DOCUMENT_PREVIEW_OPTIONS = {
2068
- type: 'epistola-document-preview',
2069
- selector: 'epistola-document-preview-element',
2070
- title: 'Epistola Document Preview',
2071
- group: 'basic',
2072
- icon: 'file-pdf-o',
2073
- emptyValue: null,
2074
- fieldOptions: ['label'],
2075
- };
2076
- function registerEpistolaDocumentPreviewComponent(injector) {
2077
- if (!customElements.get(EPISTOLA_DOCUMENT_PREVIEW_OPTIONS.selector)) {
2078
- registerCustomFormioComponent(EPISTOLA_DOCUMENT_PREVIEW_OPTIONS, EpistolaDocumentPreviewComponent, injector);
2079
- }
2080
- }
2081
-
2082
2160
  /*
2083
2161
  * Public API Surface of epistola plugin
2084
2162
  */
@@ -2087,5 +2165,5 @@ function registerEpistolaDocumentPreviewComponent(injector) {
2087
2165
  * Generated bundle index. Do not edit.
2088
2166
  */
2089
2167
 
2090
- export { 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, epistolaPluginSpecification, registerEpistolaDocumentPreviewComponent, registerEpistolaDownloadComponent, registerEpistolaPreviewButtonComponent, registerEpistolaRetryFormComponent };
2168
+ 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 };
2091
2169
  //# sourceMappingURL=epistola.app-valtimo-plugin.mjs.map