@epistola.app/valtimo-plugin 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/fesm2022/epistola.app-valtimo-plugin.mjs +1393 -0
  2. package/dist/fesm2022/epistola.app-valtimo-plugin.mjs.map +1 -0
  3. package/dist/index.d.ts +5 -0
  4. package/dist/lib/assets/epistola-logo.d.ts +1 -0
  5. package/dist/lib/assets/index.d.ts +1 -0
  6. package/dist/lib/components/check-job-status-configuration/check-job-status-configuration.component.d.ts +25 -0
  7. package/dist/lib/components/data-mapping-tree/data-mapping-tree.component.d.ts +33 -0
  8. package/dist/lib/components/download-document-configuration/download-document-configuration.component.d.ts +25 -0
  9. package/dist/lib/components/epistola-configuration/epistola-configuration.component.d.ts +29 -0
  10. package/dist/lib/components/epistola-download/epistola-download.component.d.ts +24 -0
  11. package/dist/lib/components/epistola-download/epistola-download.formio.d.ts +4 -0
  12. package/dist/lib/components/field-tree/field-tree.component.d.ts +75 -0
  13. package/dist/lib/components/generate-document-configuration/generate-document-configuration.component.d.ts +78 -0
  14. package/dist/lib/epistola.module.d.ts +17 -0
  15. package/dist/lib/epistola.specification.d.ts +3 -0
  16. package/dist/lib/models/config.d.ts +49 -0
  17. package/dist/lib/models/index.d.ts +2 -0
  18. package/dist/lib/models/template.d.ts +63 -0
  19. package/dist/lib/services/epistola-plugin.service.d.ts +42 -0
  20. package/dist/lib/services/index.d.ts +1 -0
  21. package/dist/public_api.d.ts +12 -0
  22. package/ng-package.json +17 -0
  23. package/package.json +38 -0
  24. package/src/lib/assets/epistola-logo.ts +4 -0
  25. package/src/lib/assets/index.ts +1 -0
  26. package/src/lib/components/check-job-status-configuration/check-job-status-configuration.component.html +51 -0
  27. package/src/lib/components/check-job-status-configuration/check-job-status-configuration.component.scss +1 -0
  28. package/src/lib/components/check-job-status-configuration/check-job-status-configuration.component.ts +71 -0
  29. package/src/lib/components/data-mapping-tree/data-mapping-tree.component.html +23 -0
  30. package/src/lib/components/data-mapping-tree/data-mapping-tree.component.scss +38 -0
  31. package/src/lib/components/data-mapping-tree/data-mapping-tree.component.ts +124 -0
  32. package/src/lib/components/download-document-configuration/download-document-configuration.component.html +29 -0
  33. package/src/lib/components/download-document-configuration/download-document-configuration.component.scss +1 -0
  34. package/src/lib/components/download-document-configuration/download-document-configuration.component.ts +71 -0
  35. package/src/lib/components/epistola-configuration/epistola-configuration.component.html +74 -0
  36. package/src/lib/components/epistola-configuration/epistola-configuration.component.scss +1 -0
  37. package/src/lib/components/epistola-configuration/epistola-configuration.component.ts +96 -0
  38. package/src/lib/components/epistola-download/epistola-download.component.ts +79 -0
  39. package/src/lib/components/epistola-download/epistola-download.formio.ts +19 -0
  40. package/src/lib/components/field-tree/field-tree.component.html +192 -0
  41. package/src/lib/components/field-tree/field-tree.component.scss +255 -0
  42. package/src/lib/components/field-tree/field-tree.component.ts +321 -0
  43. package/src/lib/components/generate-document-configuration/generate-document-configuration.component.html +182 -0
  44. package/src/lib/components/generate-document-configuration/generate-document-configuration.component.scss +150 -0
  45. package/src/lib/components/generate-document-configuration/generate-document-configuration.component.ts +422 -0
  46. package/src/lib/epistola.module.ts +50 -0
  47. package/src/lib/epistola.specification.ts +208 -0
  48. package/src/lib/models/config.ts +53 -0
  49. package/src/lib/models/index.ts +2 -0
  50. package/src/lib/models/template.ts +70 -0
  51. package/src/lib/services/epistola-plugin.service.ts +82 -0
  52. package/src/lib/services/index.ts +1 -0
  53. package/src/public_api.ts +16 -0
  54. package/tsconfig.lib.json +21 -0
