@valtimo/form-flow-management 13.42.0 → 13.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/fesm2022/valtimo-form-flow-management.mjs +1559 -186
  2. package/fesm2022/valtimo-form-flow-management.mjs.map +1 -1
  3. package/lib/components/editor/form-flow-editor.component.d.ts +34 -5
  4. package/lib/components/editor/form-flow-editor.component.d.ts.map +1 -1
  5. package/lib/components/editor/tabs/form-flow-json-editor-tab/form-flow-json-editor-tab.component.d.ts +17 -0
  6. package/lib/components/editor/tabs/form-flow-json-editor-tab/form-flow-json-editor-tab.component.d.ts.map +1 -0
  7. package/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-context-properties/form-flow-context-properties.component.d.ts +23 -0
  8. package/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-context-properties/form-flow-context-properties.component.d.ts.map +1 -0
  9. package/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-expression-help-modal/form-flow-expression-help-modal.component.d.ts +15 -0
  10. package/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-expression-help-modal/form-flow-expression-help-modal.component.d.ts.map +1 -0
  11. package/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-expression-list/form-flow-expression-list.component.d.ts +54 -0
  12. package/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-expression-list/form-flow-expression-list.component.d.ts.map +1 -0
  13. package/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-step-detail/form-flow-step-detail.component.d.ts +73 -0
  14. package/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-step-detail/form-flow-step-detail.component.d.ts.map +1 -0
  15. package/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-step-list/form-flow-step-list.component.d.ts +42 -0
  16. package/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-step-list/form-flow-step-list.component.d.ts.map +1 -0
  17. package/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-transition-list/form-flow-transition-list.component.d.ts +49 -0
  18. package/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-transition-list/form-flow-transition-list.component.d.ts.map +1 -0
  19. package/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-ui-editor-tab.component.d.ts +89 -0
  20. package/lib/components/editor/tabs/form-flow-ui-editor-tab/form-flow-ui-editor-tab.component.d.ts.map +1 -0
  21. package/lib/components/new-form-flow-modal/new-form-flow-modal.component.d.ts.map +1 -1
  22. package/lib/constants/form-flow-editor.test-ids.d.ts +19 -0
  23. package/lib/constants/form-flow-editor.test-ids.d.ts.map +1 -0
  24. package/lib/constants/index.d.ts +3 -0
  25. package/lib/constants/index.d.ts.map +1 -0
  26. package/lib/constants/injection-tokens.d.ts +5 -0
  27. package/lib/constants/injection-tokens.d.ts.map +1 -0
  28. package/lib/form-flow-management.module.d.ts +3 -1
  29. package/lib/form-flow-management.module.d.ts.map +1 -1
  30. package/lib/models/form-flow-custom-component.model.d.ts +18 -0
  31. package/lib/models/form-flow-custom-component.model.d.ts.map +1 -0
  32. package/lib/models/form-flow-editor.model.d.ts +6 -0
  33. package/lib/models/form-flow-editor.model.d.ts.map +1 -0
  34. package/lib/models/form-flow.model.d.ts +8 -6
  35. package/lib/models/form-flow.model.d.ts.map +1 -1
  36. package/lib/models/index.d.ts +2 -0
  37. package/lib/models/index.d.ts.map +1 -1
  38. package/lib/services/form-flow-component.service.d.ts +13 -0
  39. package/lib/services/form-flow-component.service.d.ts.map +1 -0
  40. package/lib/services/form-flow-download.service.d.ts.map +1 -1
  41. package/lib/services/form-flow-editor-form.service.d.ts +52 -0
  42. package/lib/services/form-flow-editor-form.service.d.ts.map +1 -0
  43. package/lib/services/form-flow.service.d.ts +2 -1
  44. package/lib/services/form-flow.service.d.ts.map +1 -1
  45. package/lib/services/index.d.ts +1 -0
  46. package/lib/services/index.d.ts.map +1 -1
  47. package/lib/utils/index.d.ts +2 -0
  48. package/lib/utils/index.d.ts.map +1 -0
  49. package/lib/utils/translate.utils.d.ts +8 -0
  50. package/lib/utils/translate.utils.d.ts.map +1 -0
  51. package/package.json +1 -1
  52. package/public-api.d.ts +1 -0
  53. package/public-api.d.ts.map +1 -1
@@ -1,21 +1,89 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, EventEmitter, Output, Input, ChangeDetectionStrategy, Component, ViewChild, NgModule } from '@angular/core';
2
+ import { InjectionToken, Injectable, Inject, EventEmitter, Output, Input, ChangeDetectionStrategy, Component, ViewChild, signal, ViewChildren, NgModule } from '@angular/core';
3
3
  import * as i2 from '@valtimo/shared';
4
4
  import { BaseApiService, getCaseManagementRouteParams, getContextObservable, getBuildingBlockManagementRouteParams } from '@valtimo/shared';
5
5
  import * as i1 from '@angular/common/http';
6
+ import { filter, BehaviorSubject, combineLatest, tap, switchMap, of, map, Subscription, take, catchError, shareReplay, finalize } from 'rxjs';
6
7
  import * as i1$2 from '@valtimo/components';
7
- import { CARBON_CONSTANTS, ValtimoCdsModalDirective, ViewType, CarbonListComponent, ConfirmationModalModule, EditorModule, RenderInPageHeaderDirective, OverflowMenuComponent, OverflowMenuOptionComponent, OverflowMenuTriggerComponent, CarbonListModule } from '@valtimo/components';
8
- import { BehaviorSubject, combineLatest, tap, switchMap, of, map, shareReplay, take, finalize } from 'rxjs';
9
- import * as i3$1 from '@angular/router';
8
+ import { CARBON_CONSTANTS, ValtimoCdsModalDirective, ViewType, CarbonListComponent, EditorModule, OverflowMenuComponent, OverflowMenuOptionComponent, TooltipIconModule, FitPageDirective, PendingChangesComponent, ConfirmationModalModule, RenderInPageHeaderDirective, OverflowMenuTriggerComponent, CarbonListModule } from '@valtimo/components';
9
+ import * as i3 from '@angular/router';
10
10
  import * as i4 from '@ngx-translate/core';
11
11
  import { TranslateModule } from '@ngx-translate/core';
12
12
  import * as i5 from '@angular/common';
13
13
  import { CommonModule } from '@angular/common';
14
- import * as i3 from 'carbon-components-angular';
15
- import { ModalModule, InputModule, ButtonModule, IconModule, LoadingModule, NotificationModule, DropdownModule } from 'carbon-components-angular';
14
+ import * as i2$1 from 'carbon-components-angular';
15
+ import { ModalModule, InputModule, ButtonModule, IconModule, DropdownModule, LayerModule, NotificationModule, TagModule, LoadingModule, Tab, TabsModule } from 'carbon-components-angular';
16
16
  import * as i1$1 from '@angular/forms';
17
17
  import { Validators, ReactiveFormsModule, FormsModule } from '@angular/forms';
18
- import { ArrowLeft16 } from '@carbon/icons';
18
+ import { Add16, TrashCan16, ArrowDown16, ArrowUp16, Flag16, WarningFilled16, ArrowLeft16 } from '@carbon/icons';
19
+ import * as i4$1 from '@valtimo/form-management';
20
+
21
+ /*
22
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
23
+ *
24
+ * Licensed under EUPL, Version 1.2 (the "License");
25
+ * you may not use this file except in compliance with the License.
26
+ * You may obtain a copy of the License at
27
+ *
28
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
29
+ *
30
+ * Unless required by applicable law or agreed to in writing, software
31
+ * distributed under the License is distributed on an "AS IS" basis,
32
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
33
+ * See the License for the specific language governing permissions and
34
+ * limitations under the License.
35
+ */
36
+ const FORM_FLOW_EDITOR_TEST_IDS = {
37
+ saveButton: 'formFlowEditorSaveButton',
38
+ backButton: 'formFlowEditorBackButton',
39
+ stepList: 'formFlowEditorStepList',
40
+ stepListItem: 'formFlowEditorStepListItem',
41
+ addStepButton: 'formFlowEditorAddStepButton',
42
+ deleteStepButton: 'formFlowEditorDeleteStepButton',
43
+ makeStartStepButton: 'formFlowEditorMakeStartStepButton',
44
+ stepKeyInput: 'formFlowEditorStepKeyInput',
45
+ stepTitleInput: 'formFlowEditorStepTitleInput',
46
+ stepTypeDropdown: 'formFlowEditorStepTypeDropdown',
47
+ stepPropertyInput: 'formFlowEditorStepPropertyInput',
48
+ stepPropertyDropdown: 'formFlowEditorStepPropertyDropdown',
49
+ addTransitionButton: 'formFlowEditorAddTransitionButton',
50
+ transitionRow: 'formFlowEditorTransitionRow',
51
+ addExpressionButton: 'formFlowEditorAddExpressionButton',
52
+ expressionInput: 'formFlowEditorExpressionInput',
53
+ };
54
+
55
+ /*
56
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
57
+ *
58
+ * Licensed under EUPL, Version 1.2 (the "License");
59
+ * you may not use this file except in compliance with the License.
60
+ * You may obtain a copy of the License at
61
+ *
62
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
63
+ *
64
+ * Unless required by applicable law or agreed to in writing, software
65
+ * distributed under the License is distributed on an "AS IS" basis,
66
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
67
+ * See the License for the specific language governing permissions and
68
+ * limitations under the License.
69
+ */
70
+ const FORM_FLOW_COMPONENT_TOKEN = new InjectionToken('Supported form-flow Angular components', { factory: () => [] });
71
+
72
+ /*
73
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
74
+ *
75
+ * Licensed under EUPL, Version 1.2 (the "License");
76
+ * you may not use this file except in compliance with the License.
77
+ * You may obtain a copy of the License at
78
+ *
79
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
80
+ *
81
+ * Unless required by applicable law or agreed to in writing, software
82
+ * distributed under the License is distributed on an "AS IS" basis,
83
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
84
+ * See the License for the specific language governing permissions and
85
+ * limitations under the License.
86
+ */
19
87
 
20
88
  /*
21
89
  * Copyright 2015-2025 Ritense BV, the Netherlands.
@@ -33,6 +101,43 @@ import { ArrowLeft16 } from '@carbon/icons';
33
101
  * limitations under the License.
34
102
  */
35
103
 
104
+ /*
105
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
106
+ *
107
+ * Licensed under EUPL, Version 1.2 (the "License");
108
+ * you may not use this file except in compliance with the License.
109
+ * You may obtain a copy of the License at
110
+ *
111
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
112
+ *
113
+ * Unless required by applicable law or agreed to in writing, software
114
+ * distributed under the License is distributed on an "AS IS" basis,
115
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
116
+ * See the License for the specific language governing permissions and
117
+ * limitations under the License.
118
+ */
119
+
120
+ /*
121
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
122
+ *
123
+ * Licensed under EUPL, Version 1.2 (the "License");
124
+ * you may not use this file except in compliance with the License.
125
+ * You may obtain a copy of the License at
126
+ *
127
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
128
+ *
129
+ * Unless required by applicable law or agreed to in writing, software
130
+ * distributed under the License is distributed on an "AS IS" basis,
131
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
132
+ * See the License for the specific language governing permissions and
133
+ * limitations under the License.
134
+ */
135
+ var FormFlowEditorTab;
136
+ (function (FormFlowEditorTab) {
137
+ FormFlowEditorTab["EDITOR"] = "editor";
138
+ FormFlowEditorTab["JSON_EDITOR"] = "jsonEditor";
139
+ })(FormFlowEditorTab || (FormFlowEditorTab = {}));
140
+
36
141
  /*
37
142
  * Copyright 2015-2025 Ritense BV, the Netherlands.
38
143
  *
@@ -58,6 +163,12 @@ class FormFlowService extends BaseApiService {
58
163
  getFormFlowDefinitionSchema() {
59
164
  return this.httpClient.get(this.getApiUrl('management/v1/form-flow-definition/schema'));
60
165
  }
166
+ // The registry describes what can be used in a form flow definition (step types, expression
167
+ // beans and additional properties). The backend builds it once at startup, so this is a cheap
168
+ // call that is made fresh every time — no client-side caching.
169
+ getFormFlowRegistry() {
170
+ return this.httpClient.get(this.getApiUrl('management/v1/form-flow/registry'));
171
+ }
61
172
  getFormFlowDefinitions(caseDefinitionKey, caseVersionTag) {
62
173
  return this.httpClient.get(this.getApiUrl(`management/v1/case-definition/${caseDefinitionKey}/version/${caseVersionTag}/form-flow-definition`));
63
174
  }
@@ -98,6 +209,46 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
98
209
  }]
99
210
  }], ctorParameters: () => [{ type: i1.HttpClient }, { type: i2.ConfigService }] });
100
211
 
212
+ /*
213
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
214
+ *
215
+ * Licensed under EUPL, Version 1.2 (the "License");
216
+ * you may not use this file except in compliance with the License.
217
+ * You may obtain a copy of the License at
218
+ *
219
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
220
+ *
221
+ * Unless required by applicable law or agreed to in writing, software
222
+ * distributed under the License is distributed on an "AS IS" basis,
223
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
224
+ * See the License for the specific language governing permissions and
225
+ * limitations under the License.
226
+ */
227
+ class FormFlowComponentService {
228
+ get supportedComponents$() {
229
+ return this._supportedComponents$.pipe(filter(components => !!components));
230
+ }
231
+ constructor(supportedCustomComponents) {
232
+ this.supportedCustomComponents = supportedCustomComponents;
233
+ this._supportedComponents$ = new BehaviorSubject(null);
234
+ this.setSupportedComponents(supportedCustomComponents);
235
+ }
236
+ setSupportedComponents(supportedComponents) {
237
+ this._supportedComponents$.next(supportedComponents);
238
+ }
239
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowComponentService, deps: [{ token: FORM_FLOW_COMPONENT_TOKEN }], target: i0.ɵɵFactoryTarget.Injectable }); }
240
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowComponentService, providedIn: 'root' }); }
241
+ }
242
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowComponentService, decorators: [{
243
+ type: Injectable,
244
+ args: [{
245
+ providedIn: 'root',
246
+ }]
247
+ }], ctorParameters: () => [{ type: Array, decorators: [{
248
+ type: Inject,
249
+ args: [FORM_FLOW_COMPONENT_TOKEN]
250
+ }] }] });
251
+
101
252
  /*
102
253
  * Copyright 2015-2025 Ritense BV, the Netherlands.
103
254
  *
@@ -152,7 +303,16 @@ class NewFormFlowModalComponent {
152
303
  this.closeEvent.emit({
153
304
  key: this.key.value ?? '',
154
305
  startStep: 'start-step',
155
- steps: [],
306
+ // Every form flow needs a start step, so create one up front: without it the editor opens on a
307
+ // "start step does not exist" error for a flow the user has only just named. The form
308
+ // definition is intentionally left blank — the editor flags it as the field still to fill in.
309
+ steps: [
310
+ {
311
+ key: 'start-step',
312
+ type: { name: 'form', properties: { definition: '' } },
313
+ nextSteps: [],
314
+ },
315
+ ],
156
316
  });
157
317
  this.resetForm();
158
318
  }
@@ -162,7 +322,7 @@ class NewFormFlowModalComponent {
162
322
  }, CARBON_CONSTANTS.modalAnimationMs);
163
323
  }
164
324
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: NewFormFlowModalComponent, deps: [{ token: i1$1.FormBuilder }], target: i0.ɵɵFactoryTarget.Component }); }
165
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: NewFormFlowModalComponent, isStandalone: true, selector: "valtimo-new-form-flow-modal", inputs: { open: "open" }, outputs: { closeEvent: "closeEvent" }, ngImport: i0, template: "<!--\n ~ Copyright 2015-2025 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n<cds-modal\n [open]=\"open\"\n showFooter=\"true\"\n [title]=\"'formFlow.add' | translate\"\n valtimoCdsModal\n (close)=\"onCancel()\"\n>\n <cds-modal-header [showCloseButton]=\"true\" (closeSelect)=\"onCancel()\">\n <h3 cdsModalHeaderHeading>\n {{ 'formFlow.add' | translate }}\n </h3>\n </cds-modal-header>\n\n <section cdsModalContent>\n <form [formGroup]=\"form\">\n <cds-label [invalid]=\"key.dirty && key.invalid\">\n {{ 'formFlow.key' | translate }}\n\n <input\n formControlName=\"key\"\n cdsText\n placeholder=\"{{ 'formFlow.key' | translate }}\"\n [attr.modal-primary-focus]=\"true\"\n [invalid]=\"key.dirty && key.invalid\"\n />\n </cds-label>\n </form>\n </section>\n\n <cds-modal-footer>\n <button cdsButton=\"secondary\" (click)=\"onCancel()\">\n {{ 'interface.cancel' | translate }}\n </button>\n\n <button [disabled]=\"this.form.invalid\" (click)=\"onConfirm()\" cdsButton=\"primary\">\n {{ 'interface.create' | translate }}\n </button>\n </cds-modal-footer>\n</cds-modal>\n", dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i4.TranslatePipe, name: "translate" }, { kind: "ngmodule", type: ModalModule }, { kind: "component", type: i3.Modal, selector: "cds-modal, ibm-modal", inputs: ["size", "theme", "ariaLabel", "open", "trigger", "hasScrollingContent"], outputs: ["overlaySelected", "close"] }, { kind: "component", type: i3.ModalHeader, selector: "cds-modal-header, ibm-modal-header", inputs: ["theme", "closeLabel", "showCloseButton"], outputs: ["closeSelect"] }, { kind: "component", type: i3.ModalFooter, selector: "cds-modal-footer, ibm-modal-footer" }, { kind: "directive", type: i3.ModalContent, selector: "[cdsModalContent], [ibmModalContent]", inputs: ["hasForm"] }, { kind: "directive", type: i3.ModalHeaderHeading, selector: "[cdsModalHeaderHeading], [ibmModalHeaderHeading]" }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i3.Label, selector: "cds-label, ibm-label", inputs: ["labelInputID", "disabled", "skeleton", "helperText", "invalidText", "invalid", "warn", "warnText", "ariaLabel"] }, { kind: "directive", type: i3.TextInput, selector: "[cdsText], [ibmText]", inputs: ["theme", "size", "invalid", "warn", "skeleton"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i3.Button, selector: "[cdsButton], [ibmButton]", inputs: ["ibmButton", "cdsButton", "size", "skeleton", "iconOnly", "isExpressive"] }, { kind: "directive", type: ValtimoCdsModalDirective, selector: "[valtimoCdsModal]", inputs: ["minContentHeight"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
325
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: NewFormFlowModalComponent, isStandalone: true, selector: "valtimo-new-form-flow-modal", inputs: { open: "open" }, outputs: { closeEvent: "closeEvent" }, ngImport: i0, template: "<!--\n ~ Copyright 2015-2025 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n<cds-modal\n [open]=\"open\"\n showFooter=\"true\"\n [title]=\"'formFlow.add' | translate\"\n valtimoCdsModal\n (close)=\"onCancel()\"\n>\n <cds-modal-header [showCloseButton]=\"true\" (closeSelect)=\"onCancel()\">\n <h3 cdsModalHeaderHeading>\n {{ 'formFlow.add' | translate }}\n </h3>\n </cds-modal-header>\n\n <section cdsModalContent>\n <form [formGroup]=\"form\">\n <cds-label [invalid]=\"key.dirty && key.invalid\">\n {{ 'formFlow.key' | translate }}\n\n <input\n formControlName=\"key\"\n cdsText\n placeholder=\"{{ 'formFlow.key' | translate }}\"\n [attr.modal-primary-focus]=\"true\"\n [invalid]=\"key.dirty && key.invalid\"\n />\n </cds-label>\n </form>\n </section>\n\n <cds-modal-footer>\n <button cdsButton=\"secondary\" (click)=\"onCancel()\">\n {{ 'interface.cancel' | translate }}\n </button>\n\n <button [disabled]=\"this.form.invalid\" (click)=\"onConfirm()\" cdsButton=\"primary\">\n {{ 'interface.create' | translate }}\n </button>\n </cds-modal-footer>\n</cds-modal>\n", dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i4.TranslatePipe, name: "translate" }, { kind: "ngmodule", type: ModalModule }, { kind: "component", type: i2$1.Modal, selector: "cds-modal, ibm-modal", inputs: ["size", "theme", "ariaLabel", "open", "trigger", "hasScrollingContent"], outputs: ["overlaySelected", "close"] }, { kind: "component", type: i2$1.ModalHeader, selector: "cds-modal-header, ibm-modal-header", inputs: ["theme", "closeLabel", "showCloseButton"], outputs: ["closeSelect"] }, { kind: "component", type: i2$1.ModalFooter, selector: "cds-modal-footer, ibm-modal-footer" }, { kind: "directive", type: i2$1.ModalContent, selector: "[cdsModalContent], [ibmModalContent]", inputs: ["hasForm"] }, { kind: "directive", type: i2$1.ModalHeaderHeading, selector: "[cdsModalHeaderHeading], [ibmModalHeaderHeading]" }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i2$1.Label, selector: "cds-label, ibm-label", inputs: ["labelInputID", "disabled", "skeleton", "helperText", "invalidText", "invalid", "warn", "warnText", "ariaLabel"] }, { kind: "directive", type: i2$1.TextInput, selector: "[cdsText], [ibmText]", inputs: ["theme", "size", "invalid", "warn", "skeleton"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i2$1.Button, selector: "[cdsButton], [ibmButton]", inputs: ["ibmButton", "cdsButton", "size", "skeleton", "iconOnly", "isExpressive"] }, { kind: "directive", type: ValtimoCdsModalDirective, selector: "[valtimoCdsModal]", inputs: ["minContentHeight"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
166
326
  }
167
327
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: NewFormFlowModalComponent, decorators: [{
168
328
  type: Component,
@@ -289,13 +449,13 @@ class FormFlowOverviewComponent {
289
449
  this._refresh$.next(null);
290
450
  });
291
451
  }
292
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowOverviewComponent, deps: [{ token: FormFlowService }, { token: i2.GlobalNotificationService }, { token: i3$1.ActivatedRoute }, { token: i3$1.Router }, { token: i4.TranslateService }, { token: i2.EditPermissionsService }], target: i0.ɵɵFactoryTarget.Component }); }
293
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: FormFlowOverviewComponent, isStandalone: false, selector: "ng-component", viewQueries: [{ propertyName: "carbonList", first: true, predicate: CarbonListComponent, descendants: true }], ngImport: i0, template: "<!--\n ~ Copyright 2015-2025 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n<valtimo-carbon-list\n [actionItems]=\"ACTION_ITEMS\"\n [header]=\"false\"\n [items]=\"formFlowDefinitions$ | async\"\n [fields]=\"FIELDS\"\n [loading]=\"loading$ | async\"\n (rowClicked)=\"editFormFlowDetails($event)\"\n>\n <ng-container header> {{ 'formFlow.title' | translate }} </ng-container>\n\n <div carbonToolbarContent>\n <ng-container [ngTemplateOutlet]=\"addFormFlowButton\"></ng-container>\n </div>\n\n <valtimo-no-results\n [action]=\"addFormFlowButton\"\n description=\"{{ 'formFlow.noResults.description' | translate }}\"\n title=\"{{ 'formFlow.noResults.title' | translate }}\"\n ></valtimo-no-results>\n</valtimo-carbon-list>\n\n<valtimo-new-form-flow-modal\n [open]=\"showAddModal$ | async\"\n (closeEvent)=\"onAdd($event)\"\n></valtimo-new-form-flow-modal>\n\n<ng-template #addFormFlowButton>\n <button *ngIf=\"hasEditPermissions$ | async\" cdsButton=\"primary\" (click)=\"openAddModal()\">\n {{ 'formFlow.add' | translate }}\n\n <svg class=\"cds--btn__icon\" cdsIcon=\"add\" size=\"16\"></svg>\n </button>\n</ng-template>\n\n<valtimo-confirmation-modal\n confirmButtonTextTranslationKey=\"interface.delete\"\n confirmButtonType=\"danger\"\n contentTranslationKey=\"formFlow.deleteModalContent\"\n [outputOnConfirm]=\"deleteDefinitionKey$ | async\"\n [showModalSubject$]=\"showDeleteModal$\"\n titleTranslationKey=\"interface.delete\"\n (confirmEvent)=\"onDelete($event)\"\n></valtimo-confirmation-modal>\n", dependencies: [{ kind: "directive", type: i5.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i5.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i3.Button, selector: "[cdsButton], [ibmButton]", inputs: ["ibmButton", "cdsButton", "size", "skeleton", "iconOnly", "isExpressive"] }, { kind: "directive", type: i3.IconDirective, selector: "[cdsIcon], [ibmIcon]", inputs: ["ibmIcon", "cdsIcon", "size", "title", "ariaLabel", "ariaLabelledBy", "ariaHidden", "isFocusable"] }, { kind: "component", type: i1$2.ConfirmationModalComponent, selector: "valtimo-confirmation-modal", inputs: ["titleTranslationKey", "title", "content", "contentTranslationKey", "confirmButtonText", "confirmButtonTextTranslationKey", "confirmButtonType", "showOptionalButton", "optionalButtonText", "optionalButtonTextTranslationKey", "optionalButtonType", "cancelButtonText", "cancelButtonTextTranslationKey", "cancelButtonType", "showModalSubject$", "outputOnConfirm", "outputOnOptional", "spacerAfterCancelButton"], outputs: ["confirmEvent", "optionalEvent", "cancelEvent"] }, { kind: "component", type: i1$2.CarbonListComponent, selector: "valtimo-carbon-list", inputs: ["items", "fields", "tableTranslations", "paginatorConfig", "pagination", "loading", "skeletonRowCount", "actions", "actionItems", "showActionItems", "header", "hideColumnHeader", "initialSortState", "sortState", "isSearchable", "initialSearchValue", "searchDebounceMs", "invalidSearchFields", "searchFields", "enableSingleSelection", "lastColumnTemplate", "paginationIdentifier", "showSelectionColumn", "striped", "hideToolbar", "lockedTooltipTranslationKey", "movingRowsEnabled", "dragAndDrop", "dragAndDropDisabled", "expandedRowTemplate", "expandedRowKey", "trackByKey"], outputs: ["rowClicked", "paginationClicked", "paginationSet", "search", "sortChanged", "moveRow", "itemsReordered"] }, { kind: "component", type: i1$2.CarbonNoResultsComponent, selector: "valtimo-no-results", inputs: ["action", "description", "illustration", "title", "smallPadding", "collapseVertically", "alwaysRenderVertically"] }, { kind: "component", type: NewFormFlowModalComponent, selector: "valtimo-new-form-flow-modal", inputs: ["open"], outputs: ["closeEvent"] }, { kind: "pipe", type: i5.AsyncPipe, name: "async" }, { kind: "pipe", type: i4.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
452
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowOverviewComponent, deps: [{ token: FormFlowService }, { token: i2.GlobalNotificationService }, { token: i3.ActivatedRoute }, { token: i3.Router }, { token: i4.TranslateService }, { token: i2.EditPermissionsService }], target: i0.ɵɵFactoryTarget.Component }); }
453
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: FormFlowOverviewComponent, isStandalone: false, selector: "ng-component", viewQueries: [{ propertyName: "carbonList", first: true, predicate: CarbonListComponent, descendants: true }], ngImport: i0, template: "<!--\n ~ Copyright 2015-2025 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n<valtimo-carbon-list\n [actionItems]=\"ACTION_ITEMS\"\n [header]=\"false\"\n [items]=\"formFlowDefinitions$ | async\"\n [fields]=\"FIELDS\"\n [loading]=\"loading$ | async\"\n (rowClicked)=\"editFormFlowDetails($event)\"\n>\n <ng-container header> {{ 'formFlow.title' | translate }} </ng-container>\n\n <div carbonToolbarContent>\n <ng-container [ngTemplateOutlet]=\"addFormFlowButton\"></ng-container>\n </div>\n\n <valtimo-no-results\n [action]=\"addFormFlowButton\"\n description=\"{{ 'formFlow.noResults.description' | translate }}\"\n title=\"{{ 'formFlow.noResults.title' | translate }}\"\n ></valtimo-no-results>\n</valtimo-carbon-list>\n\n<valtimo-new-form-flow-modal\n [open]=\"showAddModal$ | async\"\n (closeEvent)=\"onAdd($event)\"\n></valtimo-new-form-flow-modal>\n\n<ng-template #addFormFlowButton>\n <button *ngIf=\"hasEditPermissions$ | async\" cdsButton=\"primary\" (click)=\"openAddModal()\">\n {{ 'formFlow.add' | translate }}\n\n <svg class=\"cds--btn__icon\" cdsIcon=\"add\" size=\"16\"></svg>\n </button>\n</ng-template>\n\n<valtimo-confirmation-modal\n confirmButtonTextTranslationKey=\"interface.delete\"\n confirmButtonType=\"danger\"\n contentTranslationKey=\"formFlow.deleteModalContent\"\n [outputOnConfirm]=\"deleteDefinitionKey$ | async\"\n [showModalSubject$]=\"showDeleteModal$\"\n titleTranslationKey=\"interface.delete\"\n (confirmEvent)=\"onDelete($event)\"\n></valtimo-confirmation-modal>\n", dependencies: [{ kind: "directive", type: i5.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i5.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i2$1.Button, selector: "[cdsButton], [ibmButton]", inputs: ["ibmButton", "cdsButton", "size", "skeleton", "iconOnly", "isExpressive"] }, { kind: "directive", type: i2$1.IconDirective, selector: "[cdsIcon], [ibmIcon]", inputs: ["ibmIcon", "cdsIcon", "size", "title", "ariaLabel", "ariaLabelledBy", "ariaHidden", "isFocusable"] }, { kind: "component", type: i1$2.ConfirmationModalComponent, selector: "valtimo-confirmation-modal", inputs: ["titleTranslationKey", "title", "content", "contentTranslationKey", "confirmButtonText", "confirmButtonTextTranslationKey", "confirmButtonType", "showOptionalButton", "optionalButtonText", "optionalButtonTextTranslationKey", "optionalButtonType", "cancelButtonText", "cancelButtonTextTranslationKey", "cancelButtonType", "showModalSubject$", "outputOnConfirm", "outputOnOptional", "spacerAfterCancelButton"], outputs: ["confirmEvent", "optionalEvent", "cancelEvent"] }, { kind: "component", type: i1$2.CarbonListComponent, selector: "valtimo-carbon-list", inputs: ["items", "fields", "tableTranslations", "paginatorConfig", "pagination", "loading", "skeletonRowCount", "actions", "actionItems", "showActionItems", "header", "hideColumnHeader", "initialSortState", "sortState", "isSearchable", "initialSearchValue", "searchDebounceMs", "invalidSearchFields", "searchFields", "enableSingleSelection", "lastColumnTemplate", "paginationIdentifier", "showSelectionColumn", "striped", "hideToolbar", "lockedTooltipTranslationKey", "movingRowsEnabled", "dragAndDrop", "dragAndDropDisabled", "expandedRowTemplate", "expandedRowKey", "trackByKey"], outputs: ["rowClicked", "paginationClicked", "paginationSet", "search", "sortChanged", "moveRow", "itemsReordered"] }, { kind: "component", type: i1$2.CarbonNoResultsComponent, selector: "valtimo-no-results", inputs: ["action", "description", "illustration", "title", "smallPadding", "collapseVertically", "alwaysRenderVertically"] }, { kind: "component", type: NewFormFlowModalComponent, selector: "valtimo-new-form-flow-modal", inputs: ["open"], outputs: ["closeEvent"] }, { kind: "pipe", type: i5.AsyncPipe, name: "async" }, { kind: "pipe", type: i4.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
294
454
  }
295
455
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowOverviewComponent, decorators: [{
296
456
  type: Component,
297
457
  args: [{ standalone: false, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n ~ Copyright 2015-2025 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n<valtimo-carbon-list\n [actionItems]=\"ACTION_ITEMS\"\n [header]=\"false\"\n [items]=\"formFlowDefinitions$ | async\"\n [fields]=\"FIELDS\"\n [loading]=\"loading$ | async\"\n (rowClicked)=\"editFormFlowDetails($event)\"\n>\n <ng-container header> {{ 'formFlow.title' | translate }} </ng-container>\n\n <div carbonToolbarContent>\n <ng-container [ngTemplateOutlet]=\"addFormFlowButton\"></ng-container>\n </div>\n\n <valtimo-no-results\n [action]=\"addFormFlowButton\"\n description=\"{{ 'formFlow.noResults.description' | translate }}\"\n title=\"{{ 'formFlow.noResults.title' | translate }}\"\n ></valtimo-no-results>\n</valtimo-carbon-list>\n\n<valtimo-new-form-flow-modal\n [open]=\"showAddModal$ | async\"\n (closeEvent)=\"onAdd($event)\"\n></valtimo-new-form-flow-modal>\n\n<ng-template #addFormFlowButton>\n <button *ngIf=\"hasEditPermissions$ | async\" cdsButton=\"primary\" (click)=\"openAddModal()\">\n {{ 'formFlow.add' | translate }}\n\n <svg class=\"cds--btn__icon\" cdsIcon=\"add\" size=\"16\"></svg>\n </button>\n</ng-template>\n\n<valtimo-confirmation-modal\n confirmButtonTextTranslationKey=\"interface.delete\"\n confirmButtonType=\"danger\"\n contentTranslationKey=\"formFlow.deleteModalContent\"\n [outputOnConfirm]=\"deleteDefinitionKey$ | async\"\n [showModalSubject$]=\"showDeleteModal$\"\n titleTranslationKey=\"interface.delete\"\n (confirmEvent)=\"onDelete($event)\"\n></valtimo-confirmation-modal>\n" }]
298
- }], ctorParameters: () => [{ type: FormFlowService }, { type: i2.GlobalNotificationService }, { type: i3$1.ActivatedRoute }, { type: i3$1.Router }, { type: i4.TranslateService }, { type: i2.EditPermissionsService }], propDecorators: { carbonList: [{
458
+ }], ctorParameters: () => [{ type: FormFlowService }, { type: i2.GlobalNotificationService }, { type: i3.ActivatedRoute }, { type: i3.Router }, { type: i4.TranslateService }, { type: i2.EditPermissionsService }], propDecorators: { carbonList: [{
299
459
  type: ViewChild,
300
460
  args: [CarbonListComponent]
301
461
  }] } });
@@ -335,7 +495,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
335
495
  }] });
336
496
 
337
497
  /*
338
- * Copyright 2015-2025 Ritense BV, the Netherlands.
498
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
339
499
  *
340
500
  * Licensed under EUPL, Version 1.2 (the "License");
341
501
  * you may not use this file except in compliance with the License.
@@ -349,177 +509,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
349
509
  * See the License for the specific language governing permissions and
350
510
  * limitations under the License.
351
511
  */
