@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,255 @@
1
+ // -- Scalar field row --
2
+ .field-row {
3
+ display: flex;
4
+ align-items: flex-start;
5
+ gap: 0.75rem;
6
+ padding: 0.5rem 0;
7
+ border-left: 3px solid transparent;
8
+
9
+ &.field-required-unmapped {
10
+ border-left-color: #dc3545;
11
+ background-color: #fff5f5;
12
+ padding-left: 0.5rem;
13
+ }
14
+ }
15
+
16
+ .field-label {
17
+ flex: 0 0 200px;
18
+ min-width: 140px;
19
+ padding-top: 0.5rem;
20
+ word-break: break-word;
21
+
22
+ .field-name {
23
+ font-weight: 500;
24
+ }
25
+
26
+ .field-meta {
27
+ font-size: 0.8125rem;
28
+ color: #6c757d;
29
+ margin-left: 0.25rem;
30
+ }
31
+ }
32
+
33
+ .field-input {
34
+ flex: 1;
35
+ display: flex;
36
+ align-items: flex-start;
37
+ gap: 0.5rem;
38
+ min-width: 0;
39
+ }
40
+
41
+ .field-input-control {
42
+ flex: 1;
43
+ min-width: 0;
44
+ }
45
+
46
+ // -- 3-button input mode group --
47
+ .input-mode-group {
48
+ flex: 0 0 auto;
49
+ display: flex;
50
+ margin-top: 0.25rem;
51
+ border: 1px solid #c6c6c6;
52
+ border-radius: 4px;
53
+ overflow: hidden;
54
+
55
+ .mode-btn {
56
+ width: 32px;
57
+ height: 32px;
58
+ padding: 0;
59
+ border: none;
60
+ border-right: 1px solid #c6c6c6;
61
+ background: #f4f4f4;
62
+ color: #525252;
63
+ font-size: 0.75rem;
64
+ font-weight: 600;
65
+ cursor: pointer;
66
+ display: flex;
67
+ align-items: center;
68
+ justify-content: center;
69
+ transition: background-color 0.15s;
70
+
71
+ &:last-child {
72
+ border-right: none;
73
+ }
74
+
75
+ &:hover:not(:disabled) {
76
+ background: #e0e0e0;
77
+ }
78
+
79
+ &.mode-active {
80
+ background: #0f62fe;
81
+ color: #fff;
82
+
83
+ &:hover:not(:disabled) {
84
+ background: #0353e9;
85
+ }
86
+ }
87
+
88
+ &:disabled {
89
+ opacity: 0.5;
90
+ cursor: not-allowed;
91
+ }
92
+ }
93
+ }
94
+
95
+ // -- PV select dropdown --
96
+ .pv-select {
97
+ width: 100%;
98
+ height: 2.5rem;
99
+ padding: 0 0.75rem;
100
+ border: 1px solid #c6c6c6;
101
+ border-radius: 4px;
102
+ background: #fff;
103
+ color: #161616;
104
+ font-size: 0.875rem;
105
+ cursor: pointer;
106
+
107
+ &:focus {
108
+ outline: 2px solid #0f62fe;
109
+ outline-offset: -2px;
110
+ }
111
+
112
+ &:disabled {
113
+ opacity: 0.5;
114
+ cursor: not-allowed;
115
+ background: #f4f4f4;
116
+ }
117
+ }
118
+
119
+ // -- Object section --
120
+ .field-object {
121
+ margin: 0.25rem 0;
122
+ }
123
+
124
+ .field-object-header,
125
+ .field-array-header {
126
+ display: flex;
127
+ align-items: center;
128
+ gap: 0.5rem;
129
+ padding: 0.5rem 0.25rem;
130
+ cursor: pointer;
131
+ user-select: none;
132
+ border-left: 3px solid transparent;
133
+ border-radius: 2px;
134
+
135
+ &:hover {
136
+ background: #f4f4f4;
137
+ }
138
+
139
+ &.field-required-unmapped {
140
+ border-left-color: #dc3545;
141
+ background-color: #fff5f5;
142
+ }
143
+
144
+ .expand-icon {
145
+ flex: 0 0 1rem;
146
+ font-size: 0.75rem;
147
+ color: #525252;
148
+ }
149
+
150
+ .field-name {
151
+ font-weight: 500;
152
+ }
153
+
154
+ .field-meta {
155
+ font-size: 0.8125rem;
156
+ color: #6c757d;
157
+ }
158
+
159
+ .completeness-badge {
160
+ margin-left: auto;
161
+ font-size: 0.75rem;
162
+ padding: 0.125rem 0.5rem;
163
+ border-radius: 10px;
164
+ background: #e0e0e0;
165
+ color: #525252;
166
+ font-weight: 500;
167
+ }
168
+
169
+ .mapped-indicator {
170
+ margin-left: auto;
171
+ color: #198754;
172
+ font-weight: 600;
173
+ }
174
+ }
175
+
176
+ .field-object-children {
177
+ padding-left: 1.25rem;
178
+ border-left: 1px solid #e0e0e0;
179
+ margin-left: 0.5rem;
180
+ }
181
+
182
+ // -- Array section --
183
+ .field-array {
184
+ margin: 0.25rem 0;
185
+ }
186
+
187
+ .field-array-content {
188
+ padding-left: 1.25rem;
189
+ border-left: 1px solid #e0e0e0;
190
+ margin-left: 0.5rem;
191
+ }
192
+
193
+ // -- Array per-item field mapping --
194
+ .array-per-field-toggle {
195
+ padding: 0.5rem 0;
196
+
197
+ .toggle-label {
198
+ display: flex;
199
+ align-items: center;
200
+ gap: 0.5rem;
201
+ cursor: pointer;
202
+ font-size: 0.875rem;
203
+ color: #525252;
204
+
205
+ input[type="checkbox"] {
206
+ cursor: pointer;
207
+ }
208
+ }
209
+ }
210
+
211
+ .array-item-fields {
212
+ padding: 0.25rem 0 0.5rem;
213
+ margin-left: 0.5rem;
214
+ border-left: 1px dashed #c6c6c6;
215
+ padding-left: 1rem;
216
+ }
217
+
218
+ .item-fields-header {
219
+ padding-bottom: 0.25rem;
220
+
221
+ .item-fields-title {
222
+ font-size: 0.8125rem;
223
+ font-weight: 500;
224
+ color: #6c757d;
225
+ }
226
+ }
227
+
228
+ .item-field-row {
229
+ display: flex;
230
+ align-items: center;
231
+ gap: 0.75rem;
232
+ padding: 0.25rem 0;
233
+ }
234
+
235
+ .item-field-label {
236
+ flex: 0 0 180px;
237
+ min-width: 120px;
238
+ word-break: break-word;
239
+
240
+ .field-name {
241
+ font-weight: 500;
242
+ font-size: 0.875rem;
243
+ }
244
+
245
+ .field-meta {
246
+ font-size: 0.75rem;
247
+ color: #6c757d;
248
+ margin-left: 0.25rem;
249
+ }
250
+ }
251
+
252
+ .item-field-input {
253
+ flex: 1;
254
+ min-width: 0;
255
+ }
@@ -0,0 +1,321 @@
1
+ import {Component, EventEmitter, Input, OnChanges, Output, SimpleChanges} from '@angular/core';
2
+ import {CommonModule} from '@angular/common';
3
+ import {FormsModule} from '@angular/forms';
4
+ import {PluginTranslatePipeModule} from '@valtimo/plugin';
5
+ import {InputModule, ValuePathSelectorComponent, ValuePathSelectorPrefix} from '@valtimo/components';
6
+ import {TemplateField} from '../../models';
7
+
8
+ export type InputMode = 'browse' | 'pv' | 'expression';
9
+
10
+ /**
11
+ * Recursive component that renders a single TemplateField node as part of a tree form.
12
+ *
13
+ * - SCALAR fields render as a label + input row with a 3-mode selector (browse / pv / expression)
14
+ * - OBJECT fields render as a collapsible section with children indented inside
15
+ * - ARRAY fields render as a collapsible section with source collection input and optional per-item field mappings
16
+ *
17
+ * Input modes:
18
+ * - Browse (⊞): ValuePathSelector for doc:/case: paths
19
+ * - PV (pv): Dropdown of discovered process variables (text fallback when none found)
20
+ * - Expression (fx): Free-text input for manual expressions
21
+ */
22
+ @Component({
23
+ selector: 'epistola-field-tree',
24
+ templateUrl: './field-tree.component.html',
25
+ styleUrls: ['./field-tree.component.scss'],
26
+ standalone: true,
27
+ imports: [
28
+ CommonModule,
29
+ FormsModule,
30
+ PluginTranslatePipeModule,
31
+ InputModule,
32
+ ValuePathSelectorComponent
33
+ ]
34
+ })
35
+ export class FieldTreeComponent implements OnChanges {
36
+ @Input() field!: TemplateField;
37
+ @Input() value: any = undefined;
38
+ @Input() pluginId!: string;
39
+ @Input() caseDefinitionKey: string | null = null;
40
+ @Input() processVariables: string[] = [];
41
+ @Input() disabled = false;
42
+
43
+ @Output() valueChange = new EventEmitter<any>();
44
+
45
+ readonly ValuePathSelectorPrefix = ValuePathSelectorPrefix;
46
+
47
+ inputMode: InputMode = 'browse';
48
+ expanded = false;
49
+
50
+ /** For ARRAY fields: whether to show per-item field mapping */
51
+ arrayPerFieldMode = false;
52
+
53
+ /** Completeness badge for collapsed object/array sections */
54
+ mappedCount = 0;
55
+ totalRequired = 0;
56
+
57
+ ngOnChanges(changes: SimpleChanges): void {
58
+ if (changes['value'] || changes['field']) {
59
+ this.updateCompleteness();
60
+ // Auto-expand if there are unmapped required children
61
+ if (!this.expanded && this.totalRequired > 0 && this.mappedCount < this.totalRequired) {
62
+ this.expanded = true;
63
+ }
64
+ }
65
+ // Detect input mode from prefill value
66
+ if (changes['value'] && this.value != null) {
67
+ if (this.field?.fieldType === 'SCALAR') {
68
+ this.inputMode = this.detectInputMode(this.value);
69
+ } else if (this.field?.fieldType === 'ARRAY') {
70
+ const sourceValue = this.getSourceValue();
71
+ if (sourceValue) {
72
+ this.inputMode = this.detectInputMode(sourceValue);
73
+ }
74
+ // Detect per-field mode from value shape
75
+ if (typeof this.value === 'object' && this.value !== null && '_source' in this.value) {
76
+ this.arrayPerFieldMode = true;
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ toggleExpanded(): void {
83
+ this.expanded = !this.expanded;
84
+ }
85
+
86
+ setInputMode(mode: InputMode): void {
87
+ this.inputMode = mode;
88
+ }
89
+
90
+ /** Handle value change from ValuePathSelector (browse mode) */
91
+ onBrowseValueChange(newValue: string): void {
92
+ this.emitScalarValue(newValue);
93
+ }
94
+
95
+ /** Handle value change from PV dropdown */
96
+ onPvChange(newValue: string): void {
97
+ if (newValue) {
98
+ this.emitScalarValue('pv:' + newValue);
99
+ } else {
100
+ this.emitScalarValue('');
101
+ }
102
+ }
103
+
104
+ /** Handle value change from text input (expression mode) */
105
+ onExpressionValueChange(newValue: string): void {
106
+ this.emitScalarValue(newValue);
107
+ }
108
+
109
+ /** Handle child value change for OBJECT fields */
110
+ onChildChange(childName: string, childValue: any): void {
111
+ const current = (typeof this.value === 'object' && this.value !== null) ? {...this.value} : {};
112
+ if (childValue === undefined || childValue === null || childValue === '') {
113
+ delete current[childName];
114
+ } else {
115
+ current[childName] = childValue;
116
+ }
117
+ this.valueChange.emit(Object.keys(current).length > 0 ? current : undefined);
118
+ }
119
+
120
+ /** Get the current value for a child field within an OBJECT */
121
+ getChildValue(childName: string): any {
122
+ if (typeof this.value === 'object' && this.value !== null) {
123
+ return this.value[childName];
124
+ }
125
+ return undefined;
126
+ }
127
+
128
+ /** Get the string value for display (for SCALAR leaf inputs and direct array mode) */
129
+ getStringValue(): string {
130
+ return typeof this.value === 'string' ? this.value : '';
131
+ }
132
+
133
+ /** Get the PV name from a pv: prefixed value (for PV dropdown default selection) */
134
+ getPvName(): string {
135
+ const str = this.getStringValue();
136
+ return str.startsWith('pv:') ? str.substring(3) : '';
137
+ }
138
+
139
+ // --- ARRAY-specific methods ---
140
+
141
+ /** Get the source collection value (works for both direct string and _source format) */
142
+ getSourceValue(): string {
143
+ if (typeof this.value === 'string') {
144
+ return this.value;
145
+ }
146
+ if (typeof this.value === 'object' && this.value !== null && '_source' in this.value) {
147
+ return this.value['_source'] || '';
148
+ }
149
+ return '';
150
+ }
151
+
152
+ /** Get the PV name from the source value */
153
+ getSourcePvName(): string {
154
+ const source = this.getSourceValue();
155
+ return source.startsWith('pv:') ? source.substring(3) : '';
156
+ }
157
+
158
+ toggleArrayPerFieldMode(): void {
159
+ this.arrayPerFieldMode = !this.arrayPerFieldMode;
160
+
161
+ if (this.arrayPerFieldMode) {
162
+ // Switch from direct to per-field: convert string value to _source object
163
+ const currentSource = this.getSourceValue();
164
+ const obj: Record<string, any> = {_source: currentSource};
165
+ this.valueChange.emit(obj);
166
+ } else {
167
+ // Switch from per-field to direct: extract _source as string value
168
+ const source = this.getSourceValue();
169
+ this.valueChange.emit(source || undefined);
170
+ }
171
+ }
172
+
173
+ /** Handle source collection value change (used in ARRAY mode) */
174
+ onSourceBrowseChange(newValue: string): void {
175
+ this.updateSourceValue(newValue);
176
+ }
177
+
178
+ onSourcePvChange(newValue: string): void {
179
+ this.updateSourceValue(newValue ? 'pv:' + newValue : '');
180
+ }
181
+
182
+ onSourceExpressionChange(newValue: string): void {
183
+ this.updateSourceValue(newValue);
184
+ }
185
+
186
+ /** Handle per-item field mapping change */
187
+ onItemFieldChange(childName: string, sourceFieldName: string): void {
188
+ const current = (typeof this.value === 'object' && this.value !== null) ? {...this.value} : {_source: ''};
189
+ if (sourceFieldName && sourceFieldName.trim().length > 0) {
190
+ current[childName] = sourceFieldName;
191
+ } else {
192
+ delete current[childName];
193
+ }
194
+ this.valueChange.emit(current);
195
+ }
196
+
197
+ /** Get the current source field name mapping for a child */
198
+ getItemFieldValue(childName: string): string {
199
+ if (typeof this.value === 'object' && this.value !== null) {
200
+ return this.value[childName] || '';
201
+ }
202
+ return '';
203
+ }
204
+
205
+ /** Check if the array has any children that can be mapped per-item */
206
+ hasArrayChildren(): boolean {
207
+ return !!(this.field?.children && this.field.children.length > 0);
208
+ }
209
+
210
+ private emitScalarValue(newValue: string): void {
211
+ this.valueChange.emit(newValue || undefined);
212
+ }
213
+
214
+ private updateSourceValue(newValue: string): void {
215
+ if (this.arrayPerFieldMode) {
216
+ const current = (typeof this.value === 'object' && this.value !== null) ? {...this.value} : {};
217
+ current['_source'] = newValue || '';
218
+ this.valueChange.emit(current);
219
+ } else {
220
+ this.valueChange.emit(newValue || undefined);
221
+ }
222
+ }
223
+
224
+ private updateCompleteness(): void {
225
+ if (this.field?.fieldType === 'OBJECT' && this.field.children) {
226
+ const stats = this.countRequiredMapped(this.field.children, this.value || {});
227
+ this.mappedCount = stats.mapped;
228
+ this.totalRequired = stats.total;
229
+ } else if (this.field?.fieldType === 'ARRAY') {
230
+ this.updateArrayCompleteness();
231
+ }
232
+ }
233
+
234
+ private updateArrayCompleteness(): void {
235
+ if (!this.field?.children || this.field.children.length === 0) {
236
+ // No children: just check if source is set
237
+ this.totalRequired = this.field?.required ? 1 : 0;
238
+ this.mappedCount = this.getSourceValue() ? (this.field?.required ? 1 : 0) : 0;
239
+ return;
240
+ }
241
+
242
+ if (typeof this.value === 'object' && this.value !== null && '_source' in this.value) {
243
+ // Per-field mode: count source + required children
244
+ let total = 0;
245
+ let mapped = 0;
246
+
247
+ // Source counts as 1 required
248
+ if (this.field?.required) {
249
+ total++;
250
+ if (this.value['_source'] && this.value['_source'].trim().length > 0) {
251
+ mapped++;
252
+ }
253
+ }
254
+
255
+ // Count required children
256
+ for (const child of this.field.children) {
257
+ if (child.required) {
258
+ total++;
259
+ const val = this.value[child.name];
260
+ if (typeof val === 'string' && val.trim().length > 0) {
261
+ mapped++;
262
+ }
263
+ }
264
+ }
265
+
266
+ this.mappedCount = mapped;
267
+ this.totalRequired = total;
268
+ } else {
269
+ // Direct mode: just check if source is set
270
+ this.totalRequired = this.field?.required ? 1 : 0;
271
+ this.mappedCount = this.getSourceValue() ? (this.field?.required ? 1 : 0) : 0;
272
+ }
273
+ }
274
+
275
+ private countRequiredMapped(
276
+ fields: TemplateField[],
277
+ mapping: Record<string, any>
278
+ ): {mapped: number; total: number} {
279
+ let mapped = 0;
280
+ let total = 0;
281
+ for (const field of fields) {
282
+ if (field.fieldType === 'SCALAR' && field.required) {
283
+ total++;
284
+ const val = mapping[field.name];
285
+ if (typeof val === 'string' && val.trim().length > 0) {
286
+ mapped++;
287
+ }
288
+ } else if (field.fieldType === 'ARRAY' && field.required) {
289
+ total++;
290
+ const val = mapping[field.name];
291
+ if (typeof val === 'string' && val.trim().length > 0) {
292
+ mapped++;
293
+ } else if (typeof val === 'object' && val !== null && '_source' in val) {
294
+ if (typeof val['_source'] === 'string' && val['_source'].trim().length > 0) {
295
+ mapped++;
296
+ }
297
+ }
298
+ } else if (field.fieldType === 'OBJECT' && field.children) {
299
+ const nested = (typeof mapping[field.name] === 'object' && mapping[field.name] !== null)
300
+ ? mapping[field.name]
301
+ : {};
302
+ const childStats = this.countRequiredMapped(field.children, nested);
303
+ mapped += childStats.mapped;
304
+ total += childStats.total;
305
+ }
306
+ }
307
+ return {mapped, total};
308
+ }
309
+
310
+ private detectInputMode(value: any): InputMode {
311
+ if (typeof value !== 'string') return 'browse';
312
+ if (value.startsWith('doc:') || value.startsWith('case:')) return 'browse';
313
+ if (value.startsWith('pv:')) return 'pv';
314
+ if (value.length > 0) return 'expression';
315
+ return 'browse';
316
+ }
317
+
318
+ private isResolvableValue(value: string): boolean {
319
+ return value.startsWith('doc:') || value.startsWith('case:') || value.startsWith('pv:') || value.startsWith('template:');
320
+ }
321
+ }