@@ -0,0 +1,29 @@
1
+ <v-form
2
+ (valueChange)="formValueChange($event)"
3
+ *ngIf="{
4
+ disabled: safeDisabled$ | async,
5
+ prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null
6
+ } as obs"
7
+ >
8
+ <v-input
9
+ name="documentIdVariable"
10
+ [title]="'documentIdVariable' | pluginTranslate: pluginId | async"
11
+ [tooltip]="'documentIdVariableTooltip' | pluginTranslate: pluginId | async"
12
+ [margin]="true"
13
+ [defaultValue]="obs.prefill?.documentIdVariable || 'epistolaDocumentId'"
14
+ [disabled]="obs.disabled"
15
+ [required]="true"
16
+ >
17
+ </v-input>
18
+
19
+ <v-input
20
+ name="contentVariable"
21
+ [title]="'contentVariable' | pluginTranslate: pluginId | async"
22
+ [tooltip]="'contentVariableTooltip' | pluginTranslate: pluginId | async"
23
+ [margin]="true"
24
+ [defaultValue]="obs.prefill?.contentVariable || 'documentContent'"
25
+ [disabled]="obs.disabled"
26
+ [required]="true"
27
+ >
28
+ </v-input>
29
+ </v-form>
@@ -0,0 +1 @@
1
+ // Download document configuration styles
@@ -0,0 +1,71 @@
1
+ import {Component, EventEmitter, Input, OnDestroy, OnInit, Output} from '@angular/core';
2
+ import {CommonModule} from '@angular/common';
3
+ import {FunctionConfigurationComponent, PluginTranslatePipeModule} from '@valtimo/plugin';
4
+ import {FormModule, FormOutput, InputModule} from '@valtimo/components';
5
+ import {BehaviorSubject, combineLatest, Observable, Subscription, take} from 'rxjs';
6
+ import {delay, startWith} from 'rxjs/operators';
7
+ import {DownloadDocumentConfig} from '../../models';
8
+
9
+ @Component({
10
+ selector: 'epistola-download-document-configuration',
11
+ templateUrl: './download-document-configuration.component.html',
12
+ styleUrls: ['./download-document-configuration.component.scss'],
13
+ standalone: true,
14
+ imports: [CommonModule, PluginTranslatePipeModule, FormModule, InputModule]
15
+ })
16
+ export class DownloadDocumentConfigurationComponent
17
+ implements FunctionConfigurationComponent, OnInit, OnDestroy
18
+ {
19
+ @Input() save$!: Observable<void>;
20
+ @Input() disabled$!: Observable<boolean>;
21
+ @Input() pluginId!: string;
22
+ @Input() prefillConfiguration$!: Observable<DownloadDocumentConfig>;
23
+
24
+ @Output() valid: EventEmitter<boolean> = new EventEmitter<boolean>();
25
+ @Output() configuration: EventEmitter<DownloadDocumentConfig> = new EventEmitter<DownloadDocumentConfig>();
26
+
27
+ private saveSubscription!: Subscription;
28
+ private readonly formValue$ = new BehaviorSubject<DownloadDocumentConfig | null>(null);
29
+ private readonly valid$ = new BehaviorSubject<boolean>(false);
30
+
31
+ safeDisabled$!: Observable<boolean>;
32
+
33
+ ngOnInit(): void {
34
+ this.safeDisabled$ = this.disabled$.pipe(
35
+ startWith(true),
36
+ delay(0)
37
+ );
38
+ this.openSaveSubscription();
39
+ }
40
+
41
+ ngOnDestroy() {
42
+ this.saveSubscription?.unsubscribe();
43
+ }
44
+
45
+ formValueChange(formOutput: FormOutput): void {
46
+ const formValue = formOutput as unknown as DownloadDocumentConfig;
47
+ this.formValue$.next(formValue);
48
+ this.handleValid(formValue);
49
+ }
50
+
51
+ private handleValid(formValue: DownloadDocumentConfig): void {
52
+ const valid = !!(
53
+ formValue?.documentIdVariable &&
54
+ formValue?.contentVariable
55
+ );
56
+ this.valid$.next(valid);
57
+ this.valid.emit(valid);
58
+ }
59
+
60
+ private openSaveSubscription(): void {
61
+ this.saveSubscription = this.save$?.subscribe(() => {
62
+ combineLatest([this.formValue$, this.valid$])
63
+ .pipe(take(1))
64
+ .subscribe(([formValue, valid]) => {
65
+ if (valid && formValue) {
66
+ this.configuration.emit(formValue);
67
+ }
68
+ });
69
+ });
70
+ }
71
+ }
@@ -0,0 +1,74 @@
1
+ <v-form
2
+ (valueChange)="formValueChange($event)"
3
+ *ngIf="{
4
+ disabled: safeDisabled$ | async,
5
+ prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null
6
+ } as obs"
7
+ >
8
+ <v-input
9
+ name="configurationTitle"
10
+ [title]="'configurationTitle' | pluginTranslate: pluginId | async"
11
+ [margin]="true"
12
+ [defaultValue]="obs.prefill?.configurationTitle"
13
+ [disabled]="obs.disabled"
14
+ [required]="true"
15
+ >
16
+ </v-input>
17
+
18
+ <v-input
19
+ name="baseUrl"
20
+ [title]="'baseUrl' | pluginTranslate: pluginId | async"
21
+ [tooltip]="'baseUrlTooltip' | pluginTranslate: pluginId | async"
22
+ [margin]="true"
23
+ [defaultValue]="obs.prefill?.baseUrl"
24
+ [disabled]="obs.disabled"
25
+ [required]="true"
26
+ >
27
+ </v-input>
28
+
29
+ <v-input
30
+ name="apiKey"
31
+ [title]="'apiKey' | pluginTranslate: pluginId | async"
32
+ [tooltip]="'apiKeyTooltip' | pluginTranslate: pluginId | async"
33
+ [margin]="true"
34
+ [defaultValue]="obs.prefill?.apiKey"
35
+ [disabled]="obs.disabled"
36
+ [required]="true"
37
+ type="password"
38
+ >
39
+ </v-input>
40
+
41
+ <v-input
42
+ name="tenantId"
43
+ [title]="'tenantId' | pluginTranslate: pluginId | async"
44
+ [tooltip]="'tenantIdTooltip' | pluginTranslate: pluginId | async"
45
+ [margin]="true"
46
+ [defaultValue]="obs.prefill?.tenantId"
47
+ [disabled]="obs.disabled"
48
+ [required]="true"
49
+ >
50
+ </v-input>
51
+
52
+ <v-input
53
+ name="defaultEnvironmentId"
54
+ [title]="'defaultEnvironmentId' | pluginTranslate: pluginId | async"
55
+ [tooltip]="'defaultEnvironmentIdTooltip' | pluginTranslate: pluginId | async"
56
+ [margin]="true"
57
+ [defaultValue]="obs.prefill?.defaultEnvironmentId"
58
+ [disabled]="obs.disabled"
59
+ [required]="false"
60
+ >
61
+ </v-input>
62
+
63
+ <v-input
64
+ name="templateSyncEnabled"
65
+ type="checkbox"
66
+ [title]="'templateSyncEnabled' | pluginTranslate: pluginId | async"
67
+ [tooltip]="'templateSyncEnabledTooltip' | pluginTranslate: pluginId | async"
68
+ [margin]="true"
69
+ [defaultValue]="obs.prefill?.templateSyncEnabled ? 'true' : ''"
70
+ [disabled]="obs.disabled"
71
+ [required]="false"
72
+ >
73
+ </v-input>
74
+ </v-form>
@@ -0,0 +1 @@
1
+ // Epistola plugin configuration styles
@@ -0,0 +1,96 @@
1
+ import {Component, EventEmitter, Input, OnDestroy, OnInit, Output} from '@angular/core';
2
+ import {CommonModule} from '@angular/common';
3
+ import {PluginConfigurationComponent, PluginTranslatePipeModule} from '@valtimo/plugin';
4
+ import {FormModule, FormOutput, InputModule} from '@valtimo/components';
5
+ import {BehaviorSubject, combineLatest, Observable, Subscription, take} from 'rxjs';
6
+ import {delay, startWith} from 'rxjs/operators';
7
+ import {EpistolaPluginConfig} from '../../models';
8
+
9
+ @Component({
10
+ selector: 'epistola-configuration',
11
+ templateUrl: './epistola-configuration.component.html',
12
+ styleUrls: ['./epistola-configuration.component.scss'],
13
+ standalone: true,
14
+ imports: [CommonModule, PluginTranslatePipeModule, FormModule, InputModule]
15
+ })
16
+ export class EpistolaConfigurationComponent
17
+ implements PluginConfigurationComponent, OnInit, OnDestroy
18
+ {
19
+ @Input() save$!: Observable<void>;
20
+ @Input() disabled$!: Observable<boolean>;
21
+ @Input() pluginId!: string;
22
+ @Input() prefillConfiguration$!: Observable<EpistolaPluginConfig>;
23
+
24
+ @Output() valid: EventEmitter<boolean> = new EventEmitter<boolean>();
25
+ @Output() configuration: EventEmitter<EpistolaPluginConfig> = new EventEmitter<EpistolaPluginConfig>();
26
+
27
+ /** Epistola slug pattern: lowercase alphanumeric with hyphens, no leading/trailing hyphens. */
28
+ private static readonly SLUG_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
29
+
30
+ private saveSubscription!: Subscription;
31
+ private readonly formValue$ = new BehaviorSubject<EpistolaPluginConfig | null>(null);
32
+ private readonly valid$ = new BehaviorSubject<boolean>(false);
33
+
34
+ safeDisabled$!: Observable<boolean>;
35
+
36
+ ngOnInit(): void {
37
+ // Wrap disabled$ with startWith and delay to prevent NG0100 ExpressionChangedAfterItHasBeenCheckedError
38
+ // The disabled$ observable from Valtimo's plugin framework can emit value changes after Angular's
39
+ // change detection cycle completes, causing the error.
40
+ this.safeDisabled$ = this.disabled$.pipe(
41
+ startWith(true),
42
+ delay(0)
43
+ );
44
+ this.openSaveSubscription();
45
+ }
46
+
47
+ ngOnDestroy() {
48
+ this.saveSubscription?.unsubscribe();
49
+ }
50
+
51
+ formValueChange(formOutput: FormOutput): void {
52
+ const formValue = formOutput as unknown as EpistolaPluginConfig;
53
+ this.formValue$.next(formValue);
54
+ this.handleValid(formValue);
55
+ }
56
+
57
+ private handleValid(formValue: EpistolaPluginConfig): void {
58
+ const valid = !!(
59
+ formValue?.configurationTitle &&
60
+ formValue?.baseUrl &&
61
+ formValue?.apiKey &&
62
+ formValue?.tenantId &&
63
+ this.isValidSlug(formValue.tenantId, 3, 63) &&
64
+ this.isValidOptionalSlug(formValue.defaultEnvironmentId, 3, 30)
65
+ );
66
+ this.valid$.next(valid);
67
+ this.valid.emit(valid);
68
+ }
69
+
70
+ private isValidSlug(value: string, minLength: number, maxLength: number): boolean {
71
+ return (
72
+ value.length >= minLength &&
73
+ value.length <= maxLength &&
74
+ EpistolaConfigurationComponent.SLUG_PATTERN.test(value)
75
+ );
76
+ }
77
+
78
+ private isValidOptionalSlug(value: string | undefined, minLength: number, maxLength: number): boolean {
79
+ if (!value) {
80
+ return true;
81
+ }
82
+ return this.isValidSlug(value, minLength, maxLength);
83
+ }
84
+
85
+ private openSaveSubscription(): void {
86
+ this.saveSubscription = this.save$?.subscribe(() => {
87
+ combineLatest([this.formValue$, this.valid$])
88
+ .pipe(take(1))
89
+ .subscribe(([formValue, valid]) => {
90
+ if (valid) {
91
+ this.configuration.emit(formValue);
92
+ }
93
+ });
94
+ });
95
+ }
96
+ }
@@ -0,0 +1,79 @@
1
+ import {Component, EventEmitter, Input, Output} from '@angular/core';
2
+ import {CommonModule} from '@angular/common';
3
+ import {HttpClient} from '@angular/common/http';
4
+ import {FormioCustomComponent} from '@valtimo/components';
5
+
6
+ export interface DownloadData {
7
+ documentId: string;
8
+ tenantId: string;
9
+ }
10
+
11
+ @Component({
12
+ standalone: true,
13
+ imports: [CommonModule],
14
+ selector: 'epistola-download-component',
15
+ template: `
16
+ <button
17
+ type="button"
18
+ class="btn btn-outline-primary"
19
+ [disabled]="disabled || downloading || !hasRequiredData()"
20
+ (click)="download()"
21
+ >
22
+ <i class="mdi mdi-download mr-1"></i>
23
+ {{ downloading ? 'Downloading...' : buttonLabel }}
24
+ </button>
25
+ <span *ngIf="error" class="text-danger ml-2">{{ error }}</span>
26
+ `,
27
+ })
28
+ export class EpistolaDownloadComponent implements FormioCustomComponent<DownloadData> {
29
+ @Input() value: DownloadData;
30
+ @Output() valueChange = new EventEmitter<DownloadData>();
31
+
32
+ @Input() disabled = false;
33
+ @Input() filename = 'document.pdf';
34
+ @Input() label = 'Download PDF';
35
+
36
+ downloading = false;
37
+ error: string | null = null;
38
+
39
+ get buttonLabel(): string {
40
+ return this.label || 'Download PDF';
41
+ }
42
+
43
+ constructor(private readonly http: HttpClient) {}
44
+
45
+ hasRequiredData(): boolean {
46
+ return !!(this.value?.documentId && this.value?.tenantId);
47
+ }
48
+
49
+ download(): void {
50
+ if (!this.hasRequiredData() || this.downloading) {
51
+ return;
52
+ }
53
+
54
+ this.downloading = true;
55
+ this.error = null;
56
+
57
+ const {documentId, tenantId} = this.value;
58
+ const url = `/api/v1/plugin/epistola/documents/${encodeURIComponent(documentId)}/download`
59
+ + `?tenantId=${encodeURIComponent(tenantId)}`
60
+ + `&filename=${encodeURIComponent(this.filename)}`;
61
+
62
+ this.http.get(url, {responseType: 'blob'}).subscribe({
63
+ next: (blob) => {
64
+ const objectUrl = URL.createObjectURL(blob);
65
+ const anchor = document.createElement('a');
66
+ anchor.href = objectUrl;
67
+ anchor.download = this.filename;
68
+ anchor.click();
69
+ URL.revokeObjectURL(objectUrl);
70
+ this.downloading = false;
71
+ },
72
+ error: (err) => {
73
+ console.error('Download failed', err);
74
+ this.error = 'Download mislukt. Probeer opnieuw.';
75
+ this.downloading = false;
76
+ },
77
+ });
78
+ }
79
+ }
@@ -0,0 +1,19 @@
1
+ import {Injector} from '@angular/core';
2
+ import {FormioCustomComponentInfo, registerCustomFormioComponent} from '@valtimo/components';
3
+ import {EpistolaDownloadComponent} from './epistola-download.component';
4
+
5
+ export const EPISTOLA_DOWNLOAD_OPTIONS: FormioCustomComponentInfo = {
6
+ type: 'epistola-download',
7
+ selector: 'epistola-download-button',
8
+ title: 'Epistola Download',
9
+ group: 'basic',
10
+ icon: 'download',
11
+ emptyValue: null,
12
+ fieldOptions: ['filename', 'label'],
13
+ };
14
+
15
+ export function registerEpistolaDownloadComponent(injector: Injector): void {
16
+ if (!customElements.get(EPISTOLA_DOWNLOAD_OPTIONS.selector)) {
17
+ registerCustomFormioComponent(EPISTOLA_DOWNLOAD_OPTIONS, EpistolaDownloadComponent, injector);
18
+ }
19
+ }
@@ -0,0 +1,192 @@
1
+ <!-- SCALAR field: label row + input with 3-mode selector -->
2
+ <div *ngIf="field.fieldType === 'SCALAR'" class="field-row" [class.field-required-unmapped]="field.required && !getStringValue()">
3
+ <div class="field-label">
4
+ <span class="field-name">{{ field.name }}</span>
5
+ <span class="field-meta">({{ field.type }}{{ field.required ? ', required' : '' }})</span>
6
+ </div>
7
+ <div class="field-input">
8
+ <div class="input-mode-group">
9
+ <button type="button" class="mode-btn" [class.mode-active]="inputMode === 'browse'" [disabled]="disabled" (click)="setInputMode('browse')" [title]="'browseMode' | pluginTranslate: pluginId | async">⊞</button>
10
+ <button type="button" class="mode-btn" [class.mode-active]="inputMode === 'pv'" [disabled]="disabled" (click)="setInputMode('pv')" [title]="'pvMode' | pluginTranslate: pluginId | async">pv</button>
11
+ <button type="button" class="mode-btn" [class.mode-active]="inputMode === 'expression'" [disabled]="disabled" (click)="setInputMode('expression')" [title]="'expressionMode' | pluginTranslate: pluginId | async">fx</button>
12
+ </div>
13
+
14
+ <!-- Browse mode: ValuePathSelector -->
15
+ <div class="field-input-control" *ngIf="inputMode === 'browse'">
16
+ <valtimo-value-path-selector
17
+ [name]="'field_' + field.path"
18
+ [caseDefinitionKey]="caseDefinitionKey"
19
+ [prefixes]="[ValuePathSelectorPrefix.DOC, ValuePathSelectorPrefix.CASE]"
20
+ [notation]="'dots'"
21
+ [disabled]="disabled"
22
+ [defaultValue]="getStringValue()"
23
+ [showCaseDefinitionSelector]="!caseDefinitionKey"
24
+ (valueChangeEvent)="onBrowseValueChange($event)"
25
+ ></valtimo-value-path-selector>
26
+ </div>
27
+
28
+ <!-- PV mode: dropdown (when available) or text fallback -->
29
+ <div class="field-input-control" *ngIf="inputMode === 'pv'">
30
+ <select
31
+ *ngIf="processVariables.length > 0; else pvFallback"
32
+ class="pv-select"
33
+ [disabled]="disabled"
34
+ (change)="onPvChange($any($event.target).value)"
35
+ >
36
+ <option value="">{{ 'pvPlaceholder' | pluginTranslate: pluginId | async }}</option>
37
+ <option *ngFor="let pv of processVariables" [value]="pv" [selected]="pv === getPvName()">{{ pv }}</option>
38
+ </select>
39
+ <ng-template #pvFallback>
40
+ <v-input
41
+ [name]="'pvfb_' + field.path"
42
+ [defaultValue]="getPvName()"
43
+ [disabled]="disabled"
44
+ [placeholder]="'pvPlaceholder' | pluginTranslate: pluginId | async"
45
+ (valueChange)="onPvChange($event)"
46
+ ></v-input>
47
+ </ng-template>
48
+ </div>
49
+
50
+ <!-- Expression mode: text input -->
51
+ <div class="field-input-control" *ngIf="inputMode === 'expression'">
52
+ <v-input
53
+ [name]="'fx_' + field.path"
54
+ [defaultValue]="getStringValue()"
55
+ [disabled]="disabled"
56
+ [placeholder]="'e.g. pv:variableName or doc:path.to.field'"
57
+ (valueChange)="onExpressionValueChange($event)"
58
+ ></v-input>
59
+ </div>
60
+ </div>
61
+ </div>
62
+
63
+ <!-- OBJECT field: collapsible section with children -->
64
+ <div *ngIf="field.fieldType === 'OBJECT'" class="field-object">
65
+ <div class="field-object-header" (click)="toggleExpanded()" [class.field-required-unmapped]="totalRequired > 0 && mappedCount < totalRequired">
66
+ <span class="expand-icon">{{ expanded ? '▼' : '▶' }}</span>
67
+ <span class="field-name">{{ field.name }}</span>
68
+ <span class="field-meta">(object{{ field.required ? ', required' : '' }})</span>
69
+ <span class="completeness-badge" *ngIf="totalRequired > 0 && !expanded">
70
+ {{ mappedCount }}/{{ totalRequired }}
71
+ </span>
72
+ </div>
73
+ <div class="field-object-children" *ngIf="expanded">
74
+ <epistola-field-tree
75
+ *ngFor="let child of field.children"
76
+ [field]="child"
77
+ [value]="getChildValue(child.name)"
78
+ [pluginId]="pluginId"
79
+ [caseDefinitionKey]="caseDefinitionKey"
80
+ [processVariables]="processVariables"
81
+ [disabled]="disabled"
82
+ (valueChange)="onChildChange(child.name, $event)"
83
+ ></epistola-field-tree>
84
+ </div>
85
+ </div>
86
+
87
+ <!-- ARRAY field: collapsible section with source collection + optional per-item mapping -->
88
+ <div *ngIf="field.fieldType === 'ARRAY'" class="field-array">
89
+ <div class="field-array-header" (click)="toggleExpanded()" [class.field-required-unmapped]="field.required && !getSourceValue()">
90
+ <span class="expand-icon">{{ expanded ? '▼' : '▶' }}</span>
91
+ <span class="field-name">{{ field.name }}</span>
92
+ <span class="field-meta">(array{{ field.required ? ', required' : '' }})</span>
93
+ <span class="completeness-badge" *ngIf="totalRequired > 0 && !expanded">
94
+ {{ mappedCount }}/{{ totalRequired }}
95
+ </span>
96
+ <span class="mapped-indicator" *ngIf="totalRequired === 0 && getSourceValue() && !expanded">✓</span>
97
+ </div>
98
+ <div class="field-array-content" *ngIf="expanded">
99
+ <!-- Source collection input -->
100
+ <div class="field-row">
101
+ <div class="field-label">
102
+ <span class="field-name">{{ 'mapCollectionTo' | pluginTranslate: pluginId | async }}</span>
103
+ </div>
104
+ <div class="field-input">
105
+ <div class="input-mode-group">
106
+ <button type="button" class="mode-btn" [class.mode-active]="inputMode === 'browse'" [disabled]="disabled" (click)="setInputMode('browse')" [title]="'browseMode' | pluginTranslate: pluginId | async">⊞</button>
107
+ <button type="button" class="mode-btn" [class.mode-active]="inputMode === 'pv'" [disabled]="disabled" (click)="setInputMode('pv')" [title]="'pvMode' | pluginTranslate: pluginId | async">pv</button>
108
+ <button type="button" class="mode-btn" [class.mode-active]="inputMode === 'expression'" [disabled]="disabled" (click)="setInputMode('expression')" [title]="'expressionMode' | pluginTranslate: pluginId | async">fx</button>
109
+ </div>
110
+
111
+ <div class="field-input-control" *ngIf="inputMode === 'browse'">
112
+ <valtimo-value-path-selector
113
+ [name]="'field_' + field.path"
114
+ [caseDefinitionKey]="caseDefinitionKey"
115
+ [prefixes]="[ValuePathSelectorPrefix.DOC, ValuePathSelectorPrefix.CASE]"
116
+ [notation]="'dots'"
117
+ [disabled]="disabled"
118
+ [defaultValue]="getSourceValue()"
119
+ [showCaseDefinitionSelector]="!caseDefinitionKey"
120
+ (valueChangeEvent)="onSourceBrowseChange($event)"
121
+ ></valtimo-value-path-selector>
122
+ </div>
123
+
124
+ <div class="field-input-control" *ngIf="inputMode === 'pv'">
125
+ <select
126
+ *ngIf="processVariables.length > 0; else pvSourceFallback"
127
+ class="pv-select"
128
+ [disabled]="disabled"
129
+ (change)="onSourcePvChange($any($event.target).value)"
130
+ >
131
+ <option value="">{{ 'pvPlaceholder' | pluginTranslate: pluginId | async }}</option>
132
+ <option *ngFor="let pv of processVariables" [value]="pv" [selected]="pv === getSourcePvName()">{{ pv }}</option>
133
+ </select>
134
+ <ng-template #pvSourceFallback>
135
+ <v-input
136
+ [name]="'pvfb_' + field.path"
137
+ [defaultValue]="getSourcePvName()"
138
+ [disabled]="disabled"
139
+ [placeholder]="'pvPlaceholder' | pluginTranslate: pluginId | async"
140
+ (valueChange)="onSourcePvChange($event)"
141
+ ></v-input>
142
+ </ng-template>
143
+ </div>
144
+
145
+ <div class="field-input-control" *ngIf="inputMode === 'expression'">
146
+ <v-input
147
+ [name]="'fx_' + field.path"
148
+ [defaultValue]="getSourceValue()"
149
+ [disabled]="disabled"
150
+ [placeholder]="'e.g. pv:variableName or doc:path.to.field'"
151
+ (valueChange)="onSourceExpressionChange($event)"
152
+ ></v-input>
153
+ </div>
154
+ </div>
155
+ </div>
156
+
157
+ <!-- Per-item field mapping toggle (only shown when array has children) -->
158
+ <div class="array-per-field-toggle" *ngIf="hasArrayChildren()">
159
+ <label class="toggle-label">
160
+ <input
161
+ type="checkbox"
162
+ [checked]="arrayPerFieldMode"
163
+ [disabled]="disabled"
164
+ (change)="toggleArrayPerFieldMode()"
165
+ />
166
+ <span>{{ 'itemFieldMapping' | pluginTranslate: pluginId | async }}</span>
167
+ </label>
168
+ </div>
169
+
170
+ <!-- Per-item field mappings -->
171
+ <div class="array-item-fields" *ngIf="arrayPerFieldMode && hasArrayChildren()">
172
+ <div class="item-fields-header">
173
+ <span class="item-fields-title">{{ 'itemFieldMappingTitle' | pluginTranslate: pluginId | async }}</span>
174
+ </div>
175
+ <div class="item-field-row" *ngFor="let child of field.children">
176
+ <div class="item-field-label">
177
+ <span class="field-name">{{ child.name }}</span>
178
+ <span class="field-meta">({{ child.type }}{{ child.required ? ', required' : '' }})</span>
179
+ </div>
180
+ <div class="item-field-input">
181
+ <v-input
182
+ [name]="'itemField_' + child.path"
183
+ [defaultValue]="getItemFieldValue(child.name)"
184
+ [disabled]="disabled"
185
+ [placeholder]="'sourceFieldPlaceholder' | pluginTranslate: pluginId | async"
186
+ (valueChange)="onItemFieldChange(child.name, $event)"
187
+ ></v-input>
188
+ </div>
189
+ </div>
190
+ </div>
191
+ </div>
192
+ </div>