352
- class FormFlowEditorComponent {
353
- constructor(breadcrumbService, formFlowDownloadService, formFlowService, iconService, notificationService, pageHeaderService, pageTitleService, route, router, translateService) {
354
- this.breadcrumbService = breadcrumbService;
355
- this.formFlowDownloadService = formFlowDownloadService;
356
- this.formFlowService = formFlowService;
357
- this.iconService = iconService;
358
- this.notificationService = notificationService;
359
- this.pageHeaderService = pageHeaderService;
360
- this.pageTitleService = pageTitleService;
361
- this.route = route;
362
- this.router = router;
363
- this.translateService = translateService;
364
- this.readOnly$ = new BehaviorSubject(false);
365
- this.valid$ = new BehaviorSubject(false);
366
- this.loading$ = new BehaviorSubject(true);
367
- this.showDeleteModal$ = new BehaviorSubject(false);
368
- this.CARBON_THEME = 'g10';
369
- this._context$ = getContextObservable(this.route);
370
- this._params$ = this._context$.pipe(switchMap(context => {
371
- if (context === 'buildingBlock') {
372
- return combineLatest([
373
- getBuildingBlockManagementRouteParams(this.route),
374
- this.route.params,
375
- ]).pipe(map(([bbParams, params]) => ({
376
- caseDefinitionKey: bbParams?.buildingBlockDefinitionKey ?? '',
377
- caseDefinitionVersionTag: bbParams?.buildingBlockDefinitionVersionTag ?? '',
378
- formFlowDefinitionKey: params.formFlowDefinitionKey,
379
- })));
380
- }
381
- return combineLatest([
382
- getCaseManagementRouteParams(this.route),
383
- this.route.params,
384
- ]).pipe(map(([caseManagementParams, params]) => ({
385
- ...(caseManagementParams ?? { caseDefinitionKey: '', caseDefinitionVersionTag: '' }),
386
- ...params,
387
- })));
388
- }));
389
- this.formFlowSchemaJson$ = this.formFlowService
390
- .getFormFlowDefinitionSchema()
391
- .pipe(shareReplay(1));
512
+ class FormFlowJsonEditorTabComponent {
513
+ constructor() {
514
+ this.disabled = false;
515
+ this.schema = null;
516
+ this.validEvent = new EventEmitter();
517
+ this.valueChangeEvent = new EventEmitter();
392
518
  this.editorOptions = {
393
519
  quickSuggestions: { other: true, comments: false, strings: true },
394
520
  };
395
- this._formFlowDefinition2$ = combineLatest([this._params$, this._context$]).pipe(tap(() => this.loading$.next(true)), switchMap(([params, context]) => {
396
- this.initBreadcrumbs(params, context);
397
- if (context === 'buildingBlock') {
398
- return this.formFlowService.getBuildingBlockFormFlowDefinitionByKey(params.caseDefinitionKey, params.caseDefinitionVersionTag, params.formFlowDefinitionKey);
399
- }
400
- return this.formFlowService.getFormFlowDefinitionByKey(params.caseDefinitionKey, params.caseDefinitionVersionTag, params.formFlowDefinitionKey);
401
- }), tap((formFlowDefinition) => {
402
- this.pageTitleService.setCustomPageTitle(formFlowDefinition.key);
403
- this.readOnly$.next(formFlowDefinition.readOnly === true);
404
- this.loading$.next(false);
405
- }));
406
- this.model$ = this._formFlowDefinition2$.pipe(map((formFlowDefinition) => this.getEditorModel(formFlowDefinition)));
407
- this._updatedModelValue$ = new BehaviorSubject('');
408
- this.compactMode$ = this.pageHeaderService.compactMode$;
409
- this.iconService.registerAll([ArrowLeft16]);
410
- this.pageTitleService.disableReset();
411
- }
412
- ngOnDestroy() {
413
- this.pageTitleService.enableReset();
414
- this.breadcrumbService.clearThirdBreadcrumb();
415
- this.breadcrumbService.clearFourthBreadcrumb();
416
521
  }
417
522
  onValid(valid) {
418
- this.valid$.next(valid);
523
+ this.validEvent.emit(valid);
419
524
  }
420
525
  onValueChange(value) {
421
- this._updatedModelValue$.next(value);
422
- }
423
- updateFormFlowDefinition() {
424
- this.loading$.next(true);
425
- combineLatest([this._params$, this._updatedModelValue$, this._context$])
426
- .pipe(take(1), switchMap(([params, updatedModelValue, context]) => {
427
- const updatedDefinition = {
428
- ...JSON.parse(updatedModelValue),
429
- key: params.formFlowDefinitionKey,
430
- };
431
- if (context === 'buildingBlock') {
432
- return this.formFlowService.updateBuildingBlockFormFlowDefinition(params.caseDefinitionKey, params.caseDefinitionVersionTag, params.formFlowDefinitionKey, updatedDefinition);
433
- }
434
- return this.formFlowService.updateFormFlowDefinition(params.caseDefinitionKey, params.caseDefinitionVersionTag, params.formFlowDefinitionKey, updatedDefinition);
435
- }), finalize(() => this.loading$.next(false)))
436
- .subscribe(result => {
437
- this.showSuccessMessage(result.key);
438
- });
439
- }
440
- onDelete() {
441
- this.loading$.next(true);
442
- combineLatest([this._params$, this._context$])
443
- .pipe(take(1), switchMap(([params, context]) => {
444
- if (context === 'buildingBlock') {
445
- return this.formFlowService.deleteBuildingBlockFormFlowDefinition(params.caseDefinitionKey, params.caseDefinitionVersionTag, params.formFlowDefinitionKey);
446
- }
447
- return this.formFlowService.deleteFormFlowDefinition(params.caseDefinitionKey, params.caseDefinitionVersionTag, params.formFlowDefinitionKey);
448
- }))
449
- .subscribe(() => {
450
- this.router.navigate(['../'], { relativeTo: this.route });
451
- });
452
- }
453
- showDeleteModal() {
454
- this.showDeleteModal$.next(true);
526
+ this.valueChangeEvent.emit(value);
455
527
  }
456
- downloadFormFlowDefinition(model) {
457
- this._params$
458
- .pipe(take(1))
459
- .subscribe((params) => this.formFlowDownloadService.downloadJson(JSON.parse(model.value), params));
460
- }
461
- navigateBack() {
462
- this.router.navigate(['../'], { relativeTo: this.route });
463
- }
464
- getEditorModel(formFlowDefinition) {
465
- const clone = { ...formFlowDefinition };
466
- delete clone.readOnly;
467
- return {
468
- value: JSON.stringify(clone),
469
- language: 'json',
470
- uri: `inmemory://form-flow/${formFlowDefinition.key}.formflow.json`,
471
- };
472
- }
473
- showSuccessMessage(formFlowDefinitionKey) {
474
- this.notificationService.showToast({
475
- caption: this.translateService.instant('formFlow.savedSuccessTitleMessage', {
476
- key: formFlowDefinitionKey,
477
- }),
478
- type: 'success',
479
- title: this.translateService.instant('formFlow.savedSuccessTitle'),
480
- });
481
- }
482
- initBreadcrumbs(params, context) {
483
- if (context === 'buildingBlock') {
484
- const route = `/building-block-management/building-block/${params.caseDefinitionKey}/version/${params.caseDefinitionVersionTag}`;
485
- const generalRoute = `${route}/general`;
486
- this.breadcrumbService.setThirdBreadcrumb({
487
- route: [generalRoute],
488
- content: `${params.caseDefinitionKey} (${params.caseDefinitionVersionTag})`,
489
- href: generalRoute,
490
- });
491
- const routeWithFormFlows = `${route}/form-flows`;
492
- this.breadcrumbService.setFourthBreadcrumb({
493
- route: [routeWithFormFlows],
494
- content: this.translateService.instant('buildingBlockManagement.tabs.formFlows'),
495
- href: routeWithFormFlows,
496
- });
497
- }
498
- else {
499
- const route = `/case-management/case/${params.caseDefinitionKey}/version/${params.caseDefinitionVersionTag}`;
500
- this.breadcrumbService.setThirdBreadcrumb({
501
- route: [route],
502
- content: `${params.caseDefinitionKey} (${params.caseDefinitionVersionTag})`,
503
- href: route,
504
- });
505
- const routeWithFormFlows = `${route}/form-flows`;
506
- this.breadcrumbService.setFourthBreadcrumb({
507
- route: [routeWithFormFlows],
508
- content: this.translateService.instant('caseManagement.tabs.formFlows'),
509
- href: routeWithFormFlows,
510
- });
511
- }
512
- }
513
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowEditorComponent, deps: [{ token: i1$2.BreadcrumbService }, { token: FormFlowDownloadService }, { token: FormFlowService }, { token: i3.IconService }, { token: i2.GlobalNotificationService }, { token: i1$2.PageHeaderService }, { token: i1$2.PageTitleService }, { token: i3$1.ActivatedRoute }, { token: i3$1.Router }, { token: i4.TranslateService }], target: i0.ɵɵFactoryTarget.Component }); }
514
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: FormFlowEditorComponent, isStandalone: false, selector: "ng-component", ngImport: i0, template: "<!--\n ~ Copyright 2015-2025 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n@if ({model: model$ | async, schema: formFlowSchemaJson$ | async}; as obs) {\n @if (obs.model && obs.schema) {\n <valtimo-editor\n [disabled]=\"(loading$ | async) === false && readOnly$ | async\"\n [editorOptions]=\"editorOptions\"\n [model]=\"obs.model\"\n [jsonSchema]=\"obs.schema\"\n (validEvent)=\"onValid($event)\"\n (valueChangeEvent)=\"onValueChange($event)\"\n [fitPage]=\"true\"\n ></valtimo-editor>\n } @else {\n <cds-loading></cds-loading>\n }\n}\n\n<ng-container renderInPageHeader>\n <ng-template>\n <div\n *ngIf=\"{\n compactMode: compactMode$ | async,\n valid: valid$ | async,\n readOnly: readOnly$ | async,\n model: model$ | async,\n } as obs\"\n class=\"buttons-container\"\n >\n <v-overflow-menu placement=\"bottom-end\">\n <v-overflow-menu-trigger overflowTrigger></v-overflow-menu-trigger>\n <v-overflow-menu-option\n [disabled]=\"!obs.valid && !obs.readOnly\"\n (selected)=\"downloadFormFlowDefinition(obs.model)\"\n >{{ 'interface.export' | translate }}\n </v-overflow-menu-option>\n\n <v-overflow-menu-option\n [disabled]=\"obs.readOnly\"\n type=\"danger\"\n (selected)=\"showDeleteModal()\"\n >{{ 'interface.delete' | translate }}\n </v-overflow-menu-option>\n </v-overflow-menu>\n\n <button cdsButton=\"secondary\" [size]=\"obs.compactMode ? 'sm' : 'md'\" (click)=\"navigateBack()\">\n {{ 'interface.back' | translate }}\n\n <svg class=\"cds--btn__icon\" cdsIcon=\"arrow--left\" size=\"16\"></svg>\n </button>\n\n <button\n [disabled]=\"!obs.valid || obs.readOnly\"\n cdsButton=\"primary\"\n [size]=\"obs.compactMode ? 'sm' : 'md'\"\n (click)=\"updateFormFlowDefinition()\"\n >\n {{ 'interface.save' | translate }}\n\n <svg class=\"cds--btn__icon\" cdsIcon=\"save\" size=\"16\"></svg>\n </button>\n </div>\n </ng-template>\n</ng-container>\n\n<valtimo-confirmation-modal\n confirmButtonTextTranslationKey=\"interface.delete\"\n confirmButtonType=\"danger\"\n contentTranslationKey=\"formFlow.deleteModalContent\"\n [showModalSubject$]=\"showDeleteModal$\"\n titleTranslationKey=\"interface.delete\"\n (confirmEvent)=\"onDelete()\"\n></valtimo-confirmation-modal>\n", styles: ["cds-loading{display:flex;justify-content:center}.buttons-container{display:flex;align-items:center;justify-self:flex-end;gap:16px}\n/*!\n * Copyright 2015-2025 Ritense BV, the Netherlands.\n *\n * Licensed under EUPL, Version 1.2 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" basis,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n"], dependencies: [{ kind: "directive", type: i5.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3.Button, selector: "[cdsButton], [ibmButton]", inputs: ["ibmButton", "cdsButton", "size", "skeleton", "iconOnly", "isExpressive"] }, { kind: "directive", type: i3.IconDirective, selector: "[cdsIcon], [ibmIcon]", inputs: ["ibmIcon", "cdsIcon", "size", "title", "ariaLabel", "ariaLabelledBy", "ariaHidden", "isFocusable"] }, { kind: "component", type: i1$2.ConfirmationModalComponent, selector: "valtimo-confirmation-modal", inputs: ["titleTranslationKey", "title", "content", "contentTranslationKey", "confirmButtonText", "confirmButtonTextTranslationKey", "confirmButtonType", "showOptionalButton", "optionalButtonText", "optionalButtonTextTranslationKey", "optionalButtonType", "cancelButtonText", "cancelButtonTextTranslationKey", "cancelButtonType", "showModalSubject$", "outputOnConfirm", "outputOnOptional", "spacerAfterCancelButton"], outputs: ["confirmEvent", "optionalEvent", "cancelEvent"] }, { kind: "component", type: i1$2.EditorComponent, selector: "valtimo-editor", inputs: ["editorOptions", "model", "disabled", "formatOnLoad", "widthPx", "heightPx", "heightStyle", "jsonSchema", "fitPage"], outputs: ["validEvent", "valueChangeEvent"] }, { kind: "directive", type: i1$2.RenderInPageHeaderDirective, selector: "[renderInPageHeader]", inputs: ["fullWidth"] }, { kind: "component", type: i3.Loading, selector: "cds-loading, ibm-loading", inputs: ["title", "isActive", "size", "overlay"] }, { kind: "component", type: i1$2.OverflowMenuComponent, selector: "v-overflow-menu", inputs: ["open", "placement", "menuWidth", "offsetX", "offsetY", "closeOnSelect", "useHostAsReference", "portalToBody"], outputs: ["openChange"] }, { kind: "component", type: i1$2.OverflowMenuOptionComponent, selector: "v-overflow-menu-option", inputs: ["disabled", "type", "testId", "optionId"], outputs: ["selected"] }, { kind: "component", type: i1$2.OverflowMenuTriggerComponent, selector: "v-overflow-menu-trigger", inputs: ["compact"] }, { kind: "pipe", type: i5.AsyncPipe, name: "async" }, { kind: "pipe", type: i4.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
528
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowJsonEditorTabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
529
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: FormFlowJsonEditorTabComponent, isStandalone: true, selector: "valtimo-form-flow-json-editor-tab", inputs: { disabled: "disabled", model: "model", schema: "schema" }, outputs: { validEvent: "validEvent", valueChangeEvent: "valueChangeEvent" }, ngImport: i0, template: "<!--\n ~ Copyright 2015-2026 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n@if (schema) {\n <valtimo-editor\n [disabled]=\"disabled ?? false\"\n [editorOptions]=\"editorOptions\"\n [fitPage]=\"true\"\n [jsonSchema]=\"schema\"\n [model]=\"model\"\n (validEvent)=\"onValid($event)\"\n (valueChangeEvent)=\"onValueChange($event)\"\n ></valtimo-editor>\n}\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: EditorModule }, { kind: "component", type: i1$2.EditorComponent, selector: "valtimo-editor", inputs: ["editorOptions", "model", "disabled", "formatOnLoad", "widthPx", "heightPx", "heightStyle", "jsonSchema", "fitPage"], outputs: ["validEvent", "valueChangeEvent"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
515
530
  }
516
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowEditorComponent, decorators: [{
531
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowJsonEditorTabComponent, decorators: [{
517
532
  type: Component,
518
- args: [{ standalone: false, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n ~ Copyright 2015-2025 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n@if ({model: model$ | async, schema: formFlowSchemaJson$ | async}; as obs) {\n @if (obs.model && obs.schema) {\n <valtimo-editor\n [disabled]=\"(loading$ | async) === false && readOnly$ | async\"\n [editorOptions]=\"editorOptions\"\n [model]=\"obs.model\"\n [jsonSchema]=\"obs.schema\"\n (validEvent)=\"onValid($event)\"\n (valueChangeEvent)=\"onValueChange($event)\"\n [fitPage]=\"true\"\n ></valtimo-editor>\n } @else {\n <cds-loading></cds-loading>\n }\n}\n\n<ng-container renderInPageHeader>\n <ng-template>\n <div\n *ngIf=\"{\n compactMode: compactMode$ | async,\n valid: valid$ | async,\n readOnly: readOnly$ | async,\n model: model$ | async,\n } as obs\"\n class=\"buttons-container\"\n >\n <v-overflow-menu placement=\"bottom-end\">\n <v-overflow-menu-trigger overflowTrigger></v-overflow-menu-trigger>\n <v-overflow-menu-option\n [disabled]=\"!obs.valid && !obs.readOnly\"\n (selected)=\"downloadFormFlowDefinition(obs.model)\"\n >{{ 'interface.export' | translate }}\n </v-overflow-menu-option>\n\n <v-overflow-menu-option\n [disabled]=\"obs.readOnly\"\n type=\"danger\"\n (selected)=\"showDeleteModal()\"\n >{{ 'interface.delete' | translate }}\n </v-overflow-menu-option>\n </v-overflow-menu>\n\n <button cdsButton=\"secondary\" [size]=\"obs.compactMode ? 'sm' : 'md'\" (click)=\"navigateBack()\">\n {{ 'interface.back' | translate }}\n\n <svg class=\"cds--btn__icon\" cdsIcon=\"arrow--left\" size=\"16\"></svg>\n </button>\n\n <button\n [disabled]=\"!obs.valid || obs.readOnly\"\n cdsButton=\"primary\"\n [size]=\"obs.compactMode ? 'sm' : 'md'\"\n (click)=\"updateFormFlowDefinition()\"\n >\n {{ 'interface.save' | translate }}\n\n <svg class=\"cds--btn__icon\" cdsIcon=\"save\" size=\"16\"></svg>\n </button>\n </div>\n </ng-template>\n</ng-container>\n\n<valtimo-confirmation-modal\n confirmButtonTextTranslationKey=\"interface.delete\"\n confirmButtonType=\"danger\"\n contentTranslationKey=\"formFlow.deleteModalContent\"\n [showModalSubject$]=\"showDeleteModal$\"\n titleTranslationKey=\"interface.delete\"\n (confirmEvent)=\"onDelete()\"\n></valtimo-confirmation-modal>\n", styles: ["cds-loading{display:flex;justify-content:center}.buttons-container{display:flex;align-items:center;justify-self:flex-end;gap:16px}\n/*!\n * Copyright 2015-2025 Ritense BV, the Netherlands.\n *\n * Licensed under EUPL, Version 1.2 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" basis,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n"] }]
519
- }], ctorParameters: () => [{ type: i1$2.BreadcrumbService }, { type: FormFlowDownloadService }, { type: FormFlowService }, { type: i3.IconService }, { type: i2.GlobalNotificationService }, { type: i1$2.PageHeaderService }, { type: i1$2.PageTitleService }, { type: i3$1.ActivatedRoute }, { type: i3$1.Router }, { type: i4.TranslateService }] });
533
+ args: [{ standalone: true, selector: 'valtimo-form-flow-json-editor-tab', changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, EditorModule], template: "<!--\n ~ Copyright 2015-2026 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n@if (schema) {\n <valtimo-editor\n [disabled]=\"disabled ?? false\"\n [editorOptions]=\"editorOptions\"\n [fitPage]=\"true\"\n [jsonSchema]=\"schema\"\n [model]=\"model\"\n (validEvent)=\"onValid($event)\"\n (valueChangeEvent)=\"onValueChange($event)\"\n ></valtimo-editor>\n}\n" }]
534
+ }], propDecorators: { disabled: [{
535
+ type: Input
536
+ }], model: [{
537
+ type: Input
538
+ }], schema: [{
539
+ type: Input
540
+ }], validEvent: [{
541
+ type: Output
542
+ }], valueChangeEvent: [{
543
+ type: Output
544
+ }] } });
520
545
 
521
546
  /*
522
- * Copyright 2015-2025 Ritense BV, the Netherlands.
547
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
523
548
  *
524
549
  * Licensed under EUPL, Version 1.2 (the "License");
525
550
  * you may not use this file except in compliance with the License.
@@ -533,9 +558,1354 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
533
558
  * See the License for the specific language governing permissions and
534
559
  * limitations under the License.
535
560
  */
536
- class DeleteFormFlowModalComponent {
537
- constructor() {
538
- this.deleteEvent = new EventEmitter();
561
+ /**
562
+ * Builds and reads the reactive form tree that backs the visual form flow editor.
563
+ *
564
+ * The form mirrors the definition JSON: a `startStep` control and a `steps` array where every step
565
+ * has its key, title, type (name plus a dynamic properties group driven by the registry),
566
+ * transitions and the three expression lists. Serialization produces the same JSON contract the
567
+ * JSON editor tab emits, so both tabs are interchangeable to the surrounding editor page.
568
+ */
569
+ class FormFlowEditorFormService {
570
+ constructor(fb) {
571
+ this.fb = fb;
572
+ }
573
+ buildDefinitionForm(definition, registry) {
574
+ return this.fb.group({
575
+ startStep: this.fb.control(definition.startStep ?? '', Validators.required),
576
+ steps: this.fb.array((definition.steps ?? []).map(step => this.buildStepGroup(step, registry)), [uniqueStepKeysValidator]),
577
+ }, { validators: [startStepExistsValidator, transitionTargetsExistValidator] });
578
+ }
579
+ buildStepGroup(step, registry) {
580
+ const typeName = step.type?.name ?? '';
581
+ // The legacy single `nextStep` field is normalized into a `nextSteps` transition, mirroring
582
+ // how the backend interprets it.
583
+ const transitions = step.nextSteps ?? (step.nextStep ? [{ step: step.nextStep }] : []);
584
+ return this.fb.group({
585
+ key: this.fb.control(step.key ?? '', Validators.required),
586
+ title: this.fb.control(step.title ?? ''),
587
+ typeName: this.fb.control(typeName, Validators.required),
588
+ properties: this.buildPropertiesGroup(typeName, (step.type?.properties ?? {}), registry),
589
+ nextSteps: this.fb.array(transitions.map(transition => this.buildTransitionGroup(transition)), [singleDefaultTransitionValidator]),
590
+ onOpen: this.buildExpressionArray(step.onOpen),
591
+ onComplete: this.buildExpressionArray(step.onComplete),
592
+ onBack: this.buildExpressionArray(step.onBack),
593
+ });
594
+ }
595
+ buildNewStepGroup(existingKeys, registry) {
596
+ const defaultType = registry.stepTypes.find(stepType => stepType.name === 'form')?.name ??
597
+ registry.stepTypes[0]?.name ??
598
+ 'form';
599
+ return this.buildStepGroup({
600
+ key: this.generateStepKey(existingKeys),
601
+ type: { name: defaultType, properties: {} },
602
+ nextSteps: [],
603
+ }, registry);
604
+ }
605
+ /**
606
+ * Builds the dynamic properties group for a step type. Known types get one required control per
607
+ * registry property; unknown (custom) types keep controls for whatever properties the loaded
608
+ * definition already contained. Values for properties that exist in both the old and the new
609
+ * type are preserved.
610
+ */
611
+ buildPropertiesGroup(typeName, currentProperties, registry) {
612
+ const registryStepType = registry.stepTypes.find(stepType => stepType.name === typeName);
613
+ const propertyNames = registryStepType
614
+ ? registryStepType.properties.map(property => property.name)
615
+ : Object.keys(currentProperties);
616
+ return this.fb.group(Object.fromEntries(propertyNames.map(name => [
617
+ name,
618
+ this.fb.control(currentProperties[name] ?? '', Validators.required),
619
+ ])));
620
+ }
621
+ buildTransitionGroup(transition) {
622
+ return this.fb.group({
623
+ step: this.fb.control(transition?.step ?? '', Validators.required),
624
+ condition: this.fb.control(transition?.condition ?? ''),
625
+ });
626
+ }
627
+ buildExpressionControl(expression = '') {
628
+ return this.fb.control(expression, Validators.required);
629
+ }
630
+ serialize(form, definitionKey) {
631
+ const value = form.getRawValue();
632
+ return {
633
+ key: definitionKey,
634
+ startStep: value.startStep,
635
+ steps: value.steps.map(step => ({
636
+ key: step.key,
637
+ ...(step.title?.trim() ? { title: step.title.trim() } : {}),
638
+ type: {
639
+ name: step.typeName,
640
+ properties: step.properties,
641
+ },
642
+ nextSteps: step.nextSteps.map(transition => ({
643
+ step: transition.step,
644
+ ...(transition.condition?.trim() ? { condition: transition.condition.trim() } : {}),
645
+ })),
646
+ onBack: step.onBack,
647
+ onOpen: step.onOpen,
648
+ onComplete: step.onComplete,
649
+ })),
650
+ };
651
+ }
652
+ /**
653
+ * Rewrites every reference to a renamed step key: the start step and all transition targets.
654
+ * Used while the user types a new key, so the definition stays internally consistent.
655
+ */
656
+ renameStepReferences(form, oldKey, newKey) {
657
+ if (!oldKey || oldKey === newKey)
658
+ return;
659
+ const startStepControl = form.get('startStep');
660
+ if (startStepControl?.value === oldKey) {
661
+ startStepControl.setValue(newKey, { emitEvent: false });
662
+ }
663
+ this.getStepsArray(form).controls.forEach(stepGroup => {
664
+ stepGroup.get('nextSteps').controls.forEach(transitionGroup => {
665
+ const stepControl = transitionGroup.get('step');
666
+ if (stepControl?.value === oldKey) {
667
+ stepControl.setValue(newKey, { emitEvent: false });
668
+ }
669
+ });
670
+ });
671
+ }
672
+ getStepsArray(form) {
673
+ return form.get('steps');
674
+ }
675
+ buildExpressionArray(expressions) {
676
+ return this.fb.array((expressions ?? []).map(expression => this.buildExpressionControl(expression)));
677
+ }
678
+ generateStepKey(existingKeys) {
679
+ let index = existingKeys.length + 1;
680
+ while (existingKeys.includes(`step-${index}`)) {
681
+ index++;
682
+ }
683
+ return `step-${index}`;
684
+ }
685
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowEditorFormService, deps: [{ token: i1$1.FormBuilder }], target: i0.ɵɵFactoryTarget.Injectable }); }
686
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowEditorFormService }); }
687
+ }
688
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowEditorFormService, decorators: [{
689
+ type: Injectable
690
+ }], ctorParameters: () => [{ type: i1$1.FormBuilder }] });
691
+ /** Marks duplicated step keys invalid on the `steps` array. */
692
+ const uniqueStepKeysValidator = (control) => {
693
+ const keys = control.controls
694
+ .map(stepGroup => (stepGroup.get('key')?.value ?? '').trim())
695
+ .filter(key => key !== '');
696
+ const duplicates = keys.filter((key, index) => keys.indexOf(key) !== index);
697
+ return duplicates.length ? { duplicateStepKeys: { keys: [...new Set(duplicates)] } } : null;
698
+ };
699
+ /** The configured start step must reference an existing step. */
700
+ const startStepExistsValidator = (control) => {
701
+ const startStep = control.get('startStep')?.value;
702
+ if (!startStep)
703
+ return null;
704
+ const keys = control.get('steps').controls.map(stepGroup => stepGroup.get('key')?.value);
705
+ return keys.includes(startStep) ? null : { startStepMissing: { startStep } };
706
+ };
707
+ /** Every transition must point to an existing step. */
708
+ const transitionTargetsExistValidator = (control) => {
709
+ const steps = control.get('steps').controls;
710
+ const keys = steps.map(stepGroup => stepGroup.get('key')?.value);
711
+ const unknownTargets = steps.flatMap(stepGroup => stepGroup.get('nextSteps').controls
712
+ .map(transitionGroup => transitionGroup.get('step')?.value)
713
+ .filter(target => !!target && !keys.includes(target)));
714
+ return unknownTargets.length
715
+ ? { unknownTransitionTargets: { targets: [...new Set(unknownTargets)] } }
716
+ : null;
717
+ };
718
+ /**
719
+ * Transitions are evaluated in order and the first condition-less entry acts as the default, so at
720
+ * most one transition per step may omit its condition.
721
+ */
722
+ const singleDefaultTransitionValidator = (control) => {
723
+ const defaultCount = control.controls.filter(transitionGroup => !(transitionGroup.get('condition')?.value ?? '').trim()).length;
724
+ return defaultCount > 1 ? { multipleDefaultTransitions: true } : null;
725
+ };
726
+
727
+ /*
728
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
729
+ *
730
+ * Licensed under EUPL, Version 1.2 (the "License");
731
+ * you may not use this file except in compliance with the License.
732
+ * You may obtain a copy of the License at
733
+ *
734
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
735
+ *
736
+ * Unless required by applicable law or agreed to in writing, software
737
+ * distributed under the License is distributed on an "AS IS" basis,
738
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
739
+ * See the License for the specific language governing permissions and
740
+ * limitations under the License.
741
+ */
742
+ /**
743
+ * Translates a key and falls back to the given value when no translation exists (ngx-translate
744
+ * returns the key itself in that case). Used for registry-driven labels where only the well-known
745
+ * entries have translations.
746
+ */
747
+ function translateWithFallback(translateService, key, fallback) {
748
+ const translation = translateService.instant(key);
749
+ return translation === key ? fallback : translation;
750
+ }
751
+
752
+ /*
753
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
754
+ *
755
+ * Licensed under EUPL, Version 1.2 (the "License");
756
+ * you may not use this file except in compliance with the License.
757
+ * You may obtain a copy of the License at
758
+ *
759
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
760
+ *
761
+ * Unless required by applicable law or agreed to in writing, software
762
+ * distributed under the License is distributed on an "AS IS" basis,
763
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
764
+ * See the License for the specific language governing permissions and
765
+ * limitations under the License.
766
+ */
767
+
768
+ /*
769
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
770
+ *
771
+ * Licensed under EUPL, Version 1.2 (the "License");
772
+ * you may not use this file except in compliance with the License.
773
+ * You may obtain a copy of the License at
774
+ *
775
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
776
+ *
777
+ * Unless required by applicable law or agreed to in writing, software
778
+ * distributed under the License is distributed on an "AS IS" basis,
779
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
780
+ * See the License for the specific language governing permissions and
781
+ * limitations under the License.
782
+ */
783
+ /**
784
+ * Lists the `additionalProperties` entries that expressions can rely on, grouped by how the form
785
+ * flow is linked (user task or start event). The property names come from the form flow registry,
786
+ * so this always matches what the backend actually provides.
787
+ */
788
+ class FormFlowContextPropertiesComponent {
789
+ set registry(registry) {
790
+ const properties = registry?.additionalProperties ?? [];
791
+ this.groups = [
792
+ { labelKey: 'formFlow.uiEditor.additionalPropertiesUserTask', context: 'userTask' },
793
+ { labelKey: 'formFlow.uiEditor.additionalPropertiesStartEvent', context: 'startEvent' },
794
+ ]
795
+ .map(group => ({
796
+ labelKey: group.labelKey,
797
+ properties: properties.filter(property => property.context === group.context),
798
+ }))
799
+ .filter(group => group.properties.length > 0);
800
+ }
801
+ constructor(translateService) {
802
+ this.translateService = translateService;
803
+ this.groups = [];
804
+ }
805
+ getDescription(property) {
806
+ return translateWithFallback(this.translateService, `formFlow.uiEditor.additionalProperties.${property.name}`, '');
807
+ }
808
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowContextPropertiesComponent, deps: [{ token: i4.TranslateService }], target: i0.ɵɵFactoryTarget.Component }); }
809
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: FormFlowContextPropertiesComponent, isStandalone: true, selector: "valtimo-form-flow-context-properties", inputs: { registry: "registry" }, ngImport: i0, template: "<!--\n ~ Copyright 2015-2026 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n@if (groups.length) {\n <div class=\"context-properties\">\n <p class=\"context-properties__title\">\n {{ 'formFlow.uiEditor.additionalPropertiesTitle' | translate }}\n </p>\n\n @for (group of groups; track group.labelKey) {\n <p class=\"context-properties__group\">{{ group.labelKey | translate }}</p>\n\n <ul>\n @for (property of group.properties; track property.name) {\n <li>\n <code>additionalProperties.{{ property.name }}</code>\n\n @if (!property.alwaysPresent) {\n <span class=\"context-properties__optional\">{{\n 'formFlow.uiEditor.additionalPropertiesOptional' | translate\n }}</span>\n }\n\n @if (getDescription(property)) {\n \u2014 {{ getDescription(property) }}\n }\n </li>\n }\n </ul>\n }\n </div>\n}\n", styles: [".context-properties{display:flex;flex-direction:column;gap:var(--cds-spacing-02);color:var(--cds-text-secondary);font-size:var(--cds-label-01-font-size, .75rem)}.context-properties__title{font-weight:600}.context-properties__group{margin-top:var(--cds-spacing-02)}.context-properties__optional{color:var(--cds-text-helper);font-style:italic;margin-left:var(--cds-spacing-02)}.context-properties ul{display:flex;flex-direction:column;gap:var(--cds-spacing-02);list-style:disc;padding-left:var(--cds-spacing-05)}.context-properties code{font-family:IBM Plex Mono,monospace;color:var(--cds-text-primary)}\n/*!\n * Copyright 2015-2026 Ritense BV, the Netherlands.\n *\n * Licensed under EUPL, Version 1.2 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" basis,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i4.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
810
+ }
811
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowContextPropertiesComponent, decorators: [{
812
+ type: Component,
813
+ args: [{ standalone: true, selector: 'valtimo-form-flow-context-properties', changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, TranslateModule], template: "<!--\n ~ Copyright 2015-2026 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n@if (groups.length) {\n <div class=\"context-properties\">\n <p class=\"context-properties__title\">\n {{ 'formFlow.uiEditor.additionalPropertiesTitle' | translate }}\n </p>\n\n @for (group of groups; track group.labelKey) {\n <p class=\"context-properties__group\">{{ group.labelKey | translate }}</p>\n\n <ul>\n @for (property of group.properties; track property.name) {\n <li>\n <code>additionalProperties.{{ property.name }}</code>\n\n @if (!property.alwaysPresent) {\n <span class=\"context-properties__optional\">{{\n 'formFlow.uiEditor.additionalPropertiesOptional' | translate\n }}</span>\n }\n\n @if (getDescription(property)) {\n \u2014 {{ getDescription(property) }}\n }\n </li>\n }\n </ul>\n }\n </div>\n}\n", styles: [".context-properties{display:flex;flex-direction:column;gap:var(--cds-spacing-02);color:var(--cds-text-secondary);font-size:var(--cds-label-01-font-size, .75rem)}.context-properties__title{font-weight:600}.context-properties__group{margin-top:var(--cds-spacing-02)}.context-properties__optional{color:var(--cds-text-helper);font-style:italic;margin-left:var(--cds-spacing-02)}.context-properties ul{display:flex;flex-direction:column;gap:var(--cds-spacing-02);list-style:disc;padding-left:var(--cds-spacing-05)}.context-properties code{font-family:IBM Plex Mono,monospace;color:var(--cds-text-primary)}\n/*!\n * Copyright 2015-2026 Ritense BV, the Netherlands.\n *\n * Licensed under EUPL, Version 1.2 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" basis,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n"] }]
814
+ }], ctorParameters: () => [{ type: i4.TranslateService }], propDecorators: { registry: [{
815
+ type: Input
816
+ }] } });
817
+
818
+ /*
819
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
820
+ *
821
+ * Licensed under EUPL, Version 1.2 (the "License");
822
+ * you may not use this file except in compliance with the License.
823
+ * You may obtain a copy of the License at
824
+ *
825
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
826
+ *
827
+ * Unless required by applicable law or agreed to in writing, software
828
+ * distributed under the License is distributed on an "AS IS" basis,
829
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
830
+ * See the License for the specific language governing permissions and
831
+ * limitations under the License.
832
+ */
833
+ /**
834
+ * Explains how SpEL expressions work in a form flow — conditions, action hooks and the data they
835
+ * can access — in a modal, so the editor itself only needs a compact pointer to this help.
836
+ */
837
+ class FormFlowExpressionHelpModalComponent {
838
+ constructor() {
839
+ this.open = false;
840
+ this.registry = null;
841
+ this.closeEvent = new EventEmitter();
842
+ }
843
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowExpressionHelpModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
844
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: FormFlowExpressionHelpModalComponent, isStandalone: true, selector: "valtimo-form-flow-expression-help-modal", inputs: { open: "open", registry: "registry" }, outputs: { closeEvent: "closeEvent" }, ngImport: i0, template: "<!--\n ~ Copyright 2015-2026 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n<cds-modal\n size=\"md\"\n [open]=\"open\"\n [title]=\"'formFlow.uiEditor.help.title' | translate\"\n valtimoCdsModal\n (close)=\"closeEvent.emit()\"\n>\n <cds-modal-header [showCloseButton]=\"true\" (closeSelect)=\"closeEvent.emit()\">\n <h3 cdsModalHeaderHeading>{{ 'formFlow.uiEditor.help.title' | translate }}</h3>\n </cds-modal-header>\n\n <section cdsModalContent class=\"expression-help\">\n <p>{{ 'formFlow.uiEditor.help.intro' | translate }}</p>\n\n <h5>{{ 'formFlow.uiEditor.help.conditionsHeading' | translate }}</h5>\n\n <p>{{ 'formFlow.uiEditor.conditionExamplesTitle' | translate }}</p>\n\n <ul>\n <li>\n <code>{{ '${step.submissionData.age >= 21}' }}</code>\n \u2014 {{ 'formFlow.uiEditor.conditionExampleSubmission' | translate }}\n </li>\n\n <li>\n <code>{{ '${additionalProperties.documentId != null}' }}</code>\n \u2014 {{ 'formFlow.uiEditor.conditionExampleContext' | translate }}\n </li>\n </ul>\n\n <p>{{ 'formFlow.uiEditor.conditionExampleDefault' | translate }}</p>\n\n <h5>{{ 'formFlow.uiEditor.help.actionsHeading' | translate }}</h5>\n\n <p>{{ 'formFlow.uiEditor.help.actionsBody' | translate }}</p>\n\n <h5>{{ 'formFlow.uiEditor.help.dataHeading' | translate }}</h5>\n\n <valtimo-form-flow-context-properties\n [registry]=\"registry\"\n ></valtimo-form-flow-context-properties>\n </section>\n\n <cds-modal-footer>\n <button cdsButton=\"primary\" (click)=\"closeEvent.emit()\">\n {{ 'interface.close' | translate }}\n </button>\n </cds-modal-footer>\n</cds-modal>\n", styles: [".expression-help{display:flex;flex-direction:column;gap:var(--cds-spacing-04);color:var(--cds-text-primary)}.expression-help h5{margin-top:var(--cds-spacing-03)}.expression-help p{color:var(--cds-text-secondary)}.expression-help ul{display:flex;flex-direction:column;gap:var(--cds-spacing-02);list-style:disc;padding-left:var(--cds-spacing-05);color:var(--cds-text-secondary)}.expression-help code{font-family:IBM Plex Mono,monospace;color:var(--cds-text-primary)}\n/*!\n * Copyright 2015-2026 Ritense BV, the Netherlands.\n *\n * Licensed under EUPL, Version 1.2 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" basis,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i4.TranslatePipe, name: "translate" }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i2$1.Button, selector: "[cdsButton], [ibmButton]", inputs: ["ibmButton", "cdsButton", "size", "skeleton", "iconOnly", "isExpressive"] }, { kind: "ngmodule", type: ModalModule }, { kind: "component", type: i2$1.Modal, selector: "cds-modal, ibm-modal", inputs: ["size", "theme", "ariaLabel", "open", "trigger", "hasScrollingContent"], outputs: ["overlaySelected", "close"] }, { kind: "component", type: i2$1.ModalHeader, selector: "cds-modal-header, ibm-modal-header", inputs: ["theme", "closeLabel", "showCloseButton"], outputs: ["closeSelect"] }, { kind: "component", type: i2$1.ModalFooter, selector: "cds-modal-footer, ibm-modal-footer" }, { kind: "directive", type: i2$1.ModalContent, selector: "[cdsModalContent], [ibmModalContent]", inputs: ["hasForm"] }, { kind: "directive", type: i2$1.ModalHeaderHeading, selector: "[cdsModalHeaderHeading], [ibmModalHeaderHeading]" }, { kind: "directive", type: ValtimoCdsModalDirective, selector: "[valtimoCdsModal]", inputs: ["minContentHeight"] }, { kind: "component", type: FormFlowContextPropertiesComponent, selector: "valtimo-form-flow-context-properties", inputs: ["registry"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
845
+ }
846
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowExpressionHelpModalComponent, decorators: [{
847
+ type: Component,
848
+ args: [{ standalone: true, selector: 'valtimo-form-flow-expression-help-modal', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
849
+ CommonModule,
850
+ TranslateModule,
851
+ ButtonModule,
852
+ ModalModule,
853
+ ValtimoCdsModalDirective,
854
+ FormFlowContextPropertiesComponent,
855
+ ], template: "<!--\n ~ Copyright 2015-2026 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n<cds-modal\n size=\"md\"\n [open]=\"open\"\n [title]=\"'formFlow.uiEditor.help.title' | translate\"\n valtimoCdsModal\n (close)=\"closeEvent.emit()\"\n>\n <cds-modal-header [showCloseButton]=\"true\" (closeSelect)=\"closeEvent.emit()\">\n <h3 cdsModalHeaderHeading>{{ 'formFlow.uiEditor.help.title' | translate }}</h3>\n </cds-modal-header>\n\n <section cdsModalContent class=\"expression-help\">\n <p>{{ 'formFlow.uiEditor.help.intro' | translate }}</p>\n\n <h5>{{ 'formFlow.uiEditor.help.conditionsHeading' | translate }}</h5>\n\n <p>{{ 'formFlow.uiEditor.conditionExamplesTitle' | translate }}</p>\n\n <ul>\n <li>\n <code>{{ '${step.submissionData.age >= 21}' }}</code>\n \u2014 {{ 'formFlow.uiEditor.conditionExampleSubmission' | translate }}\n </li>\n\n <li>\n <code>{{ '${additionalProperties.documentId != null}' }}</code>\n \u2014 {{ 'formFlow.uiEditor.conditionExampleContext' | translate }}\n </li>\n </ul>\n\n <p>{{ 'formFlow.uiEditor.conditionExampleDefault' | translate }}</p>\n\n <h5>{{ 'formFlow.uiEditor.help.actionsHeading' | translate }}</h5>\n\n <p>{{ 'formFlow.uiEditor.help.actionsBody' | translate }}</p>\n\n <h5>{{ 'formFlow.uiEditor.help.dataHeading' | translate }}</h5>\n\n <valtimo-form-flow-context-properties\n [registry]=\"registry\"\n ></valtimo-form-flow-context-properties>\n </section>\n\n <cds-modal-footer>\n <button cdsButton=\"primary\" (click)=\"closeEvent.emit()\">\n {{ 'interface.close' | translate }}\n </button>\n </cds-modal-footer>\n</cds-modal>\n", styles: [".expression-help{display:flex;flex-direction:column;gap:var(--cds-spacing-04);color:var(--cds-text-primary)}.expression-help h5{margin-top:var(--cds-spacing-03)}.expression-help p{color:var(--cds-text-secondary)}.expression-help ul{display:flex;flex-direction:column;gap:var(--cds-spacing-02);list-style:disc;padding-left:var(--cds-spacing-05);color:var(--cds-text-secondary)}.expression-help code{font-family:IBM Plex Mono,monospace;color:var(--cds-text-primary)}\n/*!\n * Copyright 2015-2026 Ritense BV, the Netherlands.\n *\n * Licensed under EUPL, Version 1.2 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" basis,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n"] }]
856
+ }], propDecorators: { open: [{
857
+ type: Input
858
+ }], registry: [{
859
+ type: Input
860
+ }], closeEvent: [{
861
+ type: Output
862
+ }] } });
863
+
864
+ /*
865
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
866
+ *
867
+ * Licensed under EUPL, Version 1.2 (the "License");
868
+ * you may not use this file except in compliance with the License.
869
+ * You may obtain a copy of the License at
870
+ *
871
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
872
+ *
873
+ * Unless required by applicable law or agreed to in writing, software
874
+ * distributed under the License is distributed on an "AS IS" basis,
875
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
876
+ * See the License for the specific language governing permissions and
877
+ * limitations under the License.
878
+ */
879
+ /**
880
+ * Editable list of SpEL expressions for one of a step's lifecycle hooks (on open, on complete,
881
+ * on back). The add-menu offers the expression beans from the form flow registry as ready-made
882
+ * templates next to a blank expression.
883
+ */
884
+ class FormFlowExpressionListComponent {
885
+ set registry(registry) {
886
+ this.suggestions = (registry?.expressionBeans ?? []).flatMap(bean => bean.methods.map(method => {
887
+ const call = `${bean.name}.${this.formatMethod(method)}`;
888
+ return { label: call, expression: `\${${call}}` };
889
+ }));
890
+ }
891
+ constructor(formService, iconService) {
892
+ this.formService = formService;
893
+ this.iconService = iconService;
894
+ this.label = '';
895
+ this.description = '';
896
+ this.readOnly = false;
897
+ /** While false the admin is still modelling and empty-expression errors stay hidden; a save
898
+ * attempt flips it to reveal them. */
899
+ this.revealErrors = false;
900
+ this.suggestions = [];
901
+ this.testIds = FORM_FLOW_EDITOR_TEST_IDS;
902
+ this.iconService.registerAll([Add16, TrashCan16]);
903
+ }
904
+ get expressionControls() {
905
+ return this.expressions.controls;
906
+ }
907
+ addExpression(expression = '') {
908
+ this.expressions.push(this.formService.buildExpressionControl(expression));
909
+ this.expressions.markAsDirty();
910
+ }
911
+ removeExpression(index) {
912
+ this.expressions.removeAt(index);
913
+ this.expressions.markAsDirty();
914
+ }
915
+ formatMethod(method) {
916
+ return `${method.name}(${method.parameters.map(parameter => parameter.name).join(', ')})`;
917
+ }
918
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowExpressionListComponent, deps: [{ token: FormFlowEditorFormService }, { token: i2$1.IconService }], target: i0.ɵɵFactoryTarget.Component }); }
919
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: FormFlowExpressionListComponent, isStandalone: true, selector: "valtimo-form-flow-expression-list", inputs: { expressions: "expressions", label: "label", description: "description", readOnly: "readOnly", revealErrors: "revealErrors", registry: "registry" }, ngImport: i0, template: "<!--\n ~ Copyright 2015-2026 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n<div class=\"expression-list\">\n <div class=\"expression-list__header\">\n <h6>{{ label }}</h6>\n\n <p class=\"expression-list__description\">{{ description }}</p>\n </div>\n\n @for (control of expressionControls; track control; let index = $index) {\n <div class=\"expression-list__row\">\n <input\n cdsText\n [formControl]=\"control\"\n [invalid]=\"control.invalid && revealErrors\"\n [attr.data-test-id]=\"testIds.expressionInput\"\n placeholder=\"${valtimoFormFlow.completeTask(additionalProperties)}\"\n />\n\n @if (!readOnly) {\n <button\n cdsButton=\"ghost\"\n size=\"md\"\n [iconOnly]=\"true\"\n [attr.aria-label]=\"'interface.delete' | translate\"\n (click)=\"removeExpression(index)\"\n >\n <svg cdsIcon=\"trash-can\" size=\"16\"></svg>\n </button>\n }\n </div>\n } @empty {\n <p class=\"expression-list__empty\">{{ 'formFlow.uiEditor.noExpressions' | translate }}</p>\n }\n\n @if (!readOnly) {\n <div class=\"expression-list__add\">\n <v-overflow-menu placement=\"bottom-start\">\n <button\n cdsButton=\"tertiary\"\n size=\"sm\"\n overflowTrigger\n [attr.data-test-id]=\"testIds.addExpressionButton\"\n >\n {{ 'formFlow.uiEditor.addExpression' | translate }}\n\n <svg class=\"cds--btn__icon\" cdsIcon=\"add\" size=\"16\"></svg>\n </button>\n\n <v-overflow-menu-option (selected)=\"addExpression()\">{{\n 'formFlow.uiEditor.blankExpression' | translate\n }}</v-overflow-menu-option>\n\n @for (suggestion of suggestions; track suggestion.label) {\n <v-overflow-menu-option (selected)=\"addExpression(suggestion.expression)\">{{\n suggestion.label\n }}</v-overflow-menu-option>\n }\n </v-overflow-menu>\n </div>\n }\n</div>\n", styles: [".expression-list{display:flex;flex-direction:column;gap:var(--cds-spacing-03)}.expression-list__header h6{margin:0}.expression-list__description{color:var(--cds-text-secondary);font-size:var(--cds-label-01-font-size, .75rem);margin:var(--cds-spacing-02) 0 0 0}.expression-list__row{display:flex;align-items:center;gap:var(--cds-spacing-03)}.expression-list__row input{flex:1;font-family:IBM Plex Mono,monospace}.expression-list__empty{color:var(--cds-text-helper);font-size:var(--cds-body-compact-01-font-size, .875rem);margin:0}.expression-list__add{margin-top:var(--cds-spacing-03)}\n/*!\n * Copyright 2015-2026 Ritense BV, the Netherlands.\n *\n * Licensed under EUPL, Version 1.2 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" basis,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i4.TranslatePipe, name: "translate" }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i2$1.Button, selector: "[cdsButton], [ibmButton]", inputs: ["ibmButton", "cdsButton", "size", "skeleton", "iconOnly", "isExpressive"] }, { kind: "ngmodule", type: IconModule }, { kind: "directive", type: i2$1.IconDirective, selector: "[cdsIcon], [ibmIcon]", inputs: ["ibmIcon", "cdsIcon", "size", "title", "ariaLabel", "ariaLabelledBy", "ariaHidden", "isFocusable"] }, { kind: "ngmodule", type: InputModule }, { kind: "directive", type: i2$1.TextInput, selector: "[cdsText], [ibmText]", inputs: ["theme", "size", "invalid", "warn", "skeleton"] }, { kind: "component", type: OverflowMenuComponent, selector: "v-overflow-menu", inputs: ["open", "placement", "menuWidth", "offsetX", "offsetY", "closeOnSelect", "useHostAsReference", "portalToBody"], outputs: ["openChange"] }, { kind: "component", type: OverflowMenuOptionComponent, selector: "v-overflow-menu-option", inputs: ["disabled", "type", "testId", "optionId"], outputs: ["selected"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
920
+ }
921
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowExpressionListComponent, decorators: [{
922
+ type: Component,
923
+ args: [{ standalone: true, selector: 'valtimo-form-flow-expression-list', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
924
+ CommonModule,
925
+ ReactiveFormsModule,
926
+ TranslateModule,
927
+ ButtonModule,
928
+ IconModule,
929
+ InputModule,
930
+ OverflowMenuComponent,
931
+ OverflowMenuOptionComponent,
932
+ ], template: "<!--\n ~ Copyright 2015-2026 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n<div class=\"expression-list\">\n <div class=\"expression-list__header\">\n <h6>{{ label }}</h6>\n\n <p class=\"expression-list__description\">{{ description }}</p>\n </div>\n\n @for (control of expressionControls; track control; let index = $index) {\n <div class=\"expression-list__row\">\n <input\n cdsText\n [formControl]=\"control\"\n [invalid]=\"control.invalid && revealErrors\"\n [attr.data-test-id]=\"testIds.expressionInput\"\n placeholder=\"${valtimoFormFlow.completeTask(additionalProperties)}\"\n />\n\n @if (!readOnly) {\n <button\n cdsButton=\"ghost\"\n size=\"md\"\n [iconOnly]=\"true\"\n [attr.aria-label]=\"'interface.delete' | translate\"\n (click)=\"removeExpression(index)\"\n >\n <svg cdsIcon=\"trash-can\" size=\"16\"></svg>\n </button>\n }\n </div>\n } @empty {\n <p class=\"expression-list__empty\">{{ 'formFlow.uiEditor.noExpressions' | translate }}</p>\n }\n\n @if (!readOnly) {\n <div class=\"expression-list__add\">\n <v-overflow-menu placement=\"bottom-start\">\n <button\n cdsButton=\"tertiary\"\n size=\"sm\"\n overflowTrigger\n [attr.data-test-id]=\"testIds.addExpressionButton\"\n >\n {{ 'formFlow.uiEditor.addExpression' | translate }}\n\n <svg class=\"cds--btn__icon\" cdsIcon=\"add\" size=\"16\"></svg>\n </button>\n\n <v-overflow-menu-option (selected)=\"addExpression()\">{{\n 'formFlow.uiEditor.blankExpression' | translate\n }}</v-overflow-menu-option>\n\n @for (suggestion of suggestions; track suggestion.label) {\n <v-overflow-menu-option (selected)=\"addExpression(suggestion.expression)\">{{\n suggestion.label\n }}</v-overflow-menu-option>\n }\n </v-overflow-menu>\n </div>\n }\n</div>\n", styles: [".expression-list{display:flex;flex-direction:column;gap:var(--cds-spacing-03)}.expression-list__header h6{margin:0}.expression-list__description{color:var(--cds-text-secondary);font-size:var(--cds-label-01-font-size, .75rem);margin:var(--cds-spacing-02) 0 0 0}.expression-list__row{display:flex;align-items:center;gap:var(--cds-spacing-03)}.expression-list__row input{flex:1;font-family:IBM Plex Mono,monospace}.expression-list__empty{color:var(--cds-text-helper);font-size:var(--cds-body-compact-01-font-size, .875rem);margin:0}.expression-list__add{margin-top:var(--cds-spacing-03)}\n/*!\n * Copyright 2015-2026 Ritense BV, the Netherlands.\n *\n * Licensed under EUPL, Version 1.2 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" basis,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n"] }]
933
+ }], ctorParameters: () => [{ type: FormFlowEditorFormService }, { type: i2$1.IconService }], propDecorators: { expressions: [{
934
+ type: Input
935
+ }], label: [{
936
+ type: Input
937
+ }], description: [{
938
+ type: Input
939
+ }], readOnly: [{
940
+ type: Input
941
+ }], revealErrors: [{
942
+ type: Input
943
+ }], registry: [{
944
+ type: Input
945
+ }] } });
946
+
947
+ /*
948
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
949
+ *
950
+ * Licensed under EUPL, Version 1.2 (the "License");
951
+ * you may not use this file except in compliance with the License.
952
+ * You may obtain a copy of the License at
953
+ *
954
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
955
+ *
956
+ * Unless required by applicable law or agreed to in writing, software
957
+ * distributed under the License is distributed on an "AS IS" basis,
958
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
959
+ * See the License for the specific language governing permissions and
960
+ * limitations under the License.
961
+ */
962
+ /**
963
+ * Editable list of a step's outgoing transitions. Transitions are evaluated top to bottom — the
964
+ * first entry whose condition holds is taken and a condition-less entry acts as the default — so
965
+ * the rows can be reordered.
966
+ */
967
+ class FormFlowTransitionListComponent {
968
+ constructor(formService, iconService) {
969
+ this.formService = formService;
970
+ this.iconService = iconService;
971
+ this.stepKeys = [];
972
+ this.readOnly = false;
973
+ /** While false the admin is still modelling and empty-target errors stay hidden; a save attempt
974
+ * flips it to reveal them. */
975
+ this.revealErrors = false;
976
+ this.testIds = FORM_FLOW_EDITOR_TEST_IDS;
977
+ // Per-row dropdown items, cached per transition group. The Carbon dropdown resets its visual
978
+ // selection whenever its items array is swapped, so the array is only rebuilt when the step keys
979
+ // or the row's own selection actually change.
980
+ this._rowItems = new WeakMap();
981
+ this.iconService.registerAll([Add16, ArrowDown16, ArrowUp16, TrashCan16]);
982
+ }
983
+ ngOnChanges(changes) {
984
+ if (changes['stepKeys']) {
985
+ // A new keys reference invalidates every cached row; rows lazily rebuild on the next read.
986
+ this.transitionGroups.forEach(group => this._rowItems.delete(group));
987
+ }
988
+ }
989
+ get transitionGroups() {
990
+ return this.transitions.controls;
991
+ }
992
+ getRowItems(transitionGroup) {
993
+ const value = transitionGroup.get('step')?.value ?? '';
994
+ const cached = this._rowItems.get(transitionGroup);
995
+ if (cached && cached.keys === this.stepKeys && cached.value === value) {
996
+ return cached.items;
997
+ }
998
+ const items = this.stepKeys.map(key => ({ content: key, id: key, selected: key === value }));
999
+ this._rowItems.set(transitionGroup, { keys: this.stepKeys, value, items });
1000
+ return items;
1001
+ }
1002
+ addTransition() {
1003
+ this.transitions.push(this.formService.buildTransitionGroup());
1004
+ this.transitions.markAsDirty();
1005
+ }
1006
+ removeTransition(index) {
1007
+ this.transitions.removeAt(index);
1008
+ this.transitions.markAsDirty();
1009
+ }
1010
+ moveTransition(index, offset) {
1011
+ const target = index + offset;
1012
+ if (target < 0 || target >= this.transitions.length)
1013
+ return;
1014
+ const control = this.transitions.at(index);
1015
+ this.transitions.removeAt(index);
1016
+ this.transitions.insert(target, control);
1017
+ this.transitions.markAsDirty();
1018
+ }
1019
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowTransitionListComponent, deps: [{ token: FormFlowEditorFormService }, { token: i2$1.IconService }], target: i0.ɵɵFactoryTarget.Component }); }
1020
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: FormFlowTransitionListComponent, isStandalone: true, selector: "valtimo-form-flow-transition-list", inputs: { transitions: "transitions", stepKeys: "stepKeys", readOnly: "readOnly", revealErrors: "revealErrors" }, usesOnChanges: true, ngImport: i0, template: "<!--\n ~ Copyright 2015-2026 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n<div class=\"transition-list\">\n @for (transitionGroup of transitionGroups; track transitionGroup; let index = $index) {\n <div\n class=\"transition-list__row\"\n [formGroup]=\"transitionGroup\"\n [attr.data-test-id]=\"testIds.transitionRow\"\n >\n <div class=\"transition-list__target\">\n <cds-dropdown\n formControlName=\"step\"\n itemValueKey=\"id\"\n [invalid]=\"transitionGroup.get('step')?.invalid && revealErrors\"\n [label]=\"targetStepLabelTemplate\"\n [placeholder]=\"'formFlow.uiEditor.selectStep' | translate\"\n >\n <cds-dropdown-list [items]=\"getRowItems(transitionGroup)\"></cds-dropdown-list>\n </cds-dropdown>\n\n <ng-template #targetStepLabelTemplate>\n <span class=\"transition-list__label\">\n {{ 'formFlow.uiEditor.targetStep' | translate }}\n\n <v-tooltip-icon\n [tooltip]=\"'formFlow.uiEditor.fieldTooltips.targetStep' | translate\"\n ></v-tooltip-icon>\n </span>\n </ng-template>\n </div>\n\n <cds-label class=\"transition-list__condition\">\n <span class=\"transition-list__label\">\n {{ 'formFlow.uiEditor.condition' | translate }}\n\n <v-tooltip-icon\n [tooltip]=\"'formFlow.uiEditor.fieldTooltips.condition' | translate\"\n ></v-tooltip-icon>\n </span>\n\n <input\n cdsText\n formControlName=\"condition\"\n placeholder=\"{{ 'formFlow.uiEditor.conditionPlaceholder' | translate }}\"\n />\n </cds-label>\n\n @if (!readOnly) {\n <div class=\"transition-list__actions\">\n <button\n cdsButton=\"ghost\"\n size=\"md\"\n [iconOnly]=\"true\"\n [disabled]=\"index === 0\"\n [attr.aria-label]=\"'formFlow.uiEditor.moveUp' | translate\"\n (click)=\"moveTransition(index, -1)\"\n >\n <svg cdsIcon=\"arrow--up\" size=\"16\"></svg>\n </button>\n\n <button\n cdsButton=\"ghost\"\n size=\"md\"\n [iconOnly]=\"true\"\n [disabled]=\"index === transitionGroups.length - 1\"\n [attr.aria-label]=\"'formFlow.uiEditor.moveDown' | translate\"\n (click)=\"moveTransition(index, 1)\"\n >\n <svg cdsIcon=\"arrow--down\" size=\"16\"></svg>\n </button>\n\n <button\n cdsButton=\"ghost\"\n size=\"md\"\n [iconOnly]=\"true\"\n [attr.aria-label]=\"'interface.delete' | translate\"\n (click)=\"removeTransition(index)\"\n >\n <svg cdsIcon=\"trash-can\" size=\"16\"></svg>\n </button>\n </div>\n }\n </div>\n } @empty {\n <p class=\"transition-list__empty\">{{ 'formFlow.uiEditor.noTransitions' | translate }}</p>\n }\n\n @if (revealErrors && transitions.hasError('multipleDefaultTransitions')) {\n <p class=\"transition-list__error\">\n {{ 'formFlow.uiEditor.multipleDefaultTransitions' | translate }}\n </p>\n }\n\n @if (!readOnly) {\n <div>\n <button\n cdsButton=\"tertiary\"\n size=\"sm\"\n [attr.data-test-id]=\"testIds.addTransitionButton\"\n (click)=\"addTransition()\"\n >\n {{ 'formFlow.uiEditor.addTransition' | translate }}\n\n <svg class=\"cds--btn__icon\" cdsIcon=\"add\" size=\"16\"></svg>\n </button>\n </div>\n }\n</div>\n", styles: [".transition-list{display:flex;flex-direction:column;gap:var(--cds-spacing-04)}.transition-list__row{display:flex;align-items:flex-end;gap:var(--cds-spacing-04);padding:var(--cds-spacing-04);border:1px solid var(--cds-border-subtle)}.transition-list__target{flex:0 0 220px}.transition-list__condition{flex:1;margin-bottom:0}.transition-list__condition input{font-family:IBM Plex Mono,monospace}.transition-list__actions{display:flex;align-items:center}.transition-list__label{display:inline-flex;align-items:center;gap:var(--cds-spacing-02)}.transition-list__empty{color:var(--cds-text-helper);font-size:var(--cds-body-compact-01-font-size, .875rem);margin:0}.transition-list__error{color:var(--cds-text-error);font-size:var(--cds-label-01-font-size, .75rem);margin:0}\n/*!\n * Copyright 2015-2026 Ritense BV, the Netherlands.\n *\n * Licensed under EUPL, Version 1.2 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" basis,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i4.TranslatePipe, name: "translate" }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i2$1.Button, selector: "[cdsButton], [ibmButton]", inputs: ["ibmButton", "cdsButton", "size", "skeleton", "iconOnly", "isExpressive"] }, { kind: "ngmodule", type: DropdownModule }, { kind: "component", type: i2$1.Dropdown, selector: "cds-dropdown, ibm-dropdown", inputs: ["id", "label", "hideLabel", "helperText", "placeholder", "displayValue", "clearText", "size", "type", "theme", "disabled", "readonly", "skeleton", "inline", "disableArrowKeys", "invalid", "invalidText", "warn", "warnText", "appendInline", "scrollableContainer", "itemValueKey", "selectionFeedback", "menuButtonLabel", "selectedLabel", "dropUp", "fluid"], outputs: ["selected", "onClose", "close"] }, { kind: "component", type: i2$1.DropdownList, selector: "cds-dropdown-list, ibm-dropdown-list", inputs: ["ariaLabel", "items", "listTpl", "type", "showTitles"], outputs: ["select", "scroll", "blurIntent"] }, { kind: "ngmodule", type: IconModule }, { kind: "directive", type: i2$1.IconDirective, selector: "[cdsIcon], [ibmIcon]", inputs: ["ibmIcon", "cdsIcon", "size", "title", "ariaLabel", "ariaLabelledBy", "ariaHidden", "isFocusable"] }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i2$1.Label, selector: "cds-label, ibm-label", inputs: ["labelInputID", "disabled", "skeleton", "helperText", "invalidText", "invalid", "warn", "warnText", "ariaLabel"] }, { kind: "directive", type: i2$1.TextInput, selector: "[cdsText], [ibmText]", inputs: ["theme", "size", "invalid", "warn", "skeleton"] }, { kind: "ngmodule", type: TooltipIconModule }, { kind: "component", type: i1$2.TooltipIconComponent, selector: "v-tooltip-icon", inputs: ["tooltip", "disabled"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1021
+ }
1022
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowTransitionListComponent, decorators: [{
1023
+ type: Component,
1024
+ args: [{ standalone: true, selector: 'valtimo-form-flow-transition-list', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
1025
+ CommonModule,
1026
+ ReactiveFormsModule,
1027
+ TranslateModule,
1028
+ ButtonModule,
1029
+ DropdownModule,
1030
+ IconModule,
1031
+ InputModule,
1032
+ TooltipIconModule,
1033
+ ], template: "<!--\n ~ Copyright 2015-2026 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n<div class=\"transition-list\">\n @for (transitionGroup of transitionGroups; track transitionGroup; let index = $index) {\n <div\n class=\"transition-list__row\"\n [formGroup]=\"transitionGroup\"\n [attr.data-test-id]=\"testIds.transitionRow\"\n >\n <div class=\"transition-list__target\">\n <cds-dropdown\n formControlName=\"step\"\n itemValueKey=\"id\"\n [invalid]=\"transitionGroup.get('step')?.invalid && revealErrors\"\n [label]=\"targetStepLabelTemplate\"\n [placeholder]=\"'formFlow.uiEditor.selectStep' | translate\"\n >\n <cds-dropdown-list [items]=\"getRowItems(transitionGroup)\"></cds-dropdown-list>\n </cds-dropdown>\n\n <ng-template #targetStepLabelTemplate>\n <span class=\"transition-list__label\">\n {{ 'formFlow.uiEditor.targetStep' | translate }}\n\n <v-tooltip-icon\n [tooltip]=\"'formFlow.uiEditor.fieldTooltips.targetStep' | translate\"\n ></v-tooltip-icon>\n </span>\n </ng-template>\n </div>\n\n <cds-label class=\"transition-list__condition\">\n <span class=\"transition-list__label\">\n {{ 'formFlow.uiEditor.condition' | translate }}\n\n <v-tooltip-icon\n [tooltip]=\"'formFlow.uiEditor.fieldTooltips.condition' | translate\"\n ></v-tooltip-icon>\n </span>\n\n <input\n cdsText\n formControlName=\"condition\"\n placeholder=\"{{ 'formFlow.uiEditor.conditionPlaceholder' | translate }}\"\n />\n </cds-label>\n\n @if (!readOnly) {\n <div class=\"transition-list__actions\">\n <button\n cdsButton=\"ghost\"\n size=\"md\"\n [iconOnly]=\"true\"\n [disabled]=\"index === 0\"\n [attr.aria-label]=\"'formFlow.uiEditor.moveUp' | translate\"\n (click)=\"moveTransition(index, -1)\"\n >\n <svg cdsIcon=\"arrow--up\" size=\"16\"></svg>\n </button>\n\n <button\n cdsButton=\"ghost\"\n size=\"md\"\n [iconOnly]=\"true\"\n [disabled]=\"index === transitionGroups.length - 1\"\n [attr.aria-label]=\"'formFlow.uiEditor.moveDown' | translate\"\n (click)=\"moveTransition(index, 1)\"\n >\n <svg cdsIcon=\"arrow--down\" size=\"16\"></svg>\n </button>\n\n <button\n cdsButton=\"ghost\"\n size=\"md\"\n [iconOnly]=\"true\"\n [attr.aria-label]=\"'interface.delete' | translate\"\n (click)=\"removeTransition(index)\"\n >\n <svg cdsIcon=\"trash-can\" size=\"16\"></svg>\n </button>\n </div>\n }\n </div>\n } @empty {\n <p class=\"transition-list__empty\">{{ 'formFlow.uiEditor.noTransitions' | translate }}</p>\n }\n\n @if (revealErrors && transitions.hasError('multipleDefaultTransitions')) {\n <p class=\"transition-list__error\">\n {{ 'formFlow.uiEditor.multipleDefaultTransitions' | translate }}\n </p>\n }\n\n @if (!readOnly) {\n <div>\n <button\n cdsButton=\"tertiary\"\n size=\"sm\"\n [attr.data-test-id]=\"testIds.addTransitionButton\"\n (click)=\"addTransition()\"\n >\n {{ 'formFlow.uiEditor.addTransition' | translate }}\n\n <svg class=\"cds--btn__icon\" cdsIcon=\"add\" size=\"16\"></svg>\n </button>\n </div>\n }\n</div>\n", styles: [".transition-list{display:flex;flex-direction:column;gap:var(--cds-spacing-04)}.transition-list__row{display:flex;align-items:flex-end;gap:var(--cds-spacing-04);padding:var(--cds-spacing-04);border:1px solid var(--cds-border-subtle)}.transition-list__target{flex:0 0 220px}.transition-list__condition{flex:1;margin-bottom:0}.transition-list__condition input{font-family:IBM Plex Mono,monospace}.transition-list__actions{display:flex;align-items:center}.transition-list__label{display:inline-flex;align-items:center;gap:var(--cds-spacing-02)}.transition-list__empty{color:var(--cds-text-helper);font-size:var(--cds-body-compact-01-font-size, .875rem);margin:0}.transition-list__error{color:var(--cds-text-error);font-size:var(--cds-label-01-font-size, .75rem);margin:0}\n/*!\n * Copyright 2015-2026 Ritense BV, the Netherlands.\n *\n * Licensed under EUPL, Version 1.2 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" basis,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n"] }]
1034
+ }], ctorParameters: () => [{ type: FormFlowEditorFormService }, { type: i2$1.IconService }], propDecorators: { transitions: [{
1035
+ type: Input
1036
+ }], stepKeys: [{
1037
+ type: Input
1038
+ }], readOnly: [{
1039
+ type: Input
1040
+ }], revealErrors: [{
1041
+ type: Input
1042
+ }] } });
1043
+
1044
+ /*
1045
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
1046
+ *
1047
+ * Licensed under EUPL, Version 1.2 (the "License");
1048
+ * you may not use this file except in compliance with the License.
1049
+ * You may obtain a copy of the License at
1050
+ *
1051
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
1052
+ *
1053
+ * Unless required by applicable law or agreed to in writing, software
1054
+ * distributed under the License is distributed on an "AS IS" basis,
1055
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1056
+ * See the License for the specific language governing permissions and
1057
+ * limitations under the License.
1058
+ */
1059
+ /**
1060
+ * Detail editor for a single form flow step: its key, title, type with type-specific properties,
1061
+ * outgoing transitions and the three lifecycle expression lists.
1062
+ */
1063
+ class FormFlowStepDetailComponent {
1064
+ constructor(formService, iconService, translateService) {
1065
+ this.formService = formService;
1066
+ this.iconService = iconService;
1067
+ this.translateService = translateService;
1068
+ this.registry = null;
1069
+ this.stepKeys = [];
1070
+ this.formOptions = [];
1071
+ this.componentOptions = [];
1072
+ this.isStartStep = false;
1073
+ this.readOnly = false;
1074
+ this.duplicateKey = false;
1075
+ /** When false the admin is still modelling and required-field errors stay hidden; a save attempt
1076
+ * flips it to reveal them. */
1077
+ this.revealErrors = false;
1078
+ this.deleteStepEvent = new EventEmitter();
1079
+ this.makeStartEvent = new EventEmitter();
1080
+ this.stepTypeItems = [];
1081
+ this.$showHelpModal = signal(false);
1082
+ // The "how do expressions work" action on the inline help notifications, opening the modal that
1083
+ // holds the full explanation.
1084
+ this.helpActions = [];
1085
+ this.testIds = FORM_FLOW_EDITOR_TEST_IDS;
1086
+ // Cached per property: the Carbon dropdown resets its visual selection whenever its items array
1087
+ // is swapped, so an array is only rebuilt when its options or its selection change.
1088
+ this._selectItemsCache = new Map();
1089
+ this._typeSubscription = new Subscription();
1090
+ this.iconService.registerAll([Flag16, TrashCan16]);
1091
+ }
1092
+ ngOnInit() {
1093
+ this.helpActions = [
1094
+ {
1095
+ text: this.translateService.instant('formFlow.uiEditor.help.button'),
1096
+ click: () => this.$showHelpModal.set(true),
1097
+ },
1098
+ ];
1099
+ }
1100
+ ngOnChanges(changes) {
1101
+ if (changes['stepGroup'] || changes['registry'] || changes['componentOptions']) {
1102
+ this.buildStepTypeItems();
1103
+ }
1104
+ if (changes['stepGroup'] || changes['registry']) {
1105
+ this.openTypeSubscription();
1106
+ }
1107
+ }
1108
+ ngOnDestroy() {
1109
+ this._typeSubscription.unsubscribe();
1110
+ }
1111
+ get propertiesGroup() {
1112
+ return this.stepGroup.get('properties');
1113
+ }
1114
+ get propertyNames() {
1115
+ return Object.keys(this.propertiesGroup.controls);
1116
+ }
1117
+ get transitions() {
1118
+ return this.stepGroup.get('nextSteps');
1119
+ }
1120
+ getExpressions(hook) {
1121
+ return this.stepGroup.get(hook);
1122
+ }
1123
+ isControlInvalid(name) {
1124
+ const control = this.stepGroup.get(name);
1125
+ return !!control && control.invalid && this.revealErrors;
1126
+ }
1127
+ getPropertyLabel(name) {
1128
+ return translateWithFallback(this.translateService, `formFlow.uiEditor.properties.${name}`, name);
1129
+ }
1130
+ // The `definition` property of the built-in `form` step type references a form of the
1131
+ // surrounding case definition or building block, so it is offered as a choice list.
1132
+ isFormDefinitionProperty(name) {
1133
+ return name === 'definition' && this.formOptions.length > 0;
1134
+ }
1135
+ getFormDefinitionItems() {
1136
+ return this.getSelectItems('definition', this.formOptions);
1137
+ }
1138
+ // The `componentId` property of the built-in `custom-component` step type references an Angular
1139
+ // component registered by the implementation, so the registered ids are offered as a choice list.
1140
+ isCustomComponentProperty(name) {
1141
+ return name === 'componentId' && this.componentOptions.length > 0;
1142
+ }
1143
+ getCustomComponentItems() {
1144
+ return this.getSelectItems('componentId', this.componentOptions);
1145
+ }
1146
+ getSelectItems(propertyName, options) {
1147
+ const value = this.propertiesGroup.get(propertyName)?.value ?? '';
1148
+ const cached = this._selectItemsCache.get(propertyName);
1149
+ if (cached && cached.options === options && cached.value === value) {
1150
+ return cached.items;
1151
+ }
1152
+ // A value that is no longer available stays selectable, so opening an older definition never
1153
+ // silently changes it.
1154
+ const names = options.includes(value) || !value ? options : [...options, value];
1155
+ const items = names.map(name => ({ content: name, id: name, selected: name === value }));
1156
+ this._selectItemsCache.set(propertyName, { options, value, items });
1157
+ return items;
1158
+ }
1159
+ buildStepTypeItems() {
1160
+ const registryTypeNames = (this.registry?.stepTypes ?? []).map(stepType => stepType.name);
1161
+ const currentTypeName = this.stepGroup?.get('typeName')?.value;
1162
+ const typeNames = currentTypeName && !registryTypeNames.includes(currentTypeName)
1163
+ ? [...registryTypeNames, currentTypeName]
1164
+ : registryTypeNames;
1165
+ this.stepTypeItems = typeNames.map(name => ({
1166
+ content: name,
1167
+ id: name,
1168
+ selected: name === currentTypeName,
1169
+ // `custom-component` steps need a component registered by the implementation; without any,
1170
+ // the type cannot be configured (unless the step already uses it).
1171
+ disabled: name === 'custom-component' &&
1172
+ this.componentOptions.length === 0 &&
1173
+ currentTypeName !== 'custom-component',
1174
+ }));
1175
+ }
1176
+ getTypeTooltip() {
1177
+ const tooltip = this.translateService.instant('formFlow.uiEditor.fieldTooltips.type');
1178
+ return this.componentOptions.length === 0
1179
+ ? `${tooltip} ${this.translateService.instant('formFlow.uiEditor.fieldTooltips.typeNoComponents')}`
1180
+ : tooltip;
1181
+ }
1182
+ // Selecting another step type swaps the properties group for the controls that type needs,
1183
+ // keeping the values of properties both types share.
1184
+ openTypeSubscription() {
1185
+ this._typeSubscription.unsubscribe();
1186
+ this._typeSubscription = new Subscription();
1187
+ if (!this.stepGroup || !this.registry)
1188
+ return;
1189
+ this._typeSubscription.add(this.stepGroup.get('typeName')?.valueChanges.subscribe(typeName => {
1190
+ const currentValues = this.propertiesGroup.getRawValue();
1191
+ this.stepGroup.setControl('properties', this.formService.buildPropertiesGroup(typeName, currentValues, this.registry));
1192
+ }));
1193
+ }
1194
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowStepDetailComponent, deps: [{ token: FormFlowEditorFormService }, { token: i2$1.IconService }, { token: i4.TranslateService }], target: i0.ɵɵFactoryTarget.Component }); }
1195
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: FormFlowStepDetailComponent, isStandalone: true, selector: "valtimo-form-flow-step-detail", inputs: { stepGroup: "stepGroup", registry: "registry", stepKeys: "stepKeys", formOptions: "formOptions", componentOptions: "componentOptions", isStartStep: "isStartStep", readOnly: "readOnly", duplicateKey: "duplicateKey", revealErrors: "revealErrors" }, outputs: { deleteStepEvent: "deleteStepEvent", makeStartEvent: "makeStartEvent" }, usesOnChanges: true, ngImport: i0, template: "<!--\n ~ Copyright 2015-2026 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n<div class=\"step-detail\" [cdsLayer]=\"1\" [formGroup]=\"stepGroup\">\n <div class=\"step-detail__header\">\n <div class=\"step-detail__heading\">\n <h5>{{ 'formFlow.uiEditor.stepDetails' | translate }}</h5>\n\n @if (isStartStep) {\n <cds-tag type=\"green\" size=\"sm\">{{ 'formFlow.uiEditor.startStep' | translate }}</cds-tag>\n }\n </div>\n\n @if (!readOnly) {\n <div class=\"step-detail__header-actions\">\n @if (!isStartStep) {\n <button\n cdsButton=\"tertiary\"\n size=\"sm\"\n [attr.data-test-id]=\"testIds.makeStartStepButton\"\n (click)=\"makeStartEvent.emit()\"\n >\n {{ 'formFlow.uiEditor.makeStartStep' | translate }}\n\n <svg class=\"cds--btn__icon\" cdsIcon=\"flag\" size=\"16\"></svg>\n </button>\n }\n\n <button\n cdsButton=\"danger--ghost\"\n size=\"sm\"\n [attr.data-test-id]=\"testIds.deleteStepButton\"\n (click)=\"deleteStepEvent.emit()\"\n >\n {{ 'formFlow.uiEditor.deleteStep' | translate }}\n\n <svg class=\"cds--btn__icon\" cdsIcon=\"trash-can\" size=\"16\"></svg>\n </button>\n </div>\n }\n </div>\n\n <div class=\"step-detail__grid\">\n <cds-label\n [invalid]=\"isControlInvalid('key') || (duplicateKey && revealErrors)\"\n [invalidText]=\"\n duplicateKey\n ? ('formFlow.uiEditor.duplicateKey' | translate)\n : ('formFlow.uiEditor.required' | translate)\n \"\n >\n <span class=\"step-detail__label\">\n {{ 'formFlow.key' | translate }}\n\n <v-tooltip-icon\n [tooltip]=\"'formFlow.uiEditor.fieldTooltips.key' | translate\"\n ></v-tooltip-icon>\n </span>\n\n <input\n cdsText\n formControlName=\"key\"\n [invalid]=\"isControlInvalid('key') || (duplicateKey && revealErrors)\"\n [attr.data-test-id]=\"testIds.stepKeyInput\"\n placeholder=\"{{ 'formFlow.uiEditor.keyPlaceholder' | translate }}\"\n />\n </cds-label>\n\n <cds-label>\n <span class=\"step-detail__label\">\n {{ 'formFlow.uiEditor.title' | translate }}\n\n <v-tooltip-icon\n [tooltip]=\"'formFlow.uiEditor.fieldTooltips.title' | translate\"\n ></v-tooltip-icon>\n </span>\n\n <input\n cdsText\n formControlName=\"title\"\n [attr.data-test-id]=\"testIds.stepTitleInput\"\n placeholder=\"{{ 'formFlow.uiEditor.titlePlaceholder' | translate }}\"\n />\n </cds-label>\n\n <div [attr.data-test-id]=\"testIds.stepTypeDropdown\">\n <cds-dropdown formControlName=\"typeName\" itemValueKey=\"id\" [label]=\"stepTypeLabelTemplate\">\n <cds-dropdown-list [items]=\"stepTypeItems\"></cds-dropdown-list>\n </cds-dropdown>\n\n <ng-template #stepTypeLabelTemplate>\n <span class=\"step-detail__label\">\n {{ 'formFlow.uiEditor.stepType' | translate }}\n\n <v-tooltip-icon [tooltip]=\"getTypeTooltip()\"></v-tooltip-icon>\n </span>\n </ng-template>\n </div>\n\n <ng-container formGroupName=\"properties\">\n @for (propertyName of propertyNames; track propertyName) {\n @if (isFormDefinitionProperty(propertyName)) {\n <div [attr.data-test-id]=\"testIds.stepPropertyDropdown\">\n <cds-dropdown\n formControlName=\"definition\"\n itemValueKey=\"id\"\n [invalid]=\"propertiesGroup.get('definition')?.invalid && revealErrors\"\n [label]=\"formDefinitionLabelTemplate\"\n [placeholder]=\"'formFlow.uiEditor.selectForm' | translate\"\n >\n <cds-dropdown-list [items]=\"getFormDefinitionItems()\"></cds-dropdown-list>\n </cds-dropdown>\n\n <ng-template #formDefinitionLabelTemplate>\n <span class=\"step-detail__label\">\n {{ getPropertyLabel(propertyName) }}\n\n <v-tooltip-icon\n [tooltip]=\"'formFlow.uiEditor.fieldTooltips.definition' | translate\"\n ></v-tooltip-icon>\n </span>\n </ng-template>\n </div>\n } @else if (isCustomComponentProperty(propertyName)) {\n <div [attr.data-test-id]=\"testIds.stepPropertyDropdown\">\n <cds-dropdown\n formControlName=\"componentId\"\n itemValueKey=\"id\"\n [invalid]=\"propertiesGroup.get('componentId')?.invalid && revealErrors\"\n [label]=\"componentIdLabelTemplate\"\n [placeholder]=\"'formFlow.uiEditor.selectComponent' | translate\"\n >\n <cds-dropdown-list [items]=\"getCustomComponentItems()\"></cds-dropdown-list>\n </cds-dropdown>\n\n <ng-template #componentIdLabelTemplate>\n <span class=\"step-detail__label\">\n {{ getPropertyLabel(propertyName) }}\n\n <v-tooltip-icon\n [tooltip]=\"'formFlow.uiEditor.fieldTooltips.componentId' | translate\"\n ></v-tooltip-icon>\n </span>\n </ng-template>\n </div>\n } @else {\n <cds-label\n [invalid]=\"propertiesGroup.get(propertyName)?.invalid && revealErrors\"\n [invalidText]=\"'formFlow.uiEditor.required' | translate\"\n >\n {{ getPropertyLabel(propertyName) }}\n\n <input\n cdsText\n [formControlName]=\"propertyName\"\n [invalid]=\"propertiesGroup.get(propertyName)?.invalid && revealErrors\"\n [attr.data-test-id]=\"testIds.stepPropertyInput\"\n />\n </cds-label>\n }\n }\n </ng-container>\n </div>\n\n <hr class=\"step-detail__divider\" />\n\n <section class=\"step-detail__section\">\n <div class=\"step-detail__section-header\">\n <h5>{{ 'formFlow.uiEditor.navigation' | translate }}</h5>\n\n <p class=\"step-detail__section-description\">\n {{ 'formFlow.uiEditor.navigationDescription' | translate }}\n </p>\n </div>\n\n <valtimo-form-flow-transition-list\n [readOnly]=\"readOnly\"\n [revealErrors]=\"revealErrors\"\n [stepKeys]=\"stepKeys\"\n [transitions]=\"transitions\"\n ></valtimo-form-flow-transition-list>\n\n <cds-actionable-notification\n class=\"step-detail__help-notification\"\n [notificationObj]=\"{\n type: 'info',\n lowContrast: true,\n showClose: false,\n title: 'formFlow.uiEditor.help.conditionsInlineTitle' | translate,\n message: 'formFlow.uiEditor.help.conditionsInlineMessage' | translate,\n actions: helpActions,\n }\"\n ></cds-actionable-notification>\n </section>\n\n <hr class=\"step-detail__divider\" />\n\n <section class=\"step-detail__section\">\n <div class=\"step-detail__section-header\">\n <h5>{{ 'formFlow.uiEditor.actions' | translate }}</h5>\n\n <p class=\"step-detail__section-description\">\n {{ 'formFlow.uiEditor.actionsDescription' | translate }}\n </p>\n </div>\n\n <cds-actionable-notification\n class=\"step-detail__help-notification\"\n [notificationObj]=\"{\n type: 'info',\n lowContrast: true,\n showClose: false,\n title: 'formFlow.uiEditor.help.actionsInlineTitle' | translate,\n message: 'formFlow.uiEditor.help.actionsInlineMessage' | translate,\n actions: helpActions,\n }\"\n ></cds-actionable-notification>\n\n <valtimo-form-flow-expression-list\n [description]=\"'formFlow.uiEditor.onOpenDescription' | translate\"\n [expressions]=\"getExpressions('onOpen')\"\n [label]=\"'formFlow.uiEditor.onOpen' | translate\"\n [readOnly]=\"readOnly\"\n [registry]=\"registry\"\n [revealErrors]=\"revealErrors\"\n ></valtimo-form-flow-expression-list>\n\n <valtimo-form-flow-expression-list\n [description]=\"'formFlow.uiEditor.onCompleteDescription' | translate\"\n [expressions]=\"getExpressions('onComplete')\"\n [label]=\"'formFlow.uiEditor.onComplete' | translate\"\n [readOnly]=\"readOnly\"\n [registry]=\"registry\"\n [revealErrors]=\"revealErrors\"\n ></valtimo-form-flow-expression-list>\n\n <valtimo-form-flow-expression-list\n [description]=\"'formFlow.uiEditor.onBackDescription' | translate\"\n [expressions]=\"getExpressions('onBack')\"\n [label]=\"'formFlow.uiEditor.onBack' | translate\"\n [readOnly]=\"readOnly\"\n [registry]=\"registry\"\n [revealErrors]=\"revealErrors\"\n ></valtimo-form-flow-expression-list>\n </section>\n\n <valtimo-form-flow-expression-help-modal\n [open]=\"$showHelpModal()\"\n [registry]=\"registry\"\n (closeEvent)=\"$showHelpModal.set(false)\"\n ></valtimo-form-flow-expression-help-modal>\n</div>\n", styles: [".step-detail{display:flex;flex-direction:column;gap:var(--cds-spacing-06)}.step-detail__header{display:flex;align-items:center;justify-content:space-between;gap:var(--cds-spacing-05)}.step-detail__heading{display:flex;align-items:center;gap:var(--cds-spacing-03)}.step-detail__heading cds-tag{margin:0}.step-detail__header-actions{display:flex;align-items:center;gap:var(--cds-spacing-03)}.step-detail__grid{display:grid;grid-template-columns:1fr 1fr;gap:var(--cds-spacing-05)}.step-detail__grid cds-label{margin-bottom:0}.step-detail__divider{border:none;border-top:1px solid var(--cds-border-subtle);margin:0;width:100%}.step-detail__section{display:flex;flex-direction:column;gap:var(--cds-spacing-05)}.step-detail__section-header h5{margin:0 0 var(--cds-spacing-02) 0}.step-detail__section-description{color:var(--cds-text-secondary);font-size:var(--cds-body-compact-01-font-size, .875rem);margin:0}.step-detail__help-notification{display:block;margin:0;max-inline-size:none;max-width:none}.step-detail__label{display:inline-flex;align-items:center;gap:var(--cds-spacing-02)}\n/*!\n * Copyright 2015-2026 Ritense BV, the Netherlands.\n *\n * Licensed under EUPL, Version 1.2 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" basis,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i1$1.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i4.TranslatePipe, name: "translate" }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i2$1.Button, selector: "[cdsButton], [ibmButton]", inputs: ["ibmButton", "cdsButton", "size", "skeleton", "iconOnly", "isExpressive"] }, { kind: "ngmodule", type: DropdownModule }, { kind: "component", type: i2$1.Dropdown, selector: "cds-dropdown, ibm-dropdown", inputs: ["id", "label", "hideLabel", "helperText", "placeholder", "displayValue", "clearText", "size", "type", "theme", "disabled", "readonly", "skeleton", "inline", "disableArrowKeys", "invalid", "invalidText", "warn", "warnText", "appendInline", "scrollableContainer", "itemValueKey", "selectionFeedback", "menuButtonLabel", "selectedLabel", "dropUp", "fluid"], outputs: ["selected", "onClose", "close"] }, { kind: "component", type: i2$1.DropdownList, selector: "cds-dropdown-list, ibm-dropdown-list", inputs: ["ariaLabel", "items", "listTpl", "type", "showTitles"], outputs: ["select", "scroll", "blurIntent"] }, { kind: "ngmodule", type: IconModule }, { kind: "directive", type: i2$1.IconDirective, selector: "[cdsIcon], [ibmIcon]", inputs: ["ibmIcon", "cdsIcon", "size", "title", "ariaLabel", "ariaLabelledBy", "ariaHidden", "isFocusable"] }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i2$1.Label, selector: "cds-label, ibm-label", inputs: ["labelInputID", "disabled", "skeleton", "helperText", "invalidText", "invalid", "warn", "warnText", "ariaLabel"] }, { kind: "directive", type: i2$1.TextInput, selector: "[cdsText], [ibmText]", inputs: ["theme", "size", "invalid", "warn", "skeleton"] }, { kind: "ngmodule", type: LayerModule }, { kind: "directive", type: i2$1.LayerDirective, selector: "[cdsLayer], [ibmLayer]", inputs: ["ibmLayer", "cdsLayer"], exportAs: ["layer"] }, { kind: "ngmodule", type: NotificationModule }, { kind: "component", type: i2$1.ActionableNotification, selector: "cds-actionable-notification, ibm-actionable-notification", inputs: ["notificationObj"] }, { kind: "ngmodule", type: TagModule }, { kind: "component", type: i2$1.Tag, selector: "cds-tag, ibm-tag", inputs: ["type", "size", "class", "skeleton"] }, { kind: "ngmodule", type: TooltipIconModule }, { kind: "component", type: i1$2.TooltipIconComponent, selector: "v-tooltip-icon", inputs: ["tooltip", "disabled"] }, { kind: "component", type: FormFlowExpressionHelpModalComponent, selector: "valtimo-form-flow-expression-help-modal", inputs: ["open", "registry"], outputs: ["closeEvent"] }, { kind: "component", type: FormFlowExpressionListComponent, selector: "valtimo-form-flow-expression-list", inputs: ["expressions", "label", "description", "readOnly", "revealErrors", "registry"] }, { kind: "component", type: FormFlowTransitionListComponent, selector: "valtimo-form-flow-transition-list", inputs: ["transitions", "stepKeys", "readOnly", "revealErrors"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1196
+ }
1197
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowStepDetailComponent, decorators: [{
1198
+ type: Component,
1199
+ args: [{ standalone: true, selector: 'valtimo-form-flow-step-detail', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
1200
+ CommonModule,
1201
+ ReactiveFormsModule,
1202
+ TranslateModule,
1203
+ ButtonModule,
1204
+ DropdownModule,
1205
+ IconModule,
1206
+ InputModule,
1207
+ LayerModule,
1208
+ NotificationModule,
1209
+ TagModule,
1210
+ TooltipIconModule,
1211
+ FormFlowExpressionHelpModalComponent,
1212
+ FormFlowExpressionListComponent,
1213
+ FormFlowTransitionListComponent,
1214
+ ], template: "<!--\n ~ Copyright 2015-2026 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n<div class=\"step-detail\" [cdsLayer]=\"1\" [formGroup]=\"stepGroup\">\n <div class=\"step-detail__header\">\n <div class=\"step-detail__heading\">\n <h5>{{ 'formFlow.uiEditor.stepDetails' | translate }}</h5>\n\n @if (isStartStep) {\n <cds-tag type=\"green\" size=\"sm\">{{ 'formFlow.uiEditor.startStep' | translate }}</cds-tag>\n }\n </div>\n\n @if (!readOnly) {\n <div class=\"step-detail__header-actions\">\n @if (!isStartStep) {\n <button\n cdsButton=\"tertiary\"\n size=\"sm\"\n [attr.data-test-id]=\"testIds.makeStartStepButton\"\n (click)=\"makeStartEvent.emit()\"\n >\n {{ 'formFlow.uiEditor.makeStartStep' | translate }}\n\n <svg class=\"cds--btn__icon\" cdsIcon=\"flag\" size=\"16\"></svg>\n </button>\n }\n\n <button\n cdsButton=\"danger--ghost\"\n size=\"sm\"\n [attr.data-test-id]=\"testIds.deleteStepButton\"\n (click)=\"deleteStepEvent.emit()\"\n >\n {{ 'formFlow.uiEditor.deleteStep' | translate }}\n\n <svg class=\"cds--btn__icon\" cdsIcon=\"trash-can\" size=\"16\"></svg>\n </button>\n </div>\n }\n </div>\n\n <div class=\"step-detail__grid\">\n <cds-label\n [invalid]=\"isControlInvalid('key') || (duplicateKey && revealErrors)\"\n [invalidText]=\"\n duplicateKey\n ? ('formFlow.uiEditor.duplicateKey' | translate)\n : ('formFlow.uiEditor.required' | translate)\n \"\n >\n <span class=\"step-detail__label\">\n {{ 'formFlow.key' | translate }}\n\n <v-tooltip-icon\n [tooltip]=\"'formFlow.uiEditor.fieldTooltips.key' | translate\"\n ></v-tooltip-icon>\n </span>\n\n <input\n cdsText\n formControlName=\"key\"\n [invalid]=\"isControlInvalid('key') || (duplicateKey && revealErrors)\"\n [attr.data-test-id]=\"testIds.stepKeyInput\"\n placeholder=\"{{ 'formFlow.uiEditor.keyPlaceholder' | translate }}\"\n />\n </cds-label>\n\n <cds-label>\n <span class=\"step-detail__label\">\n {{ 'formFlow.uiEditor.title' | translate }}\n\n <v-tooltip-icon\n [tooltip]=\"'formFlow.uiEditor.fieldTooltips.title' | translate\"\n ></v-tooltip-icon>\n </span>\n\n <input\n cdsText\n formControlName=\"title\"\n [attr.data-test-id]=\"testIds.stepTitleInput\"\n placeholder=\"{{ 'formFlow.uiEditor.titlePlaceholder' | translate }}\"\n />\n </cds-label>\n\n <div [attr.data-test-id]=\"testIds.stepTypeDropdown\">\n <cds-dropdown formControlName=\"typeName\" itemValueKey=\"id\" [label]=\"stepTypeLabelTemplate\">\n <cds-dropdown-list [items]=\"stepTypeItems\"></cds-dropdown-list>\n </cds-dropdown>\n\n <ng-template #stepTypeLabelTemplate>\n <span class=\"step-detail__label\">\n {{ 'formFlow.uiEditor.stepType' | translate }}\n\n <v-tooltip-icon [tooltip]=\"getTypeTooltip()\"></v-tooltip-icon>\n </span>\n </ng-template>\n </div>\n\n <ng-container formGroupName=\"properties\">\n @for (propertyName of propertyNames; track propertyName) {\n @if (isFormDefinitionProperty(propertyName)) {\n <div [attr.data-test-id]=\"testIds.stepPropertyDropdown\">\n <cds-dropdown\n formControlName=\"definition\"\n itemValueKey=\"id\"\n [invalid]=\"propertiesGroup.get('definition')?.invalid && revealErrors\"\n [label]=\"formDefinitionLabelTemplate\"\n [placeholder]=\"'formFlow.uiEditor.selectForm' | translate\"\n >\n <cds-dropdown-list [items]=\"getFormDefinitionItems()\"></cds-dropdown-list>\n </cds-dropdown>\n\n <ng-template #formDefinitionLabelTemplate>\n <span class=\"step-detail__label\">\n {{ getPropertyLabel(propertyName) }}\n\n <v-tooltip-icon\n [tooltip]=\"'formFlow.uiEditor.fieldTooltips.definition' | translate\"\n ></v-tooltip-icon>\n </span>\n </ng-template>\n </div>\n } @else if (isCustomComponentProperty(propertyName)) {\n <div [attr.data-test-id]=\"testIds.stepPropertyDropdown\">\n <cds-dropdown\n formControlName=\"componentId\"\n itemValueKey=\"id\"\n [invalid]=\"propertiesGroup.get('componentId')?.invalid && revealErrors\"\n [label]=\"componentIdLabelTemplate\"\n [placeholder]=\"'formFlow.uiEditor.selectComponent' | translate\"\n >\n <cds-dropdown-list [items]=\"getCustomComponentItems()\"></cds-dropdown-list>\n </cds-dropdown>\n\n <ng-template #componentIdLabelTemplate>\n <span class=\"step-detail__label\">\n {{ getPropertyLabel(propertyName) }}\n\n <v-tooltip-icon\n [tooltip]=\"'formFlow.uiEditor.fieldTooltips.componentId' | translate\"\n ></v-tooltip-icon>\n </span>\n </ng-template>\n </div>\n } @else {\n <cds-label\n [invalid]=\"propertiesGroup.get(propertyName)?.invalid && revealErrors\"\n [invalidText]=\"'formFlow.uiEditor.required' | translate\"\n >\n {{ getPropertyLabel(propertyName) }}\n\n <input\n cdsText\n [formControlName]=\"propertyName\"\n [invalid]=\"propertiesGroup.get(propertyName)?.invalid && revealErrors\"\n [attr.data-test-id]=\"testIds.stepPropertyInput\"\n />\n </cds-label>\n }\n }\n </ng-container>\n </div>\n\n <hr class=\"step-detail__divider\" />\n\n <section class=\"step-detail__section\">\n <div class=\"step-detail__section-header\">\n <h5>{{ 'formFlow.uiEditor.navigation' | translate }}</h5>\n\n <p class=\"step-detail__section-description\">\n {{ 'formFlow.uiEditor.navigationDescription' | translate }}\n </p>\n </div>\n\n <valtimo-form-flow-transition-list\n [readOnly]=\"readOnly\"\n [revealErrors]=\"revealErrors\"\n [stepKeys]=\"stepKeys\"\n [transitions]=\"transitions\"\n ></valtimo-form-flow-transition-list>\n\n <cds-actionable-notification\n class=\"step-detail__help-notification\"\n [notificationObj]=\"{\n type: 'info',\n lowContrast: true,\n showClose: false,\n title: 'formFlow.uiEditor.help.conditionsInlineTitle' | translate,\n message: 'formFlow.uiEditor.help.conditionsInlineMessage' | translate,\n actions: helpActions,\n }\"\n ></cds-actionable-notification>\n </section>\n\n <hr class=\"step-detail__divider\" />\n\n <section class=\"step-detail__section\">\n <div class=\"step-detail__section-header\">\n <h5>{{ 'formFlow.uiEditor.actions' | translate }}</h5>\n\n <p class=\"step-detail__section-description\">\n {{ 'formFlow.uiEditor.actionsDescription' | translate }}\n </p>\n </div>\n\n <cds-actionable-notification\n class=\"step-detail__help-notification\"\n [notificationObj]=\"{\n type: 'info',\n lowContrast: true,\n showClose: false,\n title: 'formFlow.uiEditor.help.actionsInlineTitle' | translate,\n message: 'formFlow.uiEditor.help.actionsInlineMessage' | translate,\n actions: helpActions,\n }\"\n ></cds-actionable-notification>\n\n <valtimo-form-flow-expression-list\n [description]=\"'formFlow.uiEditor.onOpenDescription' | translate\"\n [expressions]=\"getExpressions('onOpen')\"\n [label]=\"'formFlow.uiEditor.onOpen' | translate\"\n [readOnly]=\"readOnly\"\n [registry]=\"registry\"\n [revealErrors]=\"revealErrors\"\n ></valtimo-form-flow-expression-list>\n\n <valtimo-form-flow-expression-list\n [description]=\"'formFlow.uiEditor.onCompleteDescription' | translate\"\n [expressions]=\"getExpressions('onComplete')\"\n [label]=\"'formFlow.uiEditor.onComplete' | translate\"\n [readOnly]=\"readOnly\"\n [registry]=\"registry\"\n [revealErrors]=\"revealErrors\"\n ></valtimo-form-flow-expression-list>\n\n <valtimo-form-flow-expression-list\n [description]=\"'formFlow.uiEditor.onBackDescription' | translate\"\n [expressions]=\"getExpressions('onBack')\"\n [label]=\"'formFlow.uiEditor.onBack' | translate\"\n [readOnly]=\"readOnly\"\n [registry]=\"registry\"\n [revealErrors]=\"revealErrors\"\n ></valtimo-form-flow-expression-list>\n </section>\n\n <valtimo-form-flow-expression-help-modal\n [open]=\"$showHelpModal()\"\n [registry]=\"registry\"\n (closeEvent)=\"$showHelpModal.set(false)\"\n ></valtimo-form-flow-expression-help-modal>\n</div>\n", styles: [".step-detail{display:flex;flex-direction:column;gap:var(--cds-spacing-06)}.step-detail__header{display:flex;align-items:center;justify-content:space-between;gap:var(--cds-spacing-05)}.step-detail__heading{display:flex;align-items:center;gap:var(--cds-spacing-03)}.step-detail__heading cds-tag{margin:0}.step-detail__header-actions{display:flex;align-items:center;gap:var(--cds-spacing-03)}.step-detail__grid{display:grid;grid-template-columns:1fr 1fr;gap:var(--cds-spacing-05)}.step-detail__grid cds-label{margin-bottom:0}.step-detail__divider{border:none;border-top:1px solid var(--cds-border-subtle);margin:0;width:100%}.step-detail__section{display:flex;flex-direction:column;gap:var(--cds-spacing-05)}.step-detail__section-header h5{margin:0 0 var(--cds-spacing-02) 0}.step-detail__section-description{color:var(--cds-text-secondary);font-size:var(--cds-body-compact-01-font-size, .875rem);margin:0}.step-detail__help-notification{display:block;margin:0;max-inline-size:none;max-width:none}.step-detail__label{display:inline-flex;align-items:center;gap:var(--cds-spacing-02)}\n/*!\n * Copyright 2015-2026 Ritense BV, the Netherlands.\n *\n * Licensed under EUPL, Version 1.2 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" basis,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n"] }]
1215
+ }], ctorParameters: () => [{ type: FormFlowEditorFormService }, { type: i2$1.IconService }, { type: i4.TranslateService }], propDecorators: { stepGroup: [{
1216
+ type: Input
1217
+ }], registry: [{
1218
+ type: Input
1219
+ }], stepKeys: [{
1220
+ type: Input
1221
+ }], formOptions: [{
1222
+ type: Input
1223
+ }], componentOptions: [{
1224
+ type: Input
1225
+ }], isStartStep: [{
1226
+ type: Input
1227
+ }], readOnly: [{
1228
+ type: Input
1229
+ }], duplicateKey: [{
1230
+ type: Input
1231
+ }], revealErrors: [{
1232
+ type: Input
1233
+ }], deleteStepEvent: [{
1234
+ type: Output
1235
+ }], makeStartEvent: [{
1236
+ type: Output
1237
+ }] } });
1238
+
1239
+ /*
1240
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
1241
+ *
1242
+ * Licensed under EUPL, Version 1.2 (the "License");
1243
+ * you may not use this file except in compliance with the License.
1244
+ * You may obtain a copy of the License at
1245
+ *
1246
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
1247
+ *
1248
+ * Unless required by applicable law or agreed to in writing, software
1249
+ * distributed under the License is distributed on an "AS IS" basis,
1250
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1251
+ * See the License for the specific language governing permissions and
1252
+ * limitations under the License.
1253
+ */
1254
+ /** Clickable overview of all steps in the flow, ordered as defined, with the start step first. */
1255
+ class FormFlowStepListComponent {
1256
+ constructor(iconService) {
1257
+ this.iconService = iconService;
1258
+ this.steps = [];
1259
+ this.selectedIndex = null;
1260
+ this.readOnly = false;
1261
+ this.selectEvent = new EventEmitter();
1262
+ this.addEvent = new EventEmitter();
1263
+ this.testIds = FORM_FLOW_EDITOR_TEST_IDS;
1264
+ this.iconService.registerAll([Add16, WarningFilled16]);
1265
+ }
1266
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowStepListComponent, deps: [{ token: i2$1.IconService }], target: i0.ɵɵFactoryTarget.Component }); }
1267
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: FormFlowStepListComponent, isStandalone: true, selector: "valtimo-form-flow-step-list", inputs: { steps: "steps", selectedIndex: "selectedIndex", readOnly: "readOnly" }, outputs: { selectEvent: "selectEvent", addEvent: "addEvent" }, ngImport: i0, template: "<!--\n ~ Copyright 2015-2026 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n<div class=\"step-list__header\">\n <span class=\"step-list__title\">{{ 'formFlow.uiEditor.stepsTitle' | translate }}</span>\n\n @if (!readOnly) {\n <button\n cdsButton=\"primary\"\n size=\"sm\"\n [attr.data-test-id]=\"testIds.addStepButton\"\n (click)=\"addEvent.emit()\"\n >\n {{ 'formFlow.uiEditor.addStep' | translate }}\n\n <svg class=\"cds--btn__icon\" cdsIcon=\"add\" size=\"16\"></svg>\n </button>\n }\n</div>\n\n<ul class=\"step-list__list\" [attr.data-test-id]=\"testIds.stepList\">\n @for (step of steps; track $index) {\n <li class=\"step-list__list-item\">\n <button\n class=\"step-list__item\"\n [class.step-list__item--selected]=\"selectedIndex === $index\"\n [attr.data-test-id]=\"testIds.stepListItem\"\n type=\"button\"\n (click)=\"selectEvent.emit($index)\"\n >\n <span class=\"step-list__item-heading\">\n <span class=\"step-list__item-key\" [title]=\"step.key\">{{ step.key }}</span>\n\n @if (step.invalid) {\n <svg\n class=\"step-list__item-warning\"\n cdsIcon=\"warning--filled\"\n size=\"16\"\n [attr.aria-label]=\"'formFlow.uiEditor.stepInvalid' | translate\"\n ></svg>\n }\n </span>\n\n @if (step.title) {\n <span class=\"step-list__item-title\">{{ step.title }}</span>\n }\n\n <span class=\"step-list__item-tags\">\n @if (step.isStart) {\n <cds-tag type=\"green\" size=\"sm\">{{\n 'formFlow.uiEditor.startStep' | translate\n }}</cds-tag>\n }\n\n <cds-tag type=\"blue\" size=\"sm\">{{ step.typeName }}</cds-tag>\n </span>\n </button>\n </li>\n }\n</ul>\n", styles: [":host{display:flex;flex:1 1 auto;flex-direction:column;min-height:0}.step-list__header{align-items:center;border-bottom:1px solid var(--cds-border-subtle, #e0e0e0);display:flex;flex:0 0 auto;justify-content:space-between;padding:12px}.step-list__title{color:var(--cds-text-secondary, #525252);font-size:14px;line-height:24px;font-weight:600}.step-list__list{flex:1 1 auto;list-style:none;margin:0;min-height:0;overflow-y:auto;padding:0}.step-list__list-item{margin:0}.step-list__item{background:transparent;border:0;border-left:3px solid transparent;cursor:pointer;display:flex;flex-direction:column;gap:.375rem;padding:.75rem 1rem;text-align:left;width:100%}.step-list__item:hover{background:var(--cds-layer-hover-01, #e8e8e8)}.step-list__item:focus-visible{outline:2px solid var(--cds-focus, #0f62fe);outline-offset:-2px}.step-list__item--selected{background:var(--cds-layer-selected-01, #e0e0e0);border-left-color:var(--cds-border-interactive, #0f62fe)}.step-list__item--selected:hover{background:var(--cds-layer-selected-hover-01, #d1d1d1)}.step-list__item-heading{align-items:center;display:flex;gap:.5rem;justify-content:space-between;max-width:100%}.step-list__item-key{color:var(--cds-text-primary, #161616);font-size:.875rem;font-weight:600;line-height:1.29;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.step-list__item-warning{fill:var(--cds-support-error, #da1e28);flex-shrink:0;height:1rem;width:1rem}.step-list__item-title{color:var(--cds-text-secondary, #525252);font-size:.75rem;line-height:1.34}.step-list__item-tags{align-items:center;display:flex;flex-wrap:wrap;gap:.25rem}.step-list__item-tags ::ng-deep .cds--tag{margin:0;max-width:none;box-shadow:0 0 0 1px var(--cds-layer-01)}\n/*!\n * Copyright 2015-2026 Ritense BV, the Netherlands.\n *\n * Licensed under EUPL, Version 1.2 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" basis,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i4.TranslatePipe, name: "translate" }, { kind: "ngmodule", type: ButtonModule }, { kind: "directive", type: i2$1.Button, selector: "[cdsButton], [ibmButton]", inputs: ["ibmButton", "cdsButton", "size", "skeleton", "iconOnly", "isExpressive"] }, { kind: "ngmodule", type: IconModule }, { kind: "directive", type: i2$1.IconDirective, selector: "[cdsIcon], [ibmIcon]", inputs: ["ibmIcon", "cdsIcon", "size", "title", "ariaLabel", "ariaLabelledBy", "ariaHidden", "isFocusable"] }, { kind: "ngmodule", type: TagModule }, { kind: "component", type: i2$1.Tag, selector: "cds-tag, ibm-tag", inputs: ["type", "size", "class", "skeleton"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1268
+ }
1269
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowStepListComponent, decorators: [{
1270
+ type: Component,
1271
+ args: [{ standalone: true, selector: 'valtimo-form-flow-step-list', changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, TranslateModule, ButtonModule, IconModule, TagModule], template: "<!--\n ~ Copyright 2015-2026 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n<div class=\"step-list__header\">\n <span class=\"step-list__title\">{{ 'formFlow.uiEditor.stepsTitle' | translate }}</span>\n\n @if (!readOnly) {\n <button\n cdsButton=\"primary\"\n size=\"sm\"\n [attr.data-test-id]=\"testIds.addStepButton\"\n (click)=\"addEvent.emit()\"\n >\n {{ 'formFlow.uiEditor.addStep' | translate }}\n\n <svg class=\"cds--btn__icon\" cdsIcon=\"add\" size=\"16\"></svg>\n </button>\n }\n</div>\n\n<ul class=\"step-list__list\" [attr.data-test-id]=\"testIds.stepList\">\n @for (step of steps; track $index) {\n <li class=\"step-list__list-item\">\n <button\n class=\"step-list__item\"\n [class.step-list__item--selected]=\"selectedIndex === $index\"\n [attr.data-test-id]=\"testIds.stepListItem\"\n type=\"button\"\n (click)=\"selectEvent.emit($index)\"\n >\n <span class=\"step-list__item-heading\">\n <span class=\"step-list__item-key\" [title]=\"step.key\">{{ step.key }}</span>\n\n @if (step.invalid) {\n <svg\n class=\"step-list__item-warning\"\n cdsIcon=\"warning--filled\"\n size=\"16\"\n [attr.aria-label]=\"'formFlow.uiEditor.stepInvalid' | translate\"\n ></svg>\n }\n </span>\n\n @if (step.title) {\n <span class=\"step-list__item-title\">{{ step.title }}</span>\n }\n\n <span class=\"step-list__item-tags\">\n @if (step.isStart) {\n <cds-tag type=\"green\" size=\"sm\">{{\n 'formFlow.uiEditor.startStep' | translate\n }}</cds-tag>\n }\n\n <cds-tag type=\"blue\" size=\"sm\">{{ step.typeName }}</cds-tag>\n </span>\n </button>\n </li>\n }\n</ul>\n", styles: [":host{display:flex;flex:1 1 auto;flex-direction:column;min-height:0}.step-list__header{align-items:center;border-bottom:1px solid var(--cds-border-subtle, #e0e0e0);display:flex;flex:0 0 auto;justify-content:space-between;padding:12px}.step-list__title{color:var(--cds-text-secondary, #525252);font-size:14px;line-height:24px;font-weight:600}.step-list__list{flex:1 1 auto;list-style:none;margin:0;min-height:0;overflow-y:auto;padding:0}.step-list__list-item{margin:0}.step-list__item{background:transparent;border:0;border-left:3px solid transparent;cursor:pointer;display:flex;flex-direction:column;gap:.375rem;padding:.75rem 1rem;text-align:left;width:100%}.step-list__item:hover{background:var(--cds-layer-hover-01, #e8e8e8)}.step-list__item:focus-visible{outline:2px solid var(--cds-focus, #0f62fe);outline-offset:-2px}.step-list__item--selected{background:var(--cds-layer-selected-01, #e0e0e0);border-left-color:var(--cds-border-interactive, #0f62fe)}.step-list__item--selected:hover{background:var(--cds-layer-selected-hover-01, #d1d1d1)}.step-list__item-heading{align-items:center;display:flex;gap:.5rem;justify-content:space-between;max-width:100%}.step-list__item-key{color:var(--cds-text-primary, #161616);font-size:.875rem;font-weight:600;line-height:1.29;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.step-list__item-warning{fill:var(--cds-support-error, #da1e28);flex-shrink:0;height:1rem;width:1rem}.step-list__item-title{color:var(--cds-text-secondary, #525252);font-size:.75rem;line-height:1.34}.step-list__item-tags{align-items:center;display:flex;flex-wrap:wrap;gap:.25rem}.step-list__item-tags ::ng-deep .cds--tag{margin:0;max-width:none;box-shadow:0 0 0 1px var(--cds-layer-01)}\n/*!\n * Copyright 2015-2026 Ritense BV, the Netherlands.\n *\n * Licensed under EUPL, Version 1.2 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" basis,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n"] }]
1272
+ }], ctorParameters: () => [{ type: i2$1.IconService }], propDecorators: { steps: [{
1273
+ type: Input
1274
+ }], selectedIndex: [{
1275
+ type: Input
1276
+ }], readOnly: [{
1277
+ type: Input
1278
+ }], selectEvent: [{
1279
+ type: Output
1280
+ }], addEvent: [{
1281
+ type: Output
1282
+ }] } });
1283
+
1284
+ /*
1285
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
1286
+ *
1287
+ * Licensed under EUPL, Version 1.2 (the "License");
1288
+ * you may not use this file except in compliance with the License.
1289
+ * You may obtain a copy of the License at
1290
+ *
1291
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
1292
+ *
1293
+ * Unless required by applicable law or agreed to in writing, software
1294
+ * distributed under the License is distributed on an "AS IS" basis,
1295
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1296
+ * See the License for the specific language governing permissions and
1297
+ * limitations under the License.
1298
+ */
1299
+ /**
1300
+ * The visual form flow editor. Renders the definition as a step list with a detail panel and emits
1301
+ * the modified definition as JSON through the same contract as the JSON editor tab, so the
1302
+ * surrounding editor page treats both tabs identically.
1303
+ */
1304
+ class FormFlowUiEditorTabComponent {
1305
+ constructor(formService, formFlowService, formFlowComponentService, formManagementService, route, translateService) {
1306
+ this.formService = formService;
1307
+ this.formFlowService = formFlowService;
1308
+ this.formFlowComponentService = formFlowComponentService;
1309
+ this.formManagementService = formManagementService;
1310
+ this.route = route;
1311
+ this.translateService = translateService;
1312
+ this.model = null;
1313
+ this.readOnly = false;
1314
+ /** The parent's save-attempt state: while false the admin is still modelling and no validation
1315
+ * errors are shown; a save attempt flips it and reveals them. */
1316
+ this.revealErrors = false;
1317
+ this.validEvent = new EventEmitter();
1318
+ this.valueChangeEvent = new EventEmitter();
1319
+ this.form = null;
1320
+ this.registry = null;
1321
+ this.$selectedIndex = signal(null);
1322
+ this.$stepListItems = signal([]);
1323
+ // Only replaced when the keys themselves change: the Carbon dropdown resets its visual
1324
+ // selection whenever its items array is swapped, so a stable reference prevents flicker while
1325
+ // typing in unrelated fields.
1326
+ this.$stepKeys = signal([]);
1327
+ this.$formOptions = signal([]);
1328
+ this.$componentOptions = signal([]);
1329
+ this.$definitionErrors = signal([]);
1330
+ this.$parseFailed = signal(false);
1331
+ this.testIds = FORM_FLOW_EDITOR_TEST_IDS;
1332
+ this._definition = null;
1333
+ this._definitionKey = '';
1334
+ this._previousStepKeys = [];
1335
+ this._formSubscriptions = new Subscription();
1336
+ this._subscriptions = new Subscription();
1337
+ this._subscriptions.add(this.formFlowService.getFormFlowRegistry().subscribe(registry => {
1338
+ this.registry = registry;
1339
+ this.buildForm();
1340
+ }));
1341
+ // The custom Angular components an implementation has registered for `custom-component`
1342
+ // steps. The editor offers their ids as choices.
1343
+ this._subscriptions.add(this.formFlowComponentService.supportedComponents$
1344
+ .pipe(take(1))
1345
+ .subscribe(components => this.$componentOptions.set(components.map(component => component.id).sort())));
1346
+ this.loadFormOptions();
1347
+ }
1348
+ ngOnChanges(changes) {
1349
+ if (changes['model']) {
1350
+ this.parseModel();
1351
+ this.buildForm();
1352
+ }
1353
+ if (changes['readOnly'] && !changes['readOnly'].firstChange) {
1354
+ this.applyReadOnly();
1355
+ }
1356
+ }
1357
+ ngOnDestroy() {
1358
+ this._subscriptions.unsubscribe();
1359
+ this._formSubscriptions.unsubscribe();
1360
+ }
1361
+ get selectedStepGroup() {
1362
+ const index = this.$selectedIndex();
1363
+ if (this.form === null || index === null)
1364
+ return null;
1365
+ return this.stepsArray.controls[index] ?? null;
1366
+ }
1367
+ isSelectedKeyDuplicate() {
1368
+ const index = this.$selectedIndex();
1369
+ if (index === null)
1370
+ return false;
1371
+ const keys = this.stepsArray.controls.map(stepGroup => stepGroup.get('key')?.value);
1372
+ return keys.filter(key => key === keys[index]).length > 1;
1373
+ }
1374
+ isSelectedStepStart() {
1375
+ return (!!this.selectedStepGroup &&
1376
+ this.selectedStepGroup.get('key')?.value === this.form?.get('startStep')?.value);
1377
+ }
1378
+ onMakeStartStep() {
1379
+ const key = this.selectedStepGroup?.get('key')?.value;
1380
+ if (key) {
1381
+ this.form?.get('startStep')?.setValue(key);
1382
+ this.form?.get('startStep')?.markAsDirty();
1383
+ }
1384
+ }
1385
+ onSelectStep(index) {
1386
+ this.$selectedIndex.set(index);
1387
+ }
1388
+ onAddStep() {
1389
+ if (!this.form || !this.registry)
1390
+ return;
1391
+ const existingKeys = this.stepsArray.controls.map(stepGroup => stepGroup.get('key')?.value);
1392
+ this.stepsArray.push(this.formService.buildNewStepGroup(existingKeys, this.registry));
1393
+ this.stepsArray.markAsDirty();
1394
+ this.$selectedIndex.set(this.stepsArray.length - 1);
1395
+ }
1396
+ onDeleteStep() {
1397
+ const index = this.$selectedIndex();
1398
+ if (index === null || !this.form)
1399
+ return;
1400
+ const deletedKey = this.stepsArray.at(index).get('key')?.value;
1401
+ this.stepsArray.removeAt(index);
1402
+ this.stepsArray.markAsDirty();
1403
+ // Deleting the start step promotes the first remaining step, so the definition stays valid.
1404
+ const startStepControl = this.form.get('startStep');
1405
+ if (startStepControl?.value === deletedKey) {
1406
+ startStepControl.setValue(this.stepsArray.at(0)?.get('key')?.value ?? '');
1407
+ }
1408
+ if (this.stepsArray.length === 0) {
1409
+ this.$selectedIndex.set(null);
1410
+ }
1411
+ else {
1412
+ this.$selectedIndex.set(Math.min(index, this.stepsArray.length - 1));
1413
+ }
1414
+ }
1415
+ get stepsArray() {
1416
+ return this.formService.getStepsArray(this.form);
1417
+ }
1418
+ parseModel() {
1419
+ this._definition = null;
1420
+ this.$parseFailed.set(false);
1421
+ if (!this.model?.value)
1422
+ return;
1423
+ try {
1424
+ this._definition = JSON.parse(this.model.value);
1425
+ this._definitionKey = this._definition.key;
1426
+ }
1427
+ catch {
1428
+ this.$parseFailed.set(true);
1429
+ }
1430
+ }
1431
+ buildForm() {
1432
+ if (!this._definition || !this.registry)
1433
+ return;
1434
+ this._formSubscriptions.unsubscribe();
1435
+ this._formSubscriptions = new Subscription();
1436
+ this.form = this.formService.buildDefinitionForm(this._definition, this.registry);
1437
+ this._previousStepKeys = this.currentStepKeys();
1438
+ this.applyReadOnly();
1439
+ this.selectInitialStep();
1440
+ this.refreshDerivedState();
1441
+ // The initial emission is the container's clean baseline; every change after that re-emits the
1442
+ // serialized definition and its validity, mirroring the JSON editor tab.
1443
+ this.emitState();
1444
+ // Validity only changes together with values (validators run on value changes and the
1445
+ // readOnly enable/disable path suppresses events), so valueChanges alone keeps every derived
1446
+ // signal fresh.
1447
+ this._formSubscriptions.add(this.form.valueChanges.subscribe(() => {
1448
+ this.trackStepKeyRenames();
1449
+ this.refreshDerivedState();
1450
+ this.emitState();
1451
+ }));
1452
+ }
1453
+ selectInitialStep() {
1454
+ const startStep = this._definition?.startStep;
1455
+ const startIndex = (this._definition?.steps ?? []).findIndex(step => step.key === startStep);
1456
+ if ((this._definition?.steps ?? []).length === 0) {
1457
+ this.$selectedIndex.set(null);
1458
+ return;
1459
+ }
1460
+ this.$selectedIndex.set(startIndex >= 0 ? startIndex : 0);
1461
+ }
1462
+ applyReadOnly() {
1463
+ if (!this.form)
1464
+ return;
1465
+ if (this.readOnly) {
1466
+ this.form.disable({ emitEvent: false });
1467
+ }
1468
+ else {
1469
+ this.form.enable({ emitEvent: false });
1470
+ }
1471
+ }
1472
+ // Renaming a step key rewrites the start step and every transition that targeted the old key, so
1473
+ // the definition never breaks while typing a new key.
1474
+ trackStepKeyRenames() {
1475
+ const currentKeys = this.currentStepKeys();
1476
+ if (currentKeys.length === this._previousStepKeys.length) {
1477
+ const changedIndices = currentKeys
1478
+ .map((key, index) => (key !== this._previousStepKeys[index] ? index : -1))
1479
+ .filter(index => index !== -1);
1480
+ if (changedIndices.length === 1) {
1481
+ const index = changedIndices[0];
1482
+ this.formService.renameStepReferences(this.form, this._previousStepKeys[index], currentKeys[index]);
1483
+ }
1484
+ }
1485
+ this._previousStepKeys = currentKeys;
1486
+ }
1487
+ currentStepKeys() {
1488
+ return this.stepsArray.controls.map(stepGroup => stepGroup.get('key')?.value ?? '');
1489
+ }
1490
+ refreshDerivedState() {
1491
+ if (!this.form)
1492
+ return;
1493
+ const startStep = this.form.get('startStep')?.value;
1494
+ const keys = this.currentStepKeys();
1495
+ this.$stepListItems.set(this.stepsArray.controls.map(stepGroup => ({
1496
+ key: stepGroup.get('key')?.value ?? '',
1497
+ title: stepGroup.get('title')?.value ?? '',
1498
+ typeName: stepGroup.get('typeName')?.value ?? '',
1499
+ isStart: stepGroup.get('key')?.value === startStep,
1500
+ invalid: stepGroup.invalid,
1501
+ })));
1502
+ if (!this.arraysEqual(keys, this.$stepKeys())) {
1503
+ this.$stepKeys.set(keys);
1504
+ }
1505
+ this.$definitionErrors.set(this.collectDefinitionErrors());
1506
+ }
1507
+ // Loads the form definitions of the surrounding case definition or building block, so the
1508
+ // `form` step type can offer them as choices instead of a free-text form key.
1509
+ loadFormOptions() {
1510
+ this._subscriptions.add(getContextObservable(this.route)
1511
+ .pipe(take(1), switchMap(context => {
1512
+ if (context === 'buildingBlock') {
1513
+ return getBuildingBlockManagementRouteParams(this.route).pipe(take(1), switchMap(params => this.formManagementService.queryFormDefinitionsBuildingBlock(params?.buildingBlockDefinitionKey ?? '', params?.buildingBlockDefinitionVersionTag ?? '', { size: 1000 })), map(page => page.content));
1514
+ }
1515
+ return getCaseManagementRouteParams(this.route).pipe(take(1), switchMap(params => this.formManagementService.queryFormDefinitionsCase(params?.caseDefinitionKey ?? '', params?.caseDefinitionVersionTag ?? '', { size: 1000 })), map(response => response.content));
1516
+ }), map(formDefinitions => [...new Set(formDefinitions.map(definition => definition.name).filter(Boolean))].sort()), catchError(() => of([])))
1517
+ .subscribe(names => this.$formOptions.set(names)));
1518
+ }
1519
+ arraysEqual(left, right) {
1520
+ return left.length === right.length && left.every((value, index) => value === right[index]);
1521
+ }
1522
+ collectDefinitionErrors() {
1523
+ const errors = [];
1524
+ const formErrors = this.form?.errors ?? {};
1525
+ const stepErrors = this.form?.get('steps')?.errors ?? {};
1526
+ if (stepErrors['duplicateStepKeys']) {
1527
+ errors.push(this.translateService.instant('formFlow.uiEditor.errors.duplicateKeys', {
1528
+ keys: stepErrors['duplicateStepKeys'].keys.join(', '),
1529
+ }));
1530
+ }
1531
+ if (formErrors['startStepMissing']) {
1532
+ errors.push(this.translateService.instant('formFlow.uiEditor.errors.startStepMissing', {
1533
+ startStep: formErrors['startStepMissing'].startStep,
1534
+ }));
1535
+ }
1536
+ if (formErrors['unknownTransitionTargets']) {
1537
+ errors.push(this.translateService.instant('formFlow.uiEditor.errors.unknownTargets', {
1538
+ targets: formErrors['unknownTransitionTargets'].targets.join(', '),
1539
+ }));
1540
+ }
1541
+ // A step can be invalid on its own — a missing type, an unselected form, an empty transition
1542
+ // target or expression — which the definition-level checks above never name. `invalid` (not
1543
+ // `!valid`) skips disabled controls, so a read-only flow is never reported as incomplete; this
1544
+ // mirrors the warning icon shown per step. The notification is only *shown* after a save attempt
1545
+ // (see `revealErrors` in the template), so collecting these stays quiet while modelling.
1546
+ this.form?.get('steps')?.controls.forEach((stepGroup, index) => {
1547
+ if (!stepGroup.invalid)
1548
+ return;
1549
+ const key = (stepGroup.get('key')?.value ?? '').trim();
1550
+ errors.push(this.translateService.instant('formFlow.uiEditor.errors.stepIncomplete', {
1551
+ step: key || `#${index + 1}`,
1552
+ }));
1553
+ });
1554
+ return errors;
1555
+ }
1556
+ emitState() {
1557
+ if (!this.form)
1558
+ return;
1559
+ const definition = this.formService.serialize(this.form, this._definitionKey);
1560
+ this.valueChangeEvent.emit(JSON.stringify(definition));
1561
+ this.validEvent.emit(this.form.valid);
1562
+ }
1563
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowUiEditorTabComponent, deps: [{ token: FormFlowEditorFormService }, { token: FormFlowService }, { token: FormFlowComponentService }, { token: i4$1.FormManagementService }, { token: i3.ActivatedRoute }, { token: i4.TranslateService }], target: i0.ɵɵFactoryTarget.Component }); }
1564
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: FormFlowUiEditorTabComponent, isStandalone: true, selector: "valtimo-form-flow-ui-editor-tab", inputs: { model: "model", readOnly: "readOnly", revealErrors: "revealErrors" }, outputs: { validEvent: "validEvent", valueChangeEvent: "valueChangeEvent" }, providers: [FormFlowEditorFormService], usesOnChanges: true, ngImport: i0, template: "<!--\n ~ Copyright 2015-2026 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n@if ($parseFailed()) {\n <div class=\"ui-editor__notification\">\n <cds-inline-notification\n [notificationObj]=\"{\n type: 'warning',\n title: 'formFlow.uiEditor.parseFailedTitle' | translate,\n message: 'formFlow.uiEditor.parseFailedMessage' | translate,\n showClose: false,\n lowContrast: true,\n }\"\n ></cds-inline-notification>\n </div>\n} @else if (form && registry) {\n <div [formGroup]=\"form\">\n <!-- fitPage sizes this container to the viewport; the notification, sidebar and panel all live\n inside it, so toggling the notification reflows the body instead of shifting the container's\n top offset (which fitPage only re-measures on a header change or resize). -->\n <div class=\"ui-editor\" fitPage>\n @if (revealErrors && $definitionErrors().length) {\n <div class=\"ui-editor__notification\">\n <cds-inline-notification\n [notificationObj]=\"{\n type: 'error',\n title: 'formFlow.uiEditor.errors.title' | translate,\n message: $definitionErrors().join(' '),\n showClose: false,\n lowContrast: true,\n }\"\n ></cds-inline-notification>\n </div>\n }\n\n <div class=\"ui-editor__body\">\n <aside class=\"ui-editor__sidebar\">\n <valtimo-form-flow-step-list\n [readOnly]=\"readOnly\"\n [selectedIndex]=\"$selectedIndex()\"\n [steps]=\"$stepListItems()\"\n (addEvent)=\"onAddStep()\"\n (selectEvent)=\"onSelectStep($event)\"\n ></valtimo-form-flow-step-list>\n </aside>\n\n <div class=\"ui-editor__panel\">\n @if (selectedStepGroup; as stepGroup) {\n <valtimo-form-flow-step-detail\n [componentOptions]=\"$componentOptions()\"\n [duplicateKey]=\"isSelectedKeyDuplicate()\"\n [formOptions]=\"$formOptions()\"\n [isStartStep]=\"isSelectedStepStart()\"\n [readOnly]=\"readOnly\"\n [registry]=\"registry\"\n [revealErrors]=\"revealErrors\"\n [stepGroup]=\"stepGroup\"\n [stepKeys]=\"$stepKeys()\"\n (deleteStepEvent)=\"onDeleteStep()\"\n (makeStartEvent)=\"onMakeStartStep()\"\n ></valtimo-form-flow-step-detail>\n } @else {\n <div class=\"ui-editor__empty\">\n <h5 class=\"ui-editor__empty-title\">\n {{ 'formFlow.uiEditor.noStepsTitle' | translate }}\n </h5>\n\n <p class=\"ui-editor__empty-message\">\n {{ 'formFlow.uiEditor.noStepsDescription' | translate }}\n </p>\n </div>\n }\n </div>\n </div>\n </div>\n </div>\n} @else {\n <div class=\"ui-editor__loading\">\n <cds-loading></cds-loading>\n </div>\n}\n", styles: [".ui-editor{display:flex;flex-direction:column;border:1px solid var(--cds-border-subtle, #e0e0e0);overflow:hidden}.ui-editor__body{display:flex;flex:1 1 auto;min-height:0}.ui-editor__sidebar{background:var(--cds-layer-01, #f4f4f4);border-right:1px solid var(--cds-border-subtle, #e0e0e0);display:flex;flex:0 0 22rem;flex-direction:column;min-height:0}.ui-editor__panel{flex:1 1 auto;min-width:0;overflow-y:auto;padding:1rem 1.5rem 1.5rem}.ui-editor__notification{flex:0 0 auto}.ui-editor__notification cds-inline-notification{display:block;max-width:none;margin:0}.ui-editor__notification ::ng-deep .cds--inline-notification--low-contrast:before{border-width:0 0 1px 0}.ui-editor__empty{align-items:center;border:1px dashed var(--cds-border-strong, #8d8d8d);display:flex;flex-direction:column;gap:.5rem;margin:2rem auto;max-width:30rem;padding:2rem;text-align:center}.ui-editor__empty-title{margin:0}.ui-editor__empty-message{color:var(--cds-text-secondary, #525252);margin:0 0 .5rem}.ui-editor__loading{display:flex;width:100%;justify-content:center;padding:var(--cds-spacing-07)}\n/*!\n * Copyright 2015-2026 Ritense BV, the Netherlands.\n *\n * Licensed under EUPL, Version 1.2 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" basis,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i4.TranslatePipe, name: "translate" }, { kind: "directive", type: FitPageDirective, selector: "[fitPage]", inputs: ["spaceAdjustment", "fitPageDisabled", "disableOverflow", "bottomMargin"] }, { kind: "ngmodule", type: LoadingModule }, { kind: "component", type: i2$1.Loading, selector: "cds-loading, ibm-loading", inputs: ["title", "isActive", "size", "overlay"] }, { kind: "ngmodule", type: NotificationModule }, { kind: "component", type: i2$1.Notification, selector: "cds-notification, cds-inline-notification, ibm-notification, ibm-inline-notification", inputs: ["notificationObj"] }, { kind: "component", type: FormFlowStepDetailComponent, selector: "valtimo-form-flow-step-detail", inputs: ["stepGroup", "registry", "stepKeys", "formOptions", "componentOptions", "isStartStep", "readOnly", "duplicateKey", "revealErrors"], outputs: ["deleteStepEvent", "makeStartEvent"] }, { kind: "component", type: FormFlowStepListComponent, selector: "valtimo-form-flow-step-list", inputs: ["steps", "selectedIndex", "readOnly"], outputs: ["selectEvent", "addEvent"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1565
+ }
1566
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowUiEditorTabComponent, decorators: [{
1567
+ type: Component,
1568
+ args: [{ standalone: true, selector: 'valtimo-form-flow-ui-editor-tab', changeDetection: ChangeDetectionStrategy.OnPush, providers: [FormFlowEditorFormService], imports: [
1569
+ CommonModule,
1570
+ ReactiveFormsModule,
1571
+ TranslateModule,
1572
+ FitPageDirective,
1573
+ LoadingModule,
1574
+ NotificationModule,
1575
+ FormFlowStepDetailComponent,
1576
+ FormFlowStepListComponent,
1577
+ ], template: "<!--\n ~ Copyright 2015-2026 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n@if ($parseFailed()) {\n <div class=\"ui-editor__notification\">\n <cds-inline-notification\n [notificationObj]=\"{\n type: 'warning',\n title: 'formFlow.uiEditor.parseFailedTitle' | translate,\n message: 'formFlow.uiEditor.parseFailedMessage' | translate,\n showClose: false,\n lowContrast: true,\n }\"\n ></cds-inline-notification>\n </div>\n} @else if (form && registry) {\n <div [formGroup]=\"form\">\n <!-- fitPage sizes this container to the viewport; the notification, sidebar and panel all live\n inside it, so toggling the notification reflows the body instead of shifting the container's\n top offset (which fitPage only re-measures on a header change or resize). -->\n <div class=\"ui-editor\" fitPage>\n @if (revealErrors && $definitionErrors().length) {\n <div class=\"ui-editor__notification\">\n <cds-inline-notification\n [notificationObj]=\"{\n type: 'error',\n title: 'formFlow.uiEditor.errors.title' | translate,\n message: $definitionErrors().join(' '),\n showClose: false,\n lowContrast: true,\n }\"\n ></cds-inline-notification>\n </div>\n }\n\n <div class=\"ui-editor__body\">\n <aside class=\"ui-editor__sidebar\">\n <valtimo-form-flow-step-list\n [readOnly]=\"readOnly\"\n [selectedIndex]=\"$selectedIndex()\"\n [steps]=\"$stepListItems()\"\n (addEvent)=\"onAddStep()\"\n (selectEvent)=\"onSelectStep($event)\"\n ></valtimo-form-flow-step-list>\n </aside>\n\n <div class=\"ui-editor__panel\">\n @if (selectedStepGroup; as stepGroup) {\n <valtimo-form-flow-step-detail\n [componentOptions]=\"$componentOptions()\"\n [duplicateKey]=\"isSelectedKeyDuplicate()\"\n [formOptions]=\"$formOptions()\"\n [isStartStep]=\"isSelectedStepStart()\"\n [readOnly]=\"readOnly\"\n [registry]=\"registry\"\n [revealErrors]=\"revealErrors\"\n [stepGroup]=\"stepGroup\"\n [stepKeys]=\"$stepKeys()\"\n (deleteStepEvent)=\"onDeleteStep()\"\n (makeStartEvent)=\"onMakeStartStep()\"\n ></valtimo-form-flow-step-detail>\n } @else {\n <div class=\"ui-editor__empty\">\n <h5 class=\"ui-editor__empty-title\">\n {{ 'formFlow.uiEditor.noStepsTitle' | translate }}\n </h5>\n\n <p class=\"ui-editor__empty-message\">\n {{ 'formFlow.uiEditor.noStepsDescription' | translate }}\n </p>\n </div>\n }\n </div>\n </div>\n </div>\n </div>\n} @else {\n <div class=\"ui-editor__loading\">\n <cds-loading></cds-loading>\n </div>\n}\n", styles: [".ui-editor{display:flex;flex-direction:column;border:1px solid var(--cds-border-subtle, #e0e0e0);overflow:hidden}.ui-editor__body{display:flex;flex:1 1 auto;min-height:0}.ui-editor__sidebar{background:var(--cds-layer-01, #f4f4f4);border-right:1px solid var(--cds-border-subtle, #e0e0e0);display:flex;flex:0 0 22rem;flex-direction:column;min-height:0}.ui-editor__panel{flex:1 1 auto;min-width:0;overflow-y:auto;padding:1rem 1.5rem 1.5rem}.ui-editor__notification{flex:0 0 auto}.ui-editor__notification cds-inline-notification{display:block;max-width:none;margin:0}.ui-editor__notification ::ng-deep .cds--inline-notification--low-contrast:before{border-width:0 0 1px 0}.ui-editor__empty{align-items:center;border:1px dashed var(--cds-border-strong, #8d8d8d);display:flex;flex-direction:column;gap:.5rem;margin:2rem auto;max-width:30rem;padding:2rem;text-align:center}.ui-editor__empty-title{margin:0}.ui-editor__empty-message{color:var(--cds-text-secondary, #525252);margin:0 0 .5rem}.ui-editor__loading{display:flex;width:100%;justify-content:center;padding:var(--cds-spacing-07)}\n/*!\n * Copyright 2015-2026 Ritense BV, the Netherlands.\n *\n * Licensed under EUPL, Version 1.2 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" basis,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n"] }]
1578
+ }], ctorParameters: () => [{ type: FormFlowEditorFormService }, { type: FormFlowService }, { type: FormFlowComponentService }, { type: i4$1.FormManagementService }, { type: i3.ActivatedRoute }, { type: i4.TranslateService }], propDecorators: { model: [{
1579
+ type: Input
1580
+ }], readOnly: [{
1581
+ type: Input
1582
+ }], revealErrors: [{
1583
+ type: Input
1584
+ }], validEvent: [{
1585
+ type: Output
1586
+ }], valueChangeEvent: [{
1587
+ type: Output
1588
+ }] } });
1589
+
1590
+ /*
1591
+ * Copyright 2015-2025 Ritense BV, the Netherlands.
1592
+ *
1593
+ * Licensed under EUPL, Version 1.2 (the "License");
1594
+ * you may not use this file except in compliance with the License.
1595
+ * You may obtain a copy of the License at
1596
+ *
1597
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
1598
+ *
1599
+ * Unless required by applicable law or agreed to in writing, software
1600
+ * distributed under the License is distributed on an "AS IS" basis,
1601
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1602
+ * See the License for the specific language governing permissions and
1603
+ * limitations under the License.
1604
+ */
1605
+ class FormFlowEditorComponent extends PendingChangesComponent {
1606
+ constructor(breadcrumbService, formFlowDownloadService, formFlowService, iconService, notificationService, pageHeaderService, pageTitleService, route, router, translateService) {
1607
+ super();
1608
+ this.breadcrumbService = breadcrumbService;
1609
+ this.formFlowDownloadService = formFlowDownloadService;
1610
+ this.formFlowService = formFlowService;
1611
+ this.iconService = iconService;
1612
+ this.notificationService = notificationService;
1613
+ this.pageHeaderService = pageHeaderService;
1614
+ this.pageTitleService = pageTitleService;
1615
+ this.route = route;
1616
+ this.router = router;
1617
+ this.translateService = translateService;
1618
+ this.readOnly$ = new BehaviorSubject(false);
1619
+ this.valid$ = new BehaviorSubject(false);
1620
+ this.loading$ = new BehaviorSubject(true);
1621
+ this.showDeleteModal$ = new BehaviorSubject(false);
1622
+ this.$activeTab = signal(FormFlowEditorTab.JSON_EDITOR);
1623
+ // Validate-on-save: while the admin is modelling nothing is flagged. Clicking Save on an invalid
1624
+ // definition flips this instead of calling the backend, which reveals the errors in the active
1625
+ // tab and — from then on — gates the Save button on validity until the definition is fixed.
1626
+ this.$saveAttempted = signal(false);
1627
+ this.FormFlowEditorTab = FormFlowEditorTab;
1628
+ this.testIds = FORM_FLOW_EDITOR_TEST_IDS;
1629
+ this._context$ = getContextObservable(this.route);
1630
+ this._params$ = this._context$.pipe(switchMap(context => {
1631
+ if (context === 'buildingBlock') {
1632
+ return combineLatest([
1633
+ getBuildingBlockManagementRouteParams(this.route),
1634
+ this.route.params,
1635
+ ]).pipe(map(([bbParams, params]) => ({
1636
+ caseDefinitionKey: bbParams?.buildingBlockDefinitionKey ?? '',
1637
+ caseDefinitionVersionTag: bbParams?.buildingBlockDefinitionVersionTag ?? '',
1638
+ formFlowDefinitionKey: params.formFlowDefinitionKey,
1639
+ })));
1640
+ }
1641
+ return combineLatest([
1642
+ getCaseManagementRouteParams(this.route),
1643
+ this.route.params,
1644
+ ]).pipe(map(([caseManagementParams, params]) => ({
1645
+ ...(caseManagementParams ?? { caseDefinitionKey: '', caseDefinitionVersionTag: '' }),
1646
+ ...params,
1647
+ })));
1648
+ }));
1649
+ this.formFlowSchemaJson$ = this.formFlowService
1650
+ .getFormFlowDefinitionSchema()
1651
+ .pipe(shareReplay(1));
1652
+ this._formFlowDefinition2$ = combineLatest([this._params$, this._context$]).pipe(tap(() => this.loading$.next(true)), switchMap(([params, context]) => {
1653
+ this.initBreadcrumbs(params, context);
1654
+ if (context === 'buildingBlock') {
1655
+ return this.formFlowService.getBuildingBlockFormFlowDefinitionByKey(params.caseDefinitionKey, params.caseDefinitionVersionTag, params.formFlowDefinitionKey);
1656
+ }
1657
+ return this.formFlowService.getFormFlowDefinitionByKey(params.caseDefinitionKey, params.caseDefinitionVersionTag, params.formFlowDefinitionKey);
1658
+ }), tap((formFlowDefinition) => {
1659
+ this.pageTitleService.setCustomPageTitle(formFlowDefinition.key);
1660
+ this.readOnly$.next(formFlowDefinition.readOnly === true);
1661
+ this.loading$.next(false);
1662
+ // A freshly loaded definition is the clean baseline: the next editor emission recaptures it,
1663
+ // saving without edits is possible from either tab, and the unsaved-changes flag is cleared.
1664
+ this._updatedModelValue$.next(this.serializeDefinition(formFlowDefinition));
1665
+ this._pendingBaseline = null;
1666
+ this.pendingChanges = false;
1667
+ this.valid$.next(true);
1668
+ // A reloaded definition starts a fresh modelling session; drop any prior save-attempt state.
1669
+ this.$saveAttempted.set(false);
1670
+ }));
1671
+ this.model$ = this._formFlowDefinition2$.pipe(map((formFlowDefinition) => this.getEditorModel(formFlowDefinition)));
1672
+ this._updatedModelValue$ = new BehaviorSubject('');
1673
+ // The editor's serialized value captured on (re)load. Edits that differ from it mark the editor
1674
+ // dirty so the leave-page guard can warn about unsaved changes.
1675
+ this._pendingBaseline = null;
1676
+ this.compactMode$ = this.pageHeaderService.compactMode$;
1677
+ this.iconService.registerAll([ArrowLeft16]);
1678
+ this.pageTitleService.disableReset();
1679
+ this.restoreActiveTabFromUrl();
1680
+ }
1681
+ ngOnDestroy() {
1682
+ this.pendingChanges = false;
1683
+ this.pageTitleService.enableReset();
1684
+ this.breadcrumbService.clearThirdBreadcrumb();
1685
+ this.breadcrumbService.clearFourthBreadcrumb();
1686
+ }
1687
+ onValid(valid) {
1688
+ this.valid$.next(valid);
1689
+ }
1690
+ onValueChange(value) {
1691
+ // The first emission after a (re)load is the editor's baseline. Comparison is done on the
1692
+ // normalized (whitespace-insensitive) JSON so the JSON editor's format-on-load reflow is not
1693
+ // mistaken for an edit; only a genuine content change marks the editor dirty.
1694
+ const normalized = this.normalizeJson(value);
1695
+ if (this._pendingBaseline === null) {
1696
+ this._pendingBaseline = normalized;
1697
+ this.pendingChanges = false;
1698
+ }
1699
+ else {
1700
+ this.pendingChanges = normalized !== this._pendingBaseline;
1701
+ }
1702
+ this._updatedModelValue$.next(value);
1703
+ }
1704
+ setActiveTab(tab) {
1705
+ if (this.$activeTab() === tab)
1706
+ return;
1707
+ combineLatest([this._params$, this._context$])
1708
+ .pipe(take(1))
1709
+ .subscribe(([params, context]) => {
1710
+ this.router.navigate(this.editorRouteSegments(params, context, tab));
1711
+ });
1712
+ }
1713
+ onCancelRedirect() {
1714
+ this.syncTabHeaders();
1715
+ }
1716
+ updateFormFlowDefinition() {
1717
+ // Validate on the click: while modelling nothing is flagged, so clicking Save on an invalid
1718
+ // definition reveals its errors (via $saveAttempted → the tabs' revealErrors) instead of
1719
+ // sending it to the backend. From here on the Save button gates on validity until it is fixed.
1720
+ if (!this.valid$.value) {
1721
+ this.$saveAttempted.set(true);
1722
+ return;
1723
+ }
1724
+ this.loading$.next(true);
1725
+ combineLatest([this._params$, this._updatedModelValue$, this._context$])
1726
+ .pipe(take(1), switchMap(([params, updatedModelValue, context]) => {
1727
+ const updatedDefinition = {
1728
+ ...JSON.parse(updatedModelValue),
1729
+ key: params.formFlowDefinitionKey,
1730
+ };
1731
+ if (context === 'buildingBlock') {
1732
+ return this.formFlowService.updateBuildingBlockFormFlowDefinition(params.caseDefinitionKey, params.caseDefinitionVersionTag, params.formFlowDefinitionKey, updatedDefinition);
1733
+ }
1734
+ return this.formFlowService.updateFormFlowDefinition(params.caseDefinitionKey, params.caseDefinitionVersionTag, params.formFlowDefinitionKey, updatedDefinition);
1735
+ }), finalize(() => this.loading$.next(false)))
1736
+ .subscribe(result => {
1737
+ // The saved value is the new clean baseline for the leave-page guard.
1738
+ this._pendingBaseline = this.normalizeJson(this._updatedModelValue$.getValue());
1739
+ this.pendingChanges = false;
1740
+ this.$saveAttempted.set(false);
1741
+ this.showSuccessMessage(result.key);
1742
+ });
1743
+ }
1744
+ onDelete() {
1745
+ this.loading$.next(true);
1746
+ this.pendingChanges = false;
1747
+ combineLatest([this._params$, this._context$])
1748
+ .pipe(take(1), switchMap(([params, context]) => {
1749
+ if (context === 'buildingBlock') {
1750
+ return this.formFlowService
1751
+ .deleteBuildingBlockFormFlowDefinition(params.caseDefinitionKey, params.caseDefinitionVersionTag, params.formFlowDefinitionKey)
1752
+ .pipe(map(() => ({ params, context })));
1753
+ }
1754
+ return this.formFlowService
1755
+ .deleteFormFlowDefinition(params.caseDefinitionKey, params.caseDefinitionVersionTag, params.formFlowDefinitionKey)
1756
+ .pipe(map(() => ({ params, context })));
1757
+ }))
1758
+ .subscribe(({ params, context }) => {
1759
+ this.router.navigate(this.overviewRouteSegments(params, context));
1760
+ });
1761
+ }
1762
+ showDeleteModal() {
1763
+ this.showDeleteModal$.next(true);
1764
+ }
1765
+ downloadFormFlowDefinition(model) {
1766
+ this._params$
1767
+ .pipe(take(1))
1768
+ .subscribe((params) => this.formFlowDownloadService.downloadJson(JSON.parse(model.value), params));
1769
+ }
1770
+ navigateBack() {
1771
+ combineLatest([this._params$, this._context$])
1772
+ .pipe(take(1))
1773
+ .subscribe(([params, context]) => {
1774
+ this.router.navigate(this.overviewRouteSegments(params, context));
1775
+ });
1776
+ }
1777
+ restoreActiveTabFromUrl() {
1778
+ const url = this.route.snapshot.url;
1779
+ const lastSegment = url[url.length - 1]?.path;
1780
+ if (lastSegment === 'editor') {
1781
+ this.$activeTab.set(FormFlowEditorTab.EDITOR);
1782
+ }
1783
+ }
1784
+ // The Carbon tab header highlights the clicked tab immediately; if the leave-page prompt is
1785
+ // cancelled, restore the highlight to the tab that is actually still active.
1786
+ syncTabHeaders() {
1787
+ const order = [FormFlowEditorTab.JSON_EDITOR, FormFlowEditorTab.EDITOR];
1788
+ this._tabs?.forEach((tab, index) => {
1789
+ tab.active = order[index] === this.$activeTab();
1790
+ });
1791
+ }
1792
+ editorRouteSegments(params, context, tab) {
1793
+ const segments = [...this.overviewRouteSegments(params, context), params.formFlowDefinitionKey];
1794
+ return tab === FormFlowEditorTab.EDITOR ? [...segments, 'editor'] : segments;
1795
+ }
1796
+ overviewRouteSegments(params, context) {
1797
+ if (context === 'buildingBlock') {
1798
+ return [
1799
+ '/building-block-management',
1800
+ 'building-block',
1801
+ params.caseDefinitionKey,
1802
+ 'version',
1803
+ params.caseDefinitionVersionTag,
1804
+ 'form-flows',
1805
+ ];
1806
+ }
1807
+ return [
1808
+ '/case-management',
1809
+ 'case',
1810
+ params.caseDefinitionKey,
1811
+ 'version',
1812
+ params.caseDefinitionVersionTag,
1813
+ 'form-flows',
1814
+ ];
1815
+ }
1816
+ serializeDefinition(formFlowDefinition) {
1817
+ const clone = { ...formFlowDefinition };
1818
+ delete clone.readOnly;
1819
+ return JSON.stringify(clone);
1820
+ }
1821
+ getEditorModel(formFlowDefinition) {
1822
+ return {
1823
+ value: this.serializeDefinition(formFlowDefinition),
1824
+ language: 'json',
1825
+ uri: `inmemory://form-flow/${formFlowDefinition.key}.formflow.json`,
1826
+ };
1827
+ }
1828
+ // Canonicalizes a JSON string by dropping insignificant whitespace, so reformatting (such as the
1829
+ // JSON editor's format-on-load) does not register as a change. Invalid JSON is compared as-is.
1830
+ normalizeJson(value) {
1831
+ try {
1832
+ return JSON.stringify(JSON.parse(value));
1833
+ }
1834
+ catch {
1835
+ return value;
1836
+ }
1837
+ }
1838
+ showSuccessMessage(formFlowDefinitionKey) {
1839
+ this.notificationService.showToast({
1840
+ caption: this.translateService.instant('formFlow.savedSuccessTitleMessage', {
1841
+ key: formFlowDefinitionKey,
1842
+ }),
1843
+ type: 'success',
1844
+ title: this.translateService.instant('formFlow.savedSuccessTitle'),
1845
+ });
1846
+ }
1847
+ initBreadcrumbs(params, context) {
1848
+ if (context === 'buildingBlock') {
1849
+ const route = `/building-block-management/building-block/${params.caseDefinitionKey}/version/${params.caseDefinitionVersionTag}`;
1850
+ const generalRoute = `${route}/general`;
1851
+ this.breadcrumbService.setThirdBreadcrumb({
1852
+ route: [generalRoute],
1853
+ content: `${params.caseDefinitionKey} (${params.caseDefinitionVersionTag})`,
1854
+ href: generalRoute,
1855
+ });
1856
+ const routeWithFormFlows = `${route}/form-flows`;
1857
+ // The breadcrumb service translates this key on render (and re-translates on load / language
1858
+ // switch), so a direct page load shows the label, not the raw key.
1859
+ this.breadcrumbService.setFourthBreadcrumb({
1860
+ route: [routeWithFormFlows],
1861
+ content: 'buildingBlockManagement.tabs.formFlows',
1862
+ href: routeWithFormFlows,
1863
+ });
1864
+ }
1865
+ else {
1866
+ const route = `/case-management/case/${params.caseDefinitionKey}/version/${params.caseDefinitionVersionTag}`;
1867
+ this.breadcrumbService.setThirdBreadcrumb({
1868
+ route: [route],
1869
+ content: `${params.caseDefinitionKey} (${params.caseDefinitionVersionTag})`,
1870
+ href: route,
1871
+ });
1872
+ const routeWithFormFlows = `${route}/form-flows`;
1873
+ this.breadcrumbService.setFourthBreadcrumb({
1874
+ route: [routeWithFormFlows],
1875
+ content: 'caseManagement.tabs.formFlows',
1876
+ href: routeWithFormFlows,
1877
+ });
1878
+ }
1879
+ }
1880
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowEditorComponent, deps: [{ token: i1$2.BreadcrumbService }, { token: FormFlowDownloadService }, { token: FormFlowService }, { token: i2$1.IconService }, { token: i2.GlobalNotificationService }, { token: i1$2.PageHeaderService }, { token: i1$2.PageTitleService }, { token: i3.ActivatedRoute }, { token: i3.Router }, { token: i4.TranslateService }], target: i0.ɵɵFactoryTarget.Component }); }
1881
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.25", type: FormFlowEditorComponent, isStandalone: false, selector: "ng-component", viewQueries: [{ propertyName: "_tabs", predicate: Tab, descendants: true }], usesInheritance: true, ngImport: i0, template: "<!--\n ~ Copyright 2015-2025 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n<ng-container *ngIf=\"model$ | async as model; else loading\">\n <!-- The headers only switch the active tab; the tab content is rendered below (gated by the\n active-tab signal) instead of inside the Carbon tab panes. Carbon removes a pane's content\n as soon as another header is clicked, which would destroy the editor's unsaved form before\n the leave-page confirmation is resolved. -->\n <cds-tabs type=\"contained\">\n <cds-tab\n [active]=\"$activeTab() === FormFlowEditorTab.JSON_EDITOR\"\n [heading]=\"'formFlow.tabs.jsonEditor' | translate\"\n (selected)=\"setActiveTab(FormFlowEditorTab.JSON_EDITOR)\"\n ></cds-tab>\n\n <cds-tab\n [active]=\"$activeTab() === FormFlowEditorTab.EDITOR\"\n [heading]=\"'formFlow.tabs.editor' | translate\"\n (selected)=\"setActiveTab(FormFlowEditorTab.EDITOR)\"\n ></cds-tab>\n </cds-tabs>\n\n <!-- White surface for the tab content. The Carbon tab pane used to provide this; since the\n content is rendered outside the panes, the wrapper supplies the layer background. -->\n <div class=\"form-flow-editor__content\">\n @if ($activeTab() === FormFlowEditorTab.EDITOR) {\n <valtimo-form-flow-ui-editor-tab\n [model]=\"model\"\n [readOnly]=\"readOnly$ | async\"\n [revealErrors]=\"$saveAttempted()\"\n (validEvent)=\"onValid($event)\"\n (valueChangeEvent)=\"onValueChange($event)\"\n ></valtimo-form-flow-ui-editor-tab>\n }\n\n @if ($activeTab() === FormFlowEditorTab.JSON_EDITOR) {\n <valtimo-form-flow-json-editor-tab\n [disabled]=\"(loading$ | async) === false && (readOnly$ | async)\"\n [model]=\"model\"\n [schema]=\"formFlowSchemaJson$ | async\"\n (validEvent)=\"onValid($event)\"\n (valueChangeEvent)=\"onValueChange($event)\"\n ></valtimo-form-flow-json-editor-tab>\n }\n </div>\n</ng-container>\n\n<ng-container renderInPageHeader>\n <ng-template>\n <div\n *ngIf=\"{\n compactMode: compactMode$ | async,\n valid: valid$ | async,\n readOnly: readOnly$ | async,\n model: model$ | async,\n } as obs\"\n class=\"buttons-container\"\n >\n <v-overflow-menu placement=\"bottom-end\">\n <v-overflow-menu-trigger overflowTrigger></v-overflow-menu-trigger>\n <v-overflow-menu-option\n [disabled]=\"!obs.valid && !obs.readOnly\"\n (selected)=\"downloadFormFlowDefinition(obs.model)\"\n >{{ 'interface.export' | translate }}\n </v-overflow-menu-option>\n\n <v-overflow-menu-option\n [disabled]=\"obs.readOnly\"\n type=\"danger\"\n (selected)=\"showDeleteModal()\"\n >{{ 'interface.delete' | translate }}\n </v-overflow-menu-option>\n </v-overflow-menu>\n\n <button\n cdsButton=\"secondary\"\n [size]=\"obs.compactMode ? 'sm' : 'md'\"\n [attr.data-test-id]=\"testIds.backButton\"\n (click)=\"navigateBack()\"\n >\n {{ 'interface.back' | translate }}\n\n <svg class=\"cds--btn__icon\" cdsIcon=\"arrow--left\" size=\"16\"></svg>\n </button>\n\n <button\n [disabled]=\"obs.readOnly || ($saveAttempted() && !obs.valid)\"\n cdsButton=\"primary\"\n [size]=\"obs.compactMode ? 'sm' : 'md'\"\n [attr.data-test-id]=\"testIds.saveButton\"\n (click)=\"updateFormFlowDefinition()\"\n >\n {{ 'interface.save' | translate }}\n\n <svg class=\"cds--btn__icon\" cdsIcon=\"save\" size=\"16\"></svg>\n </button>\n </div>\n </ng-template>\n</ng-container>\n\n<valtimo-confirmation-modal\n confirmButtonTextTranslationKey=\"interface.delete\"\n confirmButtonType=\"danger\"\n contentTranslationKey=\"formFlow.deleteModalContent\"\n [showModalSubject$]=\"showDeleteModal$\"\n titleTranslationKey=\"interface.delete\"\n (confirmEvent)=\"onDelete()\"\n></valtimo-confirmation-modal>\n\n<ng-template #loading>\n <div class=\"loading-container\">\n <cds-loading></cds-loading>\n </div>\n</ng-template>\n", styles: ["cds-loading{display:flex;justify-content:center}.loading-container{display:flex;width:100%;justify-content:center}.buttons-container{display:flex;align-items:center;justify-self:flex-end;gap:16px}.buttons-container button{height:min-content}:host ::ng-deep .cds--tab-content{background-color:var(--cds-layer);outline:none!important;padding:0}.form-flow-editor__content{background-color:var(--cds-layer)}\n/*!\n * Copyright 2015-2025 Ritense BV, the Netherlands.\n *\n * Licensed under EUPL, Version 1.2 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" basis,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n"], dependencies: [{ kind: "directive", type: i5.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2$1.Button, selector: "[cdsButton], [ibmButton]", inputs: ["ibmButton", "cdsButton", "size", "skeleton", "iconOnly", "isExpressive"] }, { kind: "directive", type: i2$1.IconDirective, selector: "[cdsIcon], [ibmIcon]", inputs: ["ibmIcon", "cdsIcon", "size", "title", "ariaLabel", "ariaLabelledBy", "ariaHidden", "isFocusable"] }, { kind: "component", type: i1$2.ConfirmationModalComponent, selector: "valtimo-confirmation-modal", inputs: ["titleTranslationKey", "title", "content", "contentTranslationKey", "confirmButtonText", "confirmButtonTextTranslationKey", "confirmButtonType", "showOptionalButton", "optionalButtonText", "optionalButtonTextTranslationKey", "optionalButtonType", "cancelButtonText", "cancelButtonTextTranslationKey", "cancelButtonType", "showModalSubject$", "outputOnConfirm", "outputOnOptional", "spacerAfterCancelButton"], outputs: ["confirmEvent", "optionalEvent", "cancelEvent"] }, { kind: "directive", type: i1$2.RenderInPageHeaderDirective, selector: "[renderInPageHeader]", inputs: ["fullWidth"] }, { kind: "component", type: i2$1.Loading, selector: "cds-loading, ibm-loading", inputs: ["title", "isActive", "size", "overlay"] }, { kind: "component", type: i1$2.OverflowMenuComponent, selector: "v-overflow-menu", inputs: ["open", "placement", "menuWidth", "offsetX", "offsetY", "closeOnSelect", "useHostAsReference", "portalToBody"], outputs: ["openChange"] }, { kind: "component", type: i1$2.OverflowMenuOptionComponent, selector: "v-overflow-menu-option", inputs: ["disabled", "type", "testId", "optionId"], outputs: ["selected"] }, { kind: "component", type: i1$2.OverflowMenuTriggerComponent, selector: "v-overflow-menu-trigger", inputs: ["compact", "label"] }, { kind: "component", type: i2$1.Tabs, selector: "cds-tabs, ibm-tabs", inputs: ["position", "cacheActive", "followFocus", "isNavigation", "ariaLabel", "ariaLabelledby", "type", "theme", "skeleton"] }, { kind: "component", type: i2$1.Tab, selector: "cds-tab, ibm-tab", inputs: ["heading", "title", "context", "active", "disabled", "tabIndex", "id", "cacheActive", "tabContent", "templateContext"], outputs: ["selected"] }, { kind: "component", type: FormFlowJsonEditorTabComponent, selector: "valtimo-form-flow-json-editor-tab", inputs: ["disabled", "model", "schema"], outputs: ["validEvent", "valueChangeEvent"] }, { kind: "component", type: FormFlowUiEditorTabComponent, selector: "valtimo-form-flow-ui-editor-tab", inputs: ["model", "readOnly", "revealErrors"], outputs: ["validEvent", "valueChangeEvent"] }, { kind: "pipe", type: i5.AsyncPipe, name: "async" }, { kind: "pipe", type: i4.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1882
+ }
1883
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowEditorComponent, decorators: [{
1884
+ type: Component,
1885
+ args: [{ standalone: false, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!--\n ~ Copyright 2015-2025 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n<ng-container *ngIf=\"model$ | async as model; else loading\">\n <!-- The headers only switch the active tab; the tab content is rendered below (gated by the\n active-tab signal) instead of inside the Carbon tab panes. Carbon removes a pane's content\n as soon as another header is clicked, which would destroy the editor's unsaved form before\n the leave-page confirmation is resolved. -->\n <cds-tabs type=\"contained\">\n <cds-tab\n [active]=\"$activeTab() === FormFlowEditorTab.JSON_EDITOR\"\n [heading]=\"'formFlow.tabs.jsonEditor' | translate\"\n (selected)=\"setActiveTab(FormFlowEditorTab.JSON_EDITOR)\"\n ></cds-tab>\n\n <cds-tab\n [active]=\"$activeTab() === FormFlowEditorTab.EDITOR\"\n [heading]=\"'formFlow.tabs.editor' | translate\"\n (selected)=\"setActiveTab(FormFlowEditorTab.EDITOR)\"\n ></cds-tab>\n </cds-tabs>\n\n <!-- White surface for the tab content. The Carbon tab pane used to provide this; since the\n content is rendered outside the panes, the wrapper supplies the layer background. -->\n <div class=\"form-flow-editor__content\">\n @if ($activeTab() === FormFlowEditorTab.EDITOR) {\n <valtimo-form-flow-ui-editor-tab\n [model]=\"model\"\n [readOnly]=\"readOnly$ | async\"\n [revealErrors]=\"$saveAttempted()\"\n (validEvent)=\"onValid($event)\"\n (valueChangeEvent)=\"onValueChange($event)\"\n ></valtimo-form-flow-ui-editor-tab>\n }\n\n @if ($activeTab() === FormFlowEditorTab.JSON_EDITOR) {\n <valtimo-form-flow-json-editor-tab\n [disabled]=\"(loading$ | async) === false && (readOnly$ | async)\"\n [model]=\"model\"\n [schema]=\"formFlowSchemaJson$ | async\"\n (validEvent)=\"onValid($event)\"\n (valueChangeEvent)=\"onValueChange($event)\"\n ></valtimo-form-flow-json-editor-tab>\n }\n </div>\n</ng-container>\n\n<ng-container renderInPageHeader>\n <ng-template>\n <div\n *ngIf=\"{\n compactMode: compactMode$ | async,\n valid: valid$ | async,\n readOnly: readOnly$ | async,\n model: model$ | async,\n } as obs\"\n class=\"buttons-container\"\n >\n <v-overflow-menu placement=\"bottom-end\">\n <v-overflow-menu-trigger overflowTrigger></v-overflow-menu-trigger>\n <v-overflow-menu-option\n [disabled]=\"!obs.valid && !obs.readOnly\"\n (selected)=\"downloadFormFlowDefinition(obs.model)\"\n >{{ 'interface.export' | translate }}\n </v-overflow-menu-option>\n\n <v-overflow-menu-option\n [disabled]=\"obs.readOnly\"\n type=\"danger\"\n (selected)=\"showDeleteModal()\"\n >{{ 'interface.delete' | translate }}\n </v-overflow-menu-option>\n </v-overflow-menu>\n\n <button\n cdsButton=\"secondary\"\n [size]=\"obs.compactMode ? 'sm' : 'md'\"\n [attr.data-test-id]=\"testIds.backButton\"\n (click)=\"navigateBack()\"\n >\n {{ 'interface.back' | translate }}\n\n <svg class=\"cds--btn__icon\" cdsIcon=\"arrow--left\" size=\"16\"></svg>\n </button>\n\n <button\n [disabled]=\"obs.readOnly || ($saveAttempted() && !obs.valid)\"\n cdsButton=\"primary\"\n [size]=\"obs.compactMode ? 'sm' : 'md'\"\n [attr.data-test-id]=\"testIds.saveButton\"\n (click)=\"updateFormFlowDefinition()\"\n >\n {{ 'interface.save' | translate }}\n\n <svg class=\"cds--btn__icon\" cdsIcon=\"save\" size=\"16\"></svg>\n </button>\n </div>\n </ng-template>\n</ng-container>\n\n<valtimo-confirmation-modal\n confirmButtonTextTranslationKey=\"interface.delete\"\n confirmButtonType=\"danger\"\n contentTranslationKey=\"formFlow.deleteModalContent\"\n [showModalSubject$]=\"showDeleteModal$\"\n titleTranslationKey=\"interface.delete\"\n (confirmEvent)=\"onDelete()\"\n></valtimo-confirmation-modal>\n\n<ng-template #loading>\n <div class=\"loading-container\">\n <cds-loading></cds-loading>\n </div>\n</ng-template>\n", styles: ["cds-loading{display:flex;justify-content:center}.loading-container{display:flex;width:100%;justify-content:center}.buttons-container{display:flex;align-items:center;justify-self:flex-end;gap:16px}.buttons-container button{height:min-content}:host ::ng-deep .cds--tab-content{background-color:var(--cds-layer);outline:none!important;padding:0}.form-flow-editor__content{background-color:var(--cds-layer)}\n/*!\n * Copyright 2015-2025 Ritense BV, the Netherlands.\n *\n * Licensed under EUPL, Version 1.2 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" basis,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n"] }]
1886
+ }], ctorParameters: () => [{ type: i1$2.BreadcrumbService }, { type: FormFlowDownloadService }, { type: FormFlowService }, { type: i2$1.IconService }, { type: i2.GlobalNotificationService }, { type: i1$2.PageHeaderService }, { type: i1$2.PageTitleService }, { type: i3.ActivatedRoute }, { type: i3.Router }, { type: i4.TranslateService }], propDecorators: { _tabs: [{
1887
+ type: ViewChildren,
1888
+ args: [Tab]
1889
+ }] } });
1890
+
1891
+ /*
1892
+ * Copyright 2015-2025 Ritense BV, the Netherlands.
1893
+ *
1894
+ * Licensed under EUPL, Version 1.2 (the "License");
1895
+ * you may not use this file except in compliance with the License.
1896
+ * You may obtain a copy of the License at
1897
+ *
1898
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
1899
+ *
1900
+ * Unless required by applicable law or agreed to in writing, software
1901
+ * distributed under the License is distributed on an "AS IS" basis,
1902
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1903
+ * See the License for the specific language governing permissions and
1904
+ * limitations under the License.
1905
+ */
1906
+ class DeleteFormFlowModalComponent {
1907
+ constructor() {
1908
+ this.deleteEvent = new EventEmitter();
539
1909
  }
540
1910
  onDelete(formFlowDefinitionKey) {
541
1911
  this.deleteEvent.emit(formFlowDefinitionKey);
@@ -571,9 +1941,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
571
1941
  */
572
1942
  class FormFlowManagementModule {
573
1943
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowManagementModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
574
- static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.25", ngImport: i0, type: FormFlowManagementModule, declarations: [FormFlowOverviewComponent,
575
- FormFlowEditorComponent,
576
- DeleteFormFlowModalComponent], imports: [CommonModule,
1944
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.25", ngImport: i0, type: FormFlowManagementModule, declarations: [FormFlowOverviewComponent, FormFlowEditorComponent, DeleteFormFlowModalComponent], imports: [CommonModule,
577
1945
  ButtonModule,
578
1946
  FormsModule,
579
1947
  ModalModule,
@@ -593,7 +1961,10 @@ class FormFlowManagementModule {
593
1961
  CarbonListModule,
594
1962
  DropdownModule,
595
1963
  ConfirmationModalModule,
596
- NewFormFlowModalComponent] }); }
1964
+ NewFormFlowModalComponent,
1965
+ TabsModule,
1966
+ FormFlowJsonEditorTabComponent,
1967
+ FormFlowUiEditorTabComponent] }); }
597
1968
  static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowManagementModule, imports: [CommonModule,
598
1969
  ButtonModule,
599
1970
  FormsModule,
@@ -613,16 +1984,15 @@ class FormFlowManagementModule {
613
1984
  CarbonListModule,
614
1985
  DropdownModule,
615
1986
  ConfirmationModalModule,
616
- NewFormFlowModalComponent] }); }
1987
+ NewFormFlowModalComponent,
1988
+ TabsModule,
1989
+ FormFlowJsonEditorTabComponent,
1990
+ FormFlowUiEditorTabComponent] }); }
617
1991
  }
618
1992
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: FormFlowManagementModule, decorators: [{
619
1993
  type: NgModule,
620
1994
  args: [{
621
- declarations: [
622
- FormFlowOverviewComponent,
623
- FormFlowEditorComponent,
624
- DeleteFormFlowModalComponent,
625
- ],
1995
+ declarations: [FormFlowOverviewComponent, FormFlowEditorComponent, DeleteFormFlowModalComponent],
626
1996
  imports: [
627
1997
  CommonModule,
628
1998
  ButtonModule,
@@ -645,6 +2015,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
645
2015
  DropdownModule,
646
2016
  ConfirmationModalModule,
647
2017
  NewFormFlowModalComponent,
2018
+ TabsModule,
2019
+ FormFlowJsonEditorTabComponent,
2020
+ FormFlowUiEditorTabComponent,
648
2021
  ],
649
2022
  }]
650
2023
  }] });
@@ -672,5 +2045,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
672
2045
  * Generated bundle index. Do not edit.
673
2046
  */
674
2047
 
675
- export { FormFlowEditorComponent, FormFlowManagementModule, FormFlowOverviewComponent, FormFlowService, NewFormFlowModalComponent };
2048
+ export { FORM_FLOW_COMPONENT_TOKEN, FORM_FLOW_EDITOR_TEST_IDS, FormFlowComponentService, FormFlowEditorComponent, FormFlowEditorTab, FormFlowManagementModule, FormFlowOverviewComponent, FormFlowService, NewFormFlowModalComponent };
676
2049
  //# sourceMappingURL=valtimo-form-flow-management.mjs.map