@masterteam/form-builder 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, Injectable, computed, input, signal, viewChild, Component, effect, output } from '@angular/core';
|
|
3
|
-
import * as i5 from '@angular/common';
|
|
2
|
+
import { inject, Injectable, computed, input, signal, viewChild, Component, effect, output, DestroyRef, viewChildren } from '@angular/core';
|
|
4
3
|
import { CommonModule } from '@angular/common';
|
|
5
4
|
import * as i1 from '@angular/forms';
|
|
6
5
|
import { FormControl, ReactiveFormsModule, FormGroup, FormsModule } from '@angular/forms';
|
|
@@ -8,13 +7,16 @@ import * as i2 from 'primeng/tabs';
|
|
|
8
7
|
import { TabsModule } from 'primeng/tabs';
|
|
9
8
|
import * as i3 from 'primeng/skeleton';
|
|
10
9
|
import { SkeletonModule } from 'primeng/skeleton';
|
|
10
|
+
import * as i4 from 'primeng/popover';
|
|
11
|
+
import { Popover, PopoverModule } from 'primeng/popover';
|
|
11
12
|
import { Button } from '@masterteam/components/button';
|
|
12
13
|
import { Card } from '@masterteam/components/card';
|
|
13
14
|
import { TextField } from '@masterteam/components/text-field';
|
|
15
|
+
import { SelectField } from '@masterteam/components/select-field';
|
|
14
16
|
import { ModalService } from '@masterteam/components/modal';
|
|
15
17
|
import { ConfirmationService } from '@masterteam/components/confirmation';
|
|
16
18
|
import { TranslocoDirective, TranslocoService } from '@jsverse/transloco';
|
|
17
|
-
import * as
|
|
19
|
+
import * as i5 from '@angular/cdk/drag-drop';
|
|
18
20
|
import { CdkDrag, CdkDropList, CdkDragPlaceholder, DragDropModule } from '@angular/cdk/drag-drop';
|
|
19
21
|
import { DynamicField } from '@masterteam/forms/dynamic-field';
|
|
20
22
|
import { Icon } from '@masterteam/icons';
|
|
@@ -24,9 +26,13 @@ import { CrudStateBase, handleApiRequest, RadioCardsFieldConfig } from '@mastert
|
|
|
24
26
|
import { DynamicForm } from '@masterteam/forms/dynamic-form';
|
|
25
27
|
import { ToggleField } from '@masterteam/components/toggle-field';
|
|
26
28
|
import { ModalRef } from '@masterteam/components/dialog';
|
|
27
|
-
import { FormulaToolbar, FormulaEditor } from '@masterteam/components/formula';
|
|
29
|
+
import { FormulaToolbar, FormulaEditor, serializeTokens } from '@masterteam/components/formula';
|
|
28
30
|
import { Tooltip } from '@masterteam/components/tooltip';
|
|
29
31
|
import { Tabs } from '@masterteam/components/tabs';
|
|
32
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
33
|
+
import { Table } from '@masterteam/components/table';
|
|
34
|
+
import { RadioCards } from '@masterteam/components/radio-cards';
|
|
35
|
+
import { of, map, catchError } from 'rxjs';
|
|
30
36
|
|
|
31
37
|
// ============================================================================
|
|
32
38
|
// Module Configuration Actions
|
|
@@ -143,6 +149,41 @@ class MoveField {
|
|
|
143
149
|
this.payload = payload;
|
|
144
150
|
}
|
|
145
151
|
}
|
|
152
|
+
// ============================================================================
|
|
153
|
+
// Validation Actions
|
|
154
|
+
// ============================================================================
|
|
155
|
+
class AddValidation {
|
|
156
|
+
payload;
|
|
157
|
+
static type = '[FormBuilder] Add Validation';
|
|
158
|
+
constructor(payload) {
|
|
159
|
+
this.payload = payload;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
class UpdateValidation {
|
|
163
|
+
validationId;
|
|
164
|
+
payload;
|
|
165
|
+
static type = '[FormBuilder] Update Validation';
|
|
166
|
+
constructor(validationId, payload) {
|
|
167
|
+
this.validationId = validationId;
|
|
168
|
+
this.payload = payload;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
class DeleteValidation {
|
|
172
|
+
validationId;
|
|
173
|
+
static type = '[FormBuilder] Delete Validation';
|
|
174
|
+
constructor(validationId) {
|
|
175
|
+
this.validationId = validationId;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
class ToggleValidationActive {
|
|
179
|
+
validationId;
|
|
180
|
+
payload;
|
|
181
|
+
static type = '[FormBuilder] Toggle Validation Active';
|
|
182
|
+
constructor(validationId, payload) {
|
|
183
|
+
this.validationId = validationId;
|
|
184
|
+
this.payload = payload;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
146
187
|
|
|
147
188
|
// ============================================================================
|
|
148
189
|
// Action Keys Enum
|
|
@@ -162,6 +203,11 @@ var FormBuilderActionKey;
|
|
|
162
203
|
FormBuilderActionKey["DeleteField"] = "deleteField";
|
|
163
204
|
FormBuilderActionKey["MoveField"] = "moveField";
|
|
164
205
|
FormBuilderActionKey["ReorderFields"] = "reorderFields";
|
|
206
|
+
// Validation Rules
|
|
207
|
+
FormBuilderActionKey["AddValidation"] = "addValidation";
|
|
208
|
+
FormBuilderActionKey["UpdateValidation"] = "updateValidation";
|
|
209
|
+
FormBuilderActionKey["DeleteValidation"] = "deleteValidation";
|
|
210
|
+
FormBuilderActionKey["ToggleValidationActive"] = "toggleValidationActive";
|
|
165
211
|
})(FormBuilderActionKey || (FormBuilderActionKey = {}));
|
|
166
212
|
|
|
167
213
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
@@ -214,9 +260,21 @@ let FormBuilderState = class FormBuilderState extends CrudStateBase {
|
|
|
214
260
|
static getModuleId(state) {
|
|
215
261
|
return state?.moduleId ?? null;
|
|
216
262
|
}
|
|
263
|
+
static getParentModuleType(state) {
|
|
264
|
+
return state?.parentModuleType ?? null;
|
|
265
|
+
}
|
|
266
|
+
static getParentModuleId(state) {
|
|
267
|
+
return state?.parentModuleId ?? null;
|
|
268
|
+
}
|
|
269
|
+
static getParentPath(state) {
|
|
270
|
+
return state?.parentPath ?? '';
|
|
271
|
+
}
|
|
217
272
|
static getProperties(state) {
|
|
218
273
|
return state?.properties ?? [];
|
|
219
274
|
}
|
|
275
|
+
static getValidations(state) {
|
|
276
|
+
return state?.formConfiguration?.validations ?? [];
|
|
277
|
+
}
|
|
220
278
|
// ============================================================================
|
|
221
279
|
// Module Configuration Actions
|
|
222
280
|
// ============================================================================
|
|
@@ -255,7 +313,9 @@ let FormBuilderState = class FormBuilderState extends CrudStateBase {
|
|
|
255
313
|
key: FormBuilderActionKey.GetFormConfiguration,
|
|
256
314
|
request$: req$,
|
|
257
315
|
updateState: (_state, data) => ({
|
|
258
|
-
formConfiguration: data
|
|
316
|
+
formConfiguration: data
|
|
317
|
+
? { ...data, validations: data.validations ?? [] }
|
|
318
|
+
: null,
|
|
259
319
|
}),
|
|
260
320
|
});
|
|
261
321
|
}
|
|
@@ -268,7 +328,9 @@ let FormBuilderState = class FormBuilderState extends CrudStateBase {
|
|
|
268
328
|
key: FormBuilderActionKey.ResetFormConfiguration,
|
|
269
329
|
request$: req$,
|
|
270
330
|
onSuccess: (res, _currentState) => ({
|
|
271
|
-
formConfiguration: res.data
|
|
331
|
+
formConfiguration: res.data
|
|
332
|
+
? { ...res.data, validations: res.data.validations ?? [] }
|
|
333
|
+
: null,
|
|
272
334
|
}),
|
|
273
335
|
});
|
|
274
336
|
}
|
|
@@ -344,7 +406,7 @@ let FormBuilderState = class FormBuilderState extends CrudStateBase {
|
|
|
344
406
|
const tempField = {
|
|
345
407
|
id: tempId,
|
|
346
408
|
sectionId: action.sectionId,
|
|
347
|
-
|
|
409
|
+
propertyKey: action.payload.propertyKey,
|
|
348
410
|
width: action.payload.width,
|
|
349
411
|
order: action.payload.order ?? 0,
|
|
350
412
|
hiddenInCreation: action.payload.hiddenInCreation,
|
|
@@ -661,6 +723,88 @@ let FormBuilderState = class FormBuilderState extends CrudStateBase {
|
|
|
661
723
|
},
|
|
662
724
|
});
|
|
663
725
|
}
|
|
726
|
+
// ============================================================================
|
|
727
|
+
// Validation Actions
|
|
728
|
+
// ============================================================================
|
|
729
|
+
addValidation(ctx, action) {
|
|
730
|
+
const state = ctx.getState();
|
|
731
|
+
const apiPath = `${this.getApiPath(state)}/validations`;
|
|
732
|
+
const req$ = this.http.post(apiPath, action.payload);
|
|
733
|
+
return handleApiRequest({
|
|
734
|
+
ctx,
|
|
735
|
+
key: FormBuilderActionKey.AddValidation,
|
|
736
|
+
request$: req$,
|
|
737
|
+
onSuccess: (res, currentState) => {
|
|
738
|
+
const validations = [
|
|
739
|
+
...(currentState.formConfiguration?.validations ?? []),
|
|
740
|
+
res.data,
|
|
741
|
+
];
|
|
742
|
+
return {
|
|
743
|
+
formConfiguration: {
|
|
744
|
+
...currentState.formConfiguration,
|
|
745
|
+
validations,
|
|
746
|
+
},
|
|
747
|
+
};
|
|
748
|
+
},
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
updateValidation(ctx, action) {
|
|
752
|
+
const state = ctx.getState();
|
|
753
|
+
const apiPath = `${this.getApiPath(state)}/validations/${action.validationId}`;
|
|
754
|
+
const req$ = this.http.put(apiPath, action.payload);
|
|
755
|
+
return handleApiRequest({
|
|
756
|
+
ctx,
|
|
757
|
+
key: FormBuilderActionKey.UpdateValidation,
|
|
758
|
+
request$: req$,
|
|
759
|
+
onSuccess: (res, currentState) => {
|
|
760
|
+
const validations = (currentState.formConfiguration?.validations ?? []).map((rule) => (rule.id === res.data.id ? res.data : rule));
|
|
761
|
+
return {
|
|
762
|
+
formConfiguration: {
|
|
763
|
+
...currentState.formConfiguration,
|
|
764
|
+
validations,
|
|
765
|
+
},
|
|
766
|
+
};
|
|
767
|
+
},
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
deleteValidation(ctx, action) {
|
|
771
|
+
const state = ctx.getState();
|
|
772
|
+
const apiPath = `${this.getApiPath(state)}/validations/${action.validationId}`;
|
|
773
|
+
const req$ = this.http.delete(apiPath);
|
|
774
|
+
return handleApiRequest({
|
|
775
|
+
ctx,
|
|
776
|
+
key: FormBuilderActionKey.DeleteValidation,
|
|
777
|
+
request$: req$,
|
|
778
|
+
onSuccess: (res, currentState) => {
|
|
779
|
+
const validations = (currentState.formConfiguration?.validations ?? []).filter((rule) => rule.id !== res.data.id);
|
|
780
|
+
return {
|
|
781
|
+
formConfiguration: {
|
|
782
|
+
...currentState.formConfiguration,
|
|
783
|
+
validations,
|
|
784
|
+
},
|
|
785
|
+
};
|
|
786
|
+
},
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
toggleValidationActive(ctx, action) {
|
|
790
|
+
const state = ctx.getState();
|
|
791
|
+
const apiPath = `${this.getApiPath(state)}/validations/${action.validationId}/active`;
|
|
792
|
+
const req$ = this.http.patch(apiPath, action.payload);
|
|
793
|
+
return handleApiRequest({
|
|
794
|
+
ctx,
|
|
795
|
+
key: FormBuilderActionKey.ToggleValidationActive,
|
|
796
|
+
request$: req$,
|
|
797
|
+
onSuccess: (res, currentState) => {
|
|
798
|
+
const validations = (currentState.formConfiguration?.validations ?? []).map((rule) => (rule.id === res.data.id ? res.data : rule));
|
|
799
|
+
return {
|
|
800
|
+
formConfiguration: {
|
|
801
|
+
...currentState.formConfiguration,
|
|
802
|
+
validations,
|
|
803
|
+
},
|
|
804
|
+
};
|
|
805
|
+
},
|
|
806
|
+
});
|
|
807
|
+
}
|
|
664
808
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormBuilderState, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
|
|
665
809
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormBuilderState });
|
|
666
810
|
};
|
|
@@ -703,6 +847,18 @@ __decorate([
|
|
|
703
847
|
__decorate([
|
|
704
848
|
Action(MoveField)
|
|
705
849
|
], FormBuilderState.prototype, "moveField", null);
|
|
850
|
+
__decorate([
|
|
851
|
+
Action(AddValidation)
|
|
852
|
+
], FormBuilderState.prototype, "addValidation", null);
|
|
853
|
+
__decorate([
|
|
854
|
+
Action(UpdateValidation)
|
|
855
|
+
], FormBuilderState.prototype, "updateValidation", null);
|
|
856
|
+
__decorate([
|
|
857
|
+
Action(DeleteValidation)
|
|
858
|
+
], FormBuilderState.prototype, "deleteValidation", null);
|
|
859
|
+
__decorate([
|
|
860
|
+
Action(ToggleValidationActive)
|
|
861
|
+
], FormBuilderState.prototype, "toggleValidationActive", null);
|
|
706
862
|
__decorate([
|
|
707
863
|
Selector()
|
|
708
864
|
], FormBuilderState, "getState", null);
|
|
@@ -718,9 +874,21 @@ __decorate([
|
|
|
718
874
|
__decorate([
|
|
719
875
|
Selector()
|
|
720
876
|
], FormBuilderState, "getModuleId", null);
|
|
877
|
+
__decorate([
|
|
878
|
+
Selector()
|
|
879
|
+
], FormBuilderState, "getParentModuleType", null);
|
|
880
|
+
__decorate([
|
|
881
|
+
Selector()
|
|
882
|
+
], FormBuilderState, "getParentModuleId", null);
|
|
883
|
+
__decorate([
|
|
884
|
+
Selector()
|
|
885
|
+
], FormBuilderState, "getParentPath", null);
|
|
721
886
|
__decorate([
|
|
722
887
|
Selector()
|
|
723
888
|
], FormBuilderState, "getProperties", null);
|
|
889
|
+
__decorate([
|
|
890
|
+
Selector()
|
|
891
|
+
], FormBuilderState, "getValidations", null);
|
|
724
892
|
FormBuilderState = __decorate([
|
|
725
893
|
State({
|
|
726
894
|
name: 'formBuilder',
|
|
@@ -729,7 +897,7 @@ FormBuilderState = __decorate([
|
|
|
729
897
|
], FormBuilderState);
|
|
730
898
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormBuilderState, decorators: [{
|
|
731
899
|
type: Injectable
|
|
732
|
-
}], propDecorators: { setModuleInfo: [], resetState: [], setProperties: [], getFormConfiguration: [], resetFormConfiguration: [], addSection: [], updateSection: [], deleteSection: [], addField: [], updateField: [], deleteField: [], reorderFields: [], moveField: [] } });
|
|
900
|
+
}], propDecorators: { setModuleInfo: [], resetState: [], setProperties: [], getFormConfiguration: [], resetFormConfiguration: [], addSection: [], updateSection: [], deleteSection: [], addField: [], updateField: [], deleteField: [], reorderFields: [], moveField: [], addValidation: [], updateValidation: [], deleteValidation: [], toggleValidationActive: [] } });
|
|
733
901
|
|
|
734
902
|
class FormBuilderFacade {
|
|
735
903
|
store = inject(Store);
|
|
@@ -740,8 +908,12 @@ class FormBuilderFacade {
|
|
|
740
908
|
formConfiguration = select(FormBuilderState.getFormConfiguration);
|
|
741
909
|
sections = select(FormBuilderState.getSections);
|
|
742
910
|
properties = select(FormBuilderState.getProperties);
|
|
911
|
+
validations = select(FormBuilderState.getValidations);
|
|
743
912
|
moduleType = select(FormBuilderState.getModuleType);
|
|
744
913
|
moduleId = select(FormBuilderState.getModuleId);
|
|
914
|
+
parentModuleType = select(FormBuilderState.getParentModuleType);
|
|
915
|
+
parentModuleId = select(FormBuilderState.getParentModuleId);
|
|
916
|
+
parentPath = select(FormBuilderState.getParentPath);
|
|
745
917
|
// ============================================================================
|
|
746
918
|
// Loading Signals
|
|
747
919
|
// ============================================================================
|
|
@@ -754,6 +926,10 @@ class FormBuilderFacade {
|
|
|
754
926
|
isUpdatingField = computed(() => this.stateSignal().loadingActive.includes(FormBuilderActionKey.UpdateField), ...(ngDevMode ? [{ debugName: "isUpdatingField" }] : []));
|
|
755
927
|
isDeletingField = computed(() => this.stateSignal().loadingActive.includes(FormBuilderActionKey.DeleteField), ...(ngDevMode ? [{ debugName: "isDeletingField" }] : []));
|
|
756
928
|
isMovingField = computed(() => this.stateSignal().loadingActive.includes(FormBuilderActionKey.MoveField), ...(ngDevMode ? [{ debugName: "isMovingField" }] : []));
|
|
929
|
+
isAddingValidation = computed(() => this.stateSignal().loadingActive.includes(FormBuilderActionKey.AddValidation), ...(ngDevMode ? [{ debugName: "isAddingValidation" }] : []));
|
|
930
|
+
isUpdatingValidation = computed(() => this.stateSignal().loadingActive.includes(FormBuilderActionKey.UpdateValidation), ...(ngDevMode ? [{ debugName: "isUpdatingValidation" }] : []));
|
|
931
|
+
isDeletingValidation = computed(() => this.stateSignal().loadingActive.includes(FormBuilderActionKey.DeleteValidation), ...(ngDevMode ? [{ debugName: "isDeletingValidation" }] : []));
|
|
932
|
+
isTogglingValidation = computed(() => this.stateSignal().loadingActive.includes(FormBuilderActionKey.ToggleValidationActive), ...(ngDevMode ? [{ debugName: "isTogglingValidation" }] : []));
|
|
757
933
|
// ============================================================================
|
|
758
934
|
// Error Signals
|
|
759
935
|
// ============================================================================
|
|
@@ -774,6 +950,14 @@ class FormBuilderFacade {
|
|
|
774
950
|
errors[FormBuilderActionKey.MoveField] ??
|
|
775
951
|
null);
|
|
776
952
|
}, ...(ngDevMode ? [{ debugName: "fieldError" }] : []));
|
|
953
|
+
validationError = computed(() => {
|
|
954
|
+
const errors = this.stateSignal().errors;
|
|
955
|
+
return (errors[FormBuilderActionKey.AddValidation] ??
|
|
956
|
+
errors[FormBuilderActionKey.UpdateValidation] ??
|
|
957
|
+
errors[FormBuilderActionKey.DeleteValidation] ??
|
|
958
|
+
errors[FormBuilderActionKey.ToggleValidationActive] ??
|
|
959
|
+
null);
|
|
960
|
+
}, ...(ngDevMode ? [{ debugName: "validationError" }] : []));
|
|
777
961
|
// ============================================================================
|
|
778
962
|
// Module Configuration Dispatchers
|
|
779
963
|
// ============================================================================
|
|
@@ -825,6 +1009,21 @@ class FormBuilderFacade {
|
|
|
825
1009
|
moveField(sectionId, fieldId, payload) {
|
|
826
1010
|
return this.store.dispatch(new MoveField(sectionId, fieldId, payload));
|
|
827
1011
|
}
|
|
1012
|
+
// ============================================================================
|
|
1013
|
+
// Validation Dispatchers
|
|
1014
|
+
// ============================================================================
|
|
1015
|
+
addValidation(payload) {
|
|
1016
|
+
return this.store.dispatch(new AddValidation(payload));
|
|
1017
|
+
}
|
|
1018
|
+
updateValidation(validationId, payload) {
|
|
1019
|
+
return this.store.dispatch(new UpdateValidation(validationId, payload));
|
|
1020
|
+
}
|
|
1021
|
+
deleteValidation(validationId) {
|
|
1022
|
+
return this.store.dispatch(new DeleteValidation(validationId));
|
|
1023
|
+
}
|
|
1024
|
+
toggleValidationActive(validationId, payload) {
|
|
1025
|
+
return this.store.dispatch(new ToggleValidationActive(validationId, payload));
|
|
1026
|
+
}
|
|
828
1027
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormBuilderFacade, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
829
1028
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormBuilderFacade, providedIn: 'root' });
|
|
830
1029
|
}
|
|
@@ -1150,7 +1349,7 @@ class FBFieldConditions {
|
|
|
1150
1349
|
this.ref.close({ saved: false });
|
|
1151
1350
|
}
|
|
1152
1351
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FBFieldConditions, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1153
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.0.3", type: FBFieldConditions, isStandalone: true, selector: "mt-fb-field-conditions", inputs: { initialFormula: { classPropertyName: "initialFormula", publicName: "initialFormula", isSignal: true, isRequired: false, transformFunction: null }, availableFields: { classPropertyName: "availableFields", publicName: "availableFields", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "editorRef", first: true, predicate: ["editor"], descendants: true, isSignal: true }], ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'formBuilder'\">\r\n <div [class]=\"[modalService.contentClass, 'p-4', 'overflow-y-hidden!']\">\r\n <div class=\"mt-4 h-full overflow-y-auto pb-10 flex flex-col gap-3\">\r\n <!-- Formula Toolbar -->\r\n <mt-formula-toolbar\r\n [knownProperties]=\"propertyKeys()\"\r\n [functionCategories]=\"functionCategories\"\r\n [operators]=\"operators\"\r\n [labels]=\"toolbarLabels\"\r\n (onBlockInsert)=\"onBlockInsert($event)\"\r\n />\r\n <!-- Formula Editor -->\r\n <mt-formula-editor\r\n #editor\r\n [placeholder]=\"t('build-condition-formula')\"\r\n [formControl]=\"formulaControl\"\r\n />\r\n </div>\r\n </div>\r\n\r\n <div [class]=\"modalService.footerClass\">\r\n <mt-button\r\n [label]=\"t('cancel')\"\r\n severity=\"secondary\"\r\n [disabled]=\"submitting()\"\r\n (onClick)=\"onCancel()\"\r\n />\r\n <mt-button\r\n [label]=\"t('save')\"\r\n severity=\"primary\"\r\n [loading]=\"submitting()\"\r\n (onClick)=\"onSave()\"\r\n />\r\n </div>\r\n</ng-container>\r\n", dependencies: [{ kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: FormulaToolbar, selector: "mt-formula-toolbar", inputs: ["knownProperties", "functionCategories", "operators", "initialTab", "searchPlaceholder", "labels"], outputs: ["onBlockInsert", "onTabChange"] }, { kind: "component", type: FormulaEditor, selector: "mt-formula-editor", inputs: ["placeholder", "initialTokens", "disabled"], outputs: ["formulaChange", "tokensChange", "onBlur", "onFocus"] }] });
|
|
1352
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.0.3", type: FBFieldConditions, isStandalone: true, selector: "mt-fb-field-conditions", inputs: { initialFormula: { classPropertyName: "initialFormula", publicName: "initialFormula", isSignal: true, isRequired: false, transformFunction: null }, availableFields: { classPropertyName: "availableFields", publicName: "availableFields", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "editorRef", first: true, predicate: ["editor"], descendants: true, isSignal: true }], ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'formBuilder'\">\r\n <div [class]=\"[modalService.contentClass, 'p-4', 'overflow-y-hidden!']\">\r\n <div class=\"mt-4 h-full overflow-y-auto pb-10 flex flex-col gap-3\">\r\n <!-- Formula Toolbar -->\r\n <mt-formula-toolbar\r\n [knownProperties]=\"propertyKeys()\"\r\n [functionCategories]=\"functionCategories\"\r\n [operators]=\"operators\"\r\n [labels]=\"toolbarLabels\"\r\n (onBlockInsert)=\"onBlockInsert($event)\"\r\n />\r\n <!-- Formula Editor -->\r\n <mt-formula-editor\r\n #editor\r\n [placeholder]=\"t('build-condition-formula')\"\r\n [formControl]=\"formulaControl\"\r\n />\r\n </div>\r\n </div>\r\n\r\n <div [class]=\"modalService.footerClass\">\r\n <mt-button\r\n [label]=\"t('cancel')\"\r\n severity=\"secondary\"\r\n [disabled]=\"submitting()\"\r\n (onClick)=\"onCancel()\"\r\n />\r\n <mt-button\r\n [label]=\"t('save')\"\r\n severity=\"primary\"\r\n [loading]=\"submitting()\"\r\n (onClick)=\"onSave()\"\r\n />\r\n </div>\r\n</ng-container>\r\n", dependencies: [{ kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: FormulaToolbar, selector: "mt-formula-toolbar", inputs: ["knownProperties", "propertiesTemplate", "functionCategories", "operators", "initialTab", "searchPlaceholder", "labels"], outputs: ["onBlockInsert", "onTabChange"] }, { kind: "component", type: FormulaEditor, selector: "mt-formula-editor", inputs: ["placeholder", "initialTokens", "disabled", "borderless"], outputs: ["formulaChange", "tokensChange", "onBlur", "onFocus"] }] });
|
|
1154
1353
|
}
|
|
1155
1354
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FBFieldConditions, decorators: [{
|
|
1156
1355
|
type: Component,
|
|
@@ -1193,7 +1392,7 @@ class FBFieldForm {
|
|
|
1193
1392
|
if (currentField && field.id === currentField.id)
|
|
1194
1393
|
continue;
|
|
1195
1394
|
fields.push({
|
|
1196
|
-
key: field.
|
|
1395
|
+
key: field.propertyKey,
|
|
1197
1396
|
name: field.name,
|
|
1198
1397
|
type: field.type,
|
|
1199
1398
|
});
|
|
@@ -1678,7 +1877,7 @@ class FBPreviewForm {
|
|
|
1678
1877
|
fields: visibleFields.map((field, fieldIndex) => {
|
|
1679
1878
|
const colSpan = this.getColSpan(field.width);
|
|
1680
1879
|
return {
|
|
1681
|
-
key: `field_${field.
|
|
1880
|
+
key: `field_${field.propertyKey}`,
|
|
1682
1881
|
label: field.name,
|
|
1683
1882
|
type: this.mapFieldType(field.type),
|
|
1684
1883
|
colSpan,
|
|
@@ -1741,180 +1940,1247 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImpor
|
|
|
1741
1940
|
args: [{ selector: 'mt-fb-preview-form', standalone: true, imports: [TranslocoDirective, ReactiveFormsModule, DynamicForm, Tabs], template: "<ng-container *transloco=\"let t; prefix: 'formBuilder'\">\r\n <div [class]=\"[modalService.contentClass, 'p-4', 'overflow-y-hidden!']\">\r\n <!-- Tabs for Create/Edit mode -->\r\n <div class=\"flex justify-center mb-4\">\r\n <mt-tabs [options]=\"tabOptions\" [(active)]=\"activeTab\"></mt-tabs>\r\n </div>\r\n\r\n <div class=\"h-full overflow-y-auto px-3 pb-10\">\r\n @if (isLoading()) {\r\n <div class=\"space-y-4 animate-pulse\">\r\n <div class=\"h-6 bg-surface-200 rounded w-1/3\"></div>\r\n <div class=\"h-10 bg-surface-200 rounded\"></div>\r\n <div class=\"h-10 bg-surface-200 rounded\"></div>\r\n <div class=\"h-10 bg-surface-200 rounded w-2/3\"></div>\r\n </div>\r\n } @else if (formConfig().sections.length > 0) {\r\n <mt-dynamic-form\r\n [formConfig]=\"formConfig()\"\r\n [formControl]=\"formControl\"\r\n >\r\n </mt-dynamic-form>\r\n } @else {\r\n <div\r\n class=\"flex justify-center items-center h-64 text-muted-color text-lg\"\r\n >\r\n {{ t(\"no-fields-visible\") }}\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n</ng-container>\r\n" }]
|
|
1742
1941
|
}], ctorParameters: () => [], propDecorators: { sections: [{ type: i0.Input, args: [{ isSignal: true, alias: "sections", required: false }] }] } });
|
|
1743
1942
|
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
const sections = this.sections();
|
|
1769
|
-
const propsMap = this.propertiesMap();
|
|
1770
|
-
const lang = document.documentElement.lang;
|
|
1771
|
-
return sections.map((section) => ({
|
|
1772
|
-
...section,
|
|
1773
|
-
fields: section.fields.map((field) => {
|
|
1774
|
-
const prop = propsMap.get(field.propertyId);
|
|
1775
|
-
const propName = prop?.name;
|
|
1776
|
-
const name = typeof propName === 'string'
|
|
1777
|
-
? propName
|
|
1778
|
-
: (propName?.[lang] ??
|
|
1779
|
-
propName?.['en'] ??
|
|
1780
|
-
`Property ${field.propertyId}`);
|
|
1781
|
-
return {
|
|
1782
|
-
...field,
|
|
1783
|
-
name,
|
|
1784
|
-
type: prop?.viewType || 'text',
|
|
1785
|
-
data: prop,
|
|
1786
|
-
};
|
|
1787
|
-
}),
|
|
1788
|
-
}));
|
|
1789
|
-
}, ...(ngDevMode ? [{ debugName: "enrichedSections" }] : []));
|
|
1790
|
-
// Available properties for toolbox tabs
|
|
1791
|
-
availableTabs = computed(() => {
|
|
1792
|
-
const tabs = [];
|
|
1793
|
-
const usedPropertyIds = new Set(this.sections().flatMap((s) => s.fields.map((f) => f.propertyId)));
|
|
1794
|
-
const availableProps = this.properties().filter((p) => !usedPropertyIds.has(p.id));
|
|
1795
|
-
const systemProps = availableProps.filter((p) => p['isSystem']);
|
|
1796
|
-
const customProps = availableProps.filter((p) => !p['isSystem']);
|
|
1797
|
-
if (systemProps.length > 0) {
|
|
1798
|
-
tabs.push({ id: 'system', title: 'System', properties: systemProps });
|
|
1799
|
-
}
|
|
1800
|
-
if (customProps.length > 0) {
|
|
1801
|
-
tabs.push({ id: 'custom', title: 'Custom', properties: customProps });
|
|
1802
|
-
}
|
|
1803
|
-
return tabs;
|
|
1804
|
-
}, ...(ngDevMode ? [{ debugName: "availableTabs" }] : []));
|
|
1805
|
-
// Filtered properties based on search query
|
|
1806
|
-
filteredPropertiesByTab = computed(() => {
|
|
1807
|
-
const query = this.searchQuery().toLowerCase().trim();
|
|
1808
|
-
const tabs = this.availableTabs();
|
|
1809
|
-
if (!query) {
|
|
1810
|
-
return tabs;
|
|
1811
|
-
}
|
|
1812
|
-
return tabs.map((tab) => ({
|
|
1813
|
-
...tab,
|
|
1814
|
-
properties: tab.properties.filter((prop) => {
|
|
1815
|
-
const name = prop.name;
|
|
1816
|
-
return name.toLowerCase().includes(query);
|
|
1817
|
-
}),
|
|
1818
|
-
}));
|
|
1819
|
-
}, ...(ngDevMode ? [{ debugName: "filteredPropertiesByTab" }] : []));
|
|
1820
|
-
constructor() {
|
|
1821
|
-
// Update active tab when tabs change
|
|
1822
|
-
effect(() => {
|
|
1823
|
-
const tabs = this.availableTabs();
|
|
1824
|
-
const currentTab = this.activeTab();
|
|
1825
|
-
if (tabs.length > 0 && !tabs.some((t) => t.id === currentTab)) {
|
|
1826
|
-
this.activeTab.set(tabs[0].id);
|
|
1827
|
-
}
|
|
1828
|
-
});
|
|
1829
|
-
}
|
|
1830
|
-
drop(event) {
|
|
1831
|
-
const targetSectionId = event.container.id;
|
|
1832
|
-
if (event.previousContainer === event.container) {
|
|
1833
|
-
// Reordering within the same section
|
|
1834
|
-
const fields = [...event.container.data];
|
|
1835
|
-
const [movedField] = fields.splice(event.previousIndex, 1);
|
|
1836
|
-
fields.splice(event.currentIndex, 0, movedField);
|
|
1837
|
-
// Build bulk reorder payload with new order values
|
|
1838
|
-
const reorderPayload = fields.map((field, index) => ({
|
|
1839
|
-
id: field.id,
|
|
1840
|
-
order: index,
|
|
1841
|
-
}));
|
|
1842
|
-
this.facade.reorderFields(targetSectionId, reorderPayload);
|
|
1843
|
-
}
|
|
1844
|
-
else if (event.previousContainer.id.startsWith('toolbox-')) {
|
|
1845
|
-
// Adding from toolbox
|
|
1846
|
-
const propertyItem = event.item.data;
|
|
1847
|
-
this.facade.addField(targetSectionId, {
|
|
1848
|
-
propertyId: propertyItem.id,
|
|
1849
|
-
width: '100',
|
|
1850
|
-
order: event.currentIndex,
|
|
1851
|
-
hiddenInCreation: false,
|
|
1852
|
-
});
|
|
1853
|
-
}
|
|
1854
|
-
else {
|
|
1855
|
-
// Moving between sections
|
|
1856
|
-
const sourceSectionId = event.previousContainer.id;
|
|
1857
|
-
const field = event.item.data;
|
|
1858
|
-
this.facade.moveField(sourceSectionId, field.id, {
|
|
1859
|
-
targetSectionId,
|
|
1860
|
-
order: event.currentIndex,
|
|
1861
|
-
});
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
addSection() {
|
|
1865
|
-
this.dialogRef = this.modalService.openModal(FBSectionForm, 'drawer', {
|
|
1866
|
-
header: this.translocoService.translate('formBuilder.add-section'),
|
|
1867
|
-
height: '20vw',
|
|
1868
|
-
styleClass: '!w-[27%] !absolute !shadow-none ',
|
|
1869
|
-
position: 'end',
|
|
1870
|
-
appendTo: '#page-content',
|
|
1871
|
-
dismissible: true,
|
|
1872
|
-
inputValues: {
|
|
1873
|
-
sectionsCount: this.sections().length,
|
|
1943
|
+
/**
|
|
1944
|
+
* Validation Formula Constants
|
|
1945
|
+
* Functions and operators for validation formulas
|
|
1946
|
+
*/
|
|
1947
|
+
const VALIDATION_FUNCTION_CATEGORIES = [
|
|
1948
|
+
{
|
|
1949
|
+
name: 'Validation',
|
|
1950
|
+
displayName: 'Validation',
|
|
1951
|
+
functions: [
|
|
1952
|
+
{
|
|
1953
|
+
name: 'REQUIRED',
|
|
1954
|
+
category: 'Validation',
|
|
1955
|
+
description: 'Returns true when value is not empty',
|
|
1956
|
+
signature: 'REQUIRED(value)',
|
|
1957
|
+
parameters: [
|
|
1958
|
+
{
|
|
1959
|
+
name: 'value',
|
|
1960
|
+
type: 'any',
|
|
1961
|
+
description: 'Value to check',
|
|
1962
|
+
required: true,
|
|
1963
|
+
},
|
|
1964
|
+
],
|
|
1965
|
+
returnType: 'boolean',
|
|
1966
|
+
examples: ['REQUIRED(@Email)'],
|
|
1874
1967
|
},
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1968
|
+
{
|
|
1969
|
+
name: 'ISNULL',
|
|
1970
|
+
category: 'Validation',
|
|
1971
|
+
description: 'Returns true if value is null or empty',
|
|
1972
|
+
signature: 'ISNULL(value)',
|
|
1973
|
+
parameters: [
|
|
1974
|
+
{
|
|
1975
|
+
name: 'value',
|
|
1976
|
+
type: 'any',
|
|
1977
|
+
description: 'Value to check',
|
|
1978
|
+
required: true,
|
|
1979
|
+
},
|
|
1980
|
+
],
|
|
1981
|
+
returnType: 'boolean',
|
|
1982
|
+
examples: ['ISNULL(@Phone)'],
|
|
1886
1983
|
},
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1984
|
+
{
|
|
1985
|
+
name: 'MIN_LENGTH',
|
|
1986
|
+
category: 'Validation',
|
|
1987
|
+
description: 'Returns true if value length is at least min',
|
|
1988
|
+
signature: 'MIN_LENGTH(value, min)',
|
|
1989
|
+
parameters: [
|
|
1990
|
+
{
|
|
1991
|
+
name: 'value',
|
|
1992
|
+
type: 'string',
|
|
1993
|
+
description: 'Value to check',
|
|
1994
|
+
required: true,
|
|
1995
|
+
},
|
|
1996
|
+
{
|
|
1997
|
+
name: 'min',
|
|
1998
|
+
type: 'number',
|
|
1999
|
+
description: 'Minimum length',
|
|
2000
|
+
required: true,
|
|
2001
|
+
},
|
|
2002
|
+
],
|
|
2003
|
+
returnType: 'boolean',
|
|
2004
|
+
examples: ['MIN_LENGTH(@Password, 8)'],
|
|
1895
2005
|
},
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.3", type: FormBuilder, isStandalone: true, selector: "mt-form-builder", ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'formBuilder'\">\r\n <div class=\"flex gap-7 h-full w-full overflow-hidden\" cdkDropListGroup>\r\n <!-- Properties Sidebar -->\r\n <mt-card class=\"w-1/5 h-full flex flex-col overflow-hidden\">\r\n <ng-template #headless>\r\n <!-- Header -->\r\n <h3 class=\"text-xl font-semibold px-4 pt-5\">\r\n {{ t(\"form-elements\") }}\r\n </h3>\r\n\r\n @if (properties().length === 0) {\r\n @if (isLoading()) {\r\n <!-- Properties Loading Skeleton -->\r\n <div class=\"flex gap-4 py-3 px-5 mt-4\">\r\n <p-skeleton height=\"2rem\" styleClass=\"rounded-lg\" />\r\n <p-skeleton height=\"2rem\" styleClass=\"rounded-lg\" />\r\n </div>\r\n <div class=\"py-4 px-5 space-y-5\">\r\n @for (i of [1, 2, 3, 4, 5, 6]; track i) {\r\n <p-skeleton height=\"3rem\" styleClass=\"rounded-lg\" />\r\n }\r\n </div>\r\n } @else {\r\n <!-- No Properties State -->\r\n <div class=\"flex-1 flex items-center justify-center p-4\">\r\n <p class=\"text-sm font-semibold text-gray-500\">\r\n {{ t(\"no-data-found\") }}\r\n </p>\r\n </div>\r\n }\r\n } @else {\r\n <!-- Tabs using PrimeNG -->\r\n <p-tabs\r\n [(value)]=\"activeTab\"\r\n styleClass=\"structure-tabs\"\r\n class=\"flex flex-1 flex-col min-h-0\"\r\n >\r\n <p-tablist class=\"shrink-0\">\r\n @for (tab of availableTabs(); track tab.id) {\r\n <p-tab [value]=\"tab.id\">{{ tab.title | titlecase }}</p-tab>\r\n }\r\n </p-tablist>\r\n <p-tabpanels\r\n class=\"!bg-transparent !p-0 !pb-3 flex-1 overflow-hidden\"\r\n >\r\n @for (tab of filteredPropertiesByTab(); track tab.id) {\r\n <p-tabpanel [value]=\"tab.id\" class=\"h-full flex flex-col\">\r\n <!-- Node List -->\r\n <div\r\n class=\"space-y-4 px-4 pb-4 [&_.cdk-drag-placeholder]:hidden flex-1 overflow-y-auto\"\r\n [id]=\"'toolbox-' + tab.id\"\r\n cdkDropList\r\n cdkDropListSortingDisabled\r\n [cdkDropListData]=\"tab.properties\"\r\n [cdkDropListEnterPredicate]=\"noReturnPredicate\"\r\n >\r\n <!-- Search Field (Sticky) -->\r\n <div class=\"sticky top-0 bg-surface-0 mb-0 py-3 pb-2 mb-1\">\r\n <mt-text-field\r\n [placeholder]=\"t('search-properties')\"\r\n [(ngModel)]=\"searchQuery\"\r\n icon=\"general.search-lg\"\r\n />\r\n </div>\r\n @for (node of tab.properties; track $index) {\r\n <div\r\n cdkDrag\r\n [cdkDragData]=\"node\"\r\n class=\"group cursor-move select-none flex items-center gap-3 py-3 px-3 rounded-lg border border-dashed border-surface-300 hover:bg-emphasis dark:border-surface-500 transition-colors\"\r\n >\r\n <div\r\n *cdkDragPlaceholder\r\n class=\"col-span-12 min-h-27 w-full rounded-2xl bg-black/10 z-1\"\r\n ></div>\r\n <span class=\"flex-1 text-base font-medium\">{{\r\n node.name\r\n }}</span>\r\n\r\n <mt-icon\r\n class=\"text-lg\"\r\n icon=\"general.menu-05\"\r\n ></mt-icon>\r\n </div>\r\n }\r\n\r\n @if (tab.properties.length === 0) {\r\n <div class=\"py-8 text-center text-muted-color\">\r\n <p class=\"text-sm\">\r\n @if (searchQuery()) {\r\n {{ t(\"no-data-found\") }}\r\n } @else {\r\n All {{ tab.title }} items are in use\r\n }\r\n </p>\r\n </div>\r\n }\r\n </div>\r\n </p-tabpanel>\r\n }\r\n </p-tabpanels>\r\n </p-tabs>\r\n }\r\n </ng-template>\r\n </mt-card>\r\n\r\n <!-- Main Canvas Area -->\r\n <div class=\"flex flex-1 gap-4 h-full overflow-y-auto\">\r\n <div class=\"flex flex-col w-2/3 gap-4 h-full\">\r\n <mt-card>\r\n <ng-template #headless>\r\n <div class=\"p-4 flex items-center gap-3\">\r\n <mt-button\r\n icon=\"layout.layout-top\"\r\n [label]=\"t('add-section')\"\r\n variant=\"outlined\"\r\n severity=\"primary\"\r\n (onClick)=\"addSection()\"\r\n [disabled]=\"isLoading()\"\r\n ></mt-button>\r\n <mt-button\r\n icon=\"general.eye\"\r\n [label]=\"t('preview')\"\r\n variant=\"outlined\"\r\n severity=\"primary\"\r\n (onClick)=\"openPreview()\"\r\n [disabled]=\"isLoading()\"\r\n ></mt-button>\r\n <mt-button\r\n icon=\"finance.credit-card-plus\"\r\n [label]=\"t('reset')\"\r\n variant=\"outlined\"\r\n severity=\"primary\"\r\n (onClick)=\"resetFormConfiguration()\"\r\n [disabled]=\"isLoading()\"\r\n ></mt-button>\r\n </div>\r\n </ng-template>\r\n </mt-card>\r\n\r\n @if (isLoading()) {\r\n <!-- Form Loading Skeleton -->\r\n @for (i of [1, 2]; track i) {\r\n <mt-card>\r\n <ng-template #headless>\r\n <div class=\"p-4 space-y-4\">\r\n <!-- Section header skeleton -->\r\n <div class=\"flex items-center justify-between\">\r\n <p-skeleton width=\"10rem\" height=\"1.5rem\" />\r\n <div class=\"flex gap-2\">\r\n <p-skeleton width=\"2rem\" height=\"2rem\" shape=\"circle\" />\r\n <p-skeleton width=\"2rem\" height=\"2rem\" shape=\"circle\" />\r\n </div>\r\n </div>\r\n <!-- Fields skeleton -->\r\n <div class=\"grid grid-cols-12 gap-4\">\r\n <div class=\"col-span-6\">\r\n <p-skeleton height=\"4rem\" styleClass=\"rounded-lg\" />\r\n </div>\r\n <div class=\"col-span-6\">\r\n <p-skeleton height=\"4rem\" styleClass=\"rounded-lg\" />\r\n </div>\r\n <div class=\"col-span-12\">\r\n <p-skeleton height=\"4rem\" styleClass=\"rounded-lg\" />\r\n </div>\r\n </div>\r\n </div>\r\n </ng-template>\r\n </mt-card>\r\n }\r\n } @else {\r\n @for (section of enrichedSections(); track section.id) {\r\n <mt-fb-section\r\n [section]=\"section\"\r\n [sectionsCount]=\"enrichedSections().length\"\r\n [allSections]=\"enrichedSections()\"\r\n (onFieldDrop)=\"drop($event)\"\r\n >\r\n </mt-fb-section>\r\n } @empty {\r\n <mt-card>\r\n <div class=\"h-27 p-4\">\r\n <div\r\n class=\"flex justify-center items-center gap-4 h-full border-1 border-primary rounded-xl bg-primary-50 text-primary\"\r\n >\r\n <span>{{ t(\"no-section\") }}</span>\r\n </div>\r\n </div>\r\n </mt-card>\r\n }\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n</ng-container>\r\n", styles: [".cdk-drag{cursor:grab}.cdk-drag:active,.cdk-drag-preview{cursor:grabbing}.cdk-drag-placeholder{opacity:.5}.cdk-drop-list-dragging .cdk-drag{cursor:grabbing}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TabsModule }, { kind: "component", type: i2.Tabs, selector: "p-tabs", inputs: ["value", "scrollable", "lazy", "selectOnFocus", "showNavigators", "tabindex"], outputs: ["valueChange"] }, { kind: "component", type: i2.TabPanels, selector: "p-tabpanels" }, { kind: "component", type: i2.TabPanel, selector: "p-tabpanel", inputs: ["lazy", "value"], outputs: ["valueChange"] }, { kind: "component", type: i2.TabList, selector: "p-tablist" }, { kind: "component", type: i2.Tab, selector: "p-tab", inputs: ["value", "disabled"], outputs: ["valueChange"] }, { kind: "ngmodule", type: SkeletonModule }, { kind: "component", type: i3.Skeleton, selector: "p-skeleton", inputs: ["styleClass", "shape", "animation", "borderRadius", "size", "width", "height"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: Card, selector: "mt-card", inputs: ["class", "title", "paddingless"] }, { kind: "component", type: TextField, selector: "mt-text-field", inputs: ["field", "hint", "label", "placeholder", "class", "type", "readonly", "pInputs", "required", "icon", "iconPosition"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: FBSection, selector: "mt-fb-section", inputs: ["section", "sectionsCount", "allSections"], outputs: ["onFieldDrop"] }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i4.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i4.CdkDropListGroup, selector: "[cdkDropListGroup]", inputs: ["cdkDropListGroupDisabled"], exportAs: ["cdkDropListGroup"] }, { kind: "directive", type: i4.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i4.CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "component", type: Icon, selector: "mt-icon", inputs: ["icon"] }, { kind: "pipe", type: i5.TitleCasePipe, name: "titlecase" }] });
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
2006
|
+
{
|
|
2007
|
+
name: 'MAX_LENGTH',
|
|
2008
|
+
category: 'Validation',
|
|
2009
|
+
description: 'Returns true if value length is at most max',
|
|
2010
|
+
signature: 'MAX_LENGTH(value, max)',
|
|
2011
|
+
parameters: [
|
|
2012
|
+
{
|
|
2013
|
+
name: 'value',
|
|
2014
|
+
type: 'string',
|
|
2015
|
+
description: 'Value to check',
|
|
2016
|
+
required: true,
|
|
2017
|
+
},
|
|
2018
|
+
{
|
|
2019
|
+
name: 'max',
|
|
2020
|
+
type: 'number',
|
|
2021
|
+
description: 'Maximum length',
|
|
2022
|
+
required: true,
|
|
2023
|
+
},
|
|
2024
|
+
],
|
|
2025
|
+
returnType: 'boolean',
|
|
2026
|
+
examples: ['MAX_LENGTH(@Title, 120)'],
|
|
2027
|
+
},
|
|
2028
|
+
{
|
|
2029
|
+
name: 'REGEX',
|
|
2030
|
+
category: 'Validation',
|
|
2031
|
+
description: 'Returns true if value matches regex pattern',
|
|
2032
|
+
signature: 'REGEX(value, pattern)',
|
|
2033
|
+
parameters: [
|
|
2034
|
+
{
|
|
2035
|
+
name: 'value',
|
|
2036
|
+
type: 'string',
|
|
2037
|
+
description: 'Value to check',
|
|
2038
|
+
required: true,
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
name: 'pattern',
|
|
2042
|
+
type: 'string',
|
|
2043
|
+
description: 'Regex pattern',
|
|
2044
|
+
required: true,
|
|
2045
|
+
},
|
|
2046
|
+
],
|
|
2047
|
+
returnType: 'boolean',
|
|
2048
|
+
examples: ['REGEX(@Email, "^[^@]+@[^@]+\\.[^@]+$")'],
|
|
2049
|
+
},
|
|
2050
|
+
{
|
|
2051
|
+
name: 'BEFORE',
|
|
2052
|
+
category: 'Validation',
|
|
2053
|
+
description: 'Returns true if date is before reference date',
|
|
2054
|
+
signature: 'BEFORE(date, reference)',
|
|
2055
|
+
parameters: [
|
|
2056
|
+
{
|
|
2057
|
+
name: 'date',
|
|
2058
|
+
type: 'date',
|
|
2059
|
+
description: 'Date to check',
|
|
2060
|
+
required: true,
|
|
2061
|
+
},
|
|
2062
|
+
{
|
|
2063
|
+
name: 'reference',
|
|
2064
|
+
type: 'date',
|
|
2065
|
+
description: 'Reference date',
|
|
2066
|
+
required: true,
|
|
2067
|
+
},
|
|
2068
|
+
],
|
|
2069
|
+
returnType: 'boolean',
|
|
2070
|
+
examples: ['BEFORE(@EndDate, @StartDate)'],
|
|
2071
|
+
},
|
|
2072
|
+
{
|
|
2073
|
+
name: 'AFTER',
|
|
2074
|
+
category: 'Validation',
|
|
2075
|
+
description: 'Returns true if date is after reference date',
|
|
2076
|
+
signature: 'AFTER(date, reference)',
|
|
2077
|
+
parameters: [
|
|
2078
|
+
{
|
|
2079
|
+
name: 'date',
|
|
2080
|
+
type: 'date',
|
|
2081
|
+
description: 'Date to check',
|
|
2082
|
+
required: true,
|
|
2083
|
+
},
|
|
2084
|
+
{
|
|
2085
|
+
name: 'reference',
|
|
2086
|
+
type: 'date',
|
|
2087
|
+
description: 'Reference date',
|
|
2088
|
+
required: true,
|
|
2089
|
+
},
|
|
2090
|
+
],
|
|
2091
|
+
returnType: 'boolean',
|
|
2092
|
+
examples: ['AFTER(@StartDate, @EndDate)'],
|
|
2093
|
+
},
|
|
2094
|
+
],
|
|
2095
|
+
},
|
|
2096
|
+
{
|
|
2097
|
+
name: 'Logic',
|
|
2098
|
+
displayName: 'Logic Helpers',
|
|
2099
|
+
functions: [
|
|
2100
|
+
{
|
|
2101
|
+
name: 'AND',
|
|
2102
|
+
category: 'Logic',
|
|
2103
|
+
description: 'Returns true if all conditions are true',
|
|
2104
|
+
signature: 'AND(condition1, condition2)',
|
|
2105
|
+
parameters: [
|
|
2106
|
+
{
|
|
2107
|
+
name: 'condition1',
|
|
2108
|
+
type: 'boolean',
|
|
2109
|
+
description: 'First condition',
|
|
2110
|
+
required: true,
|
|
2111
|
+
},
|
|
2112
|
+
{
|
|
2113
|
+
name: 'condition2',
|
|
2114
|
+
type: 'boolean',
|
|
2115
|
+
description: 'Second condition',
|
|
2116
|
+
required: true,
|
|
2117
|
+
},
|
|
2118
|
+
],
|
|
2119
|
+
returnType: 'boolean',
|
|
2120
|
+
examples: ['AND(REQUIRED(@Email), REGEX(@Email, "^.+@.+$"))'],
|
|
2121
|
+
},
|
|
2122
|
+
{
|
|
2123
|
+
name: 'OR',
|
|
2124
|
+
category: 'Logic',
|
|
2125
|
+
description: 'Returns true if any condition is true',
|
|
2126
|
+
signature: 'OR(condition1, condition2)',
|
|
2127
|
+
parameters: [
|
|
2128
|
+
{
|
|
2129
|
+
name: 'condition1',
|
|
2130
|
+
type: 'boolean',
|
|
2131
|
+
description: 'First condition',
|
|
2132
|
+
required: true,
|
|
2133
|
+
},
|
|
2134
|
+
{
|
|
2135
|
+
name: 'condition2',
|
|
2136
|
+
type: 'boolean',
|
|
2137
|
+
description: 'Second condition',
|
|
2138
|
+
required: true,
|
|
2139
|
+
},
|
|
2140
|
+
],
|
|
2141
|
+
returnType: 'boolean',
|
|
2142
|
+
examples: ['OR(ISNULL(@Phone), REGEX(@Phone, "^\\+?[0-9]+$"))'],
|
|
2143
|
+
},
|
|
2144
|
+
{
|
|
2145
|
+
name: 'NOT',
|
|
2146
|
+
category: 'Logic',
|
|
2147
|
+
description: 'Returns the opposite of a condition',
|
|
2148
|
+
signature: 'NOT(condition)',
|
|
2149
|
+
parameters: [
|
|
2150
|
+
{
|
|
2151
|
+
name: 'condition',
|
|
2152
|
+
type: 'boolean',
|
|
2153
|
+
description: 'Condition to negate',
|
|
2154
|
+
required: true,
|
|
2155
|
+
},
|
|
2156
|
+
],
|
|
2157
|
+
returnType: 'boolean',
|
|
2158
|
+
examples: ['NOT(ISNULL(@Password))'],
|
|
2159
|
+
},
|
|
2160
|
+
],
|
|
2161
|
+
},
|
|
2162
|
+
];
|
|
2163
|
+
const VALIDATION_OPERATORS = [
|
|
2164
|
+
// Arithmetic
|
|
2165
|
+
{
|
|
2166
|
+
symbol: '+',
|
|
2167
|
+
name: 'Add',
|
|
2168
|
+
type: 'arithmetic',
|
|
2169
|
+
description: 'Addition',
|
|
2170
|
+
precedence: 4,
|
|
2171
|
+
},
|
|
2172
|
+
{
|
|
2173
|
+
symbol: '-',
|
|
2174
|
+
name: 'Subtract',
|
|
2175
|
+
type: 'arithmetic',
|
|
2176
|
+
description: 'Subtraction',
|
|
2177
|
+
precedence: 4,
|
|
2178
|
+
},
|
|
2179
|
+
{
|
|
2180
|
+
symbol: '*',
|
|
2181
|
+
name: 'Multiply',
|
|
2182
|
+
type: 'arithmetic',
|
|
2183
|
+
description: 'Multiplication',
|
|
2184
|
+
precedence: 5,
|
|
2185
|
+
},
|
|
2186
|
+
{
|
|
2187
|
+
symbol: '/',
|
|
2188
|
+
name: 'Divide',
|
|
2189
|
+
type: 'arithmetic',
|
|
2190
|
+
description: 'Division',
|
|
2191
|
+
precedence: 5,
|
|
2192
|
+
},
|
|
2193
|
+
// Comparison
|
|
2194
|
+
{
|
|
2195
|
+
symbol: '==',
|
|
2196
|
+
name: 'Equal',
|
|
2197
|
+
type: 'comparison',
|
|
2198
|
+
description: 'Equal to',
|
|
2199
|
+
precedence: 3,
|
|
2200
|
+
},
|
|
2201
|
+
{
|
|
2202
|
+
symbol: '!=',
|
|
2203
|
+
name: 'Not Equal',
|
|
2204
|
+
type: 'comparison',
|
|
2205
|
+
description: 'Not equal to',
|
|
2206
|
+
precedence: 3,
|
|
2207
|
+
},
|
|
2208
|
+
{
|
|
2209
|
+
symbol: '>',
|
|
2210
|
+
name: 'Greater Than',
|
|
2211
|
+
type: 'comparison',
|
|
2212
|
+
description: 'Greater than',
|
|
2213
|
+
precedence: 3,
|
|
2214
|
+
},
|
|
2215
|
+
{
|
|
2216
|
+
symbol: '<',
|
|
2217
|
+
name: 'Less Than',
|
|
2218
|
+
type: 'comparison',
|
|
2219
|
+
description: 'Less than',
|
|
2220
|
+
precedence: 3,
|
|
2221
|
+
},
|
|
2222
|
+
{
|
|
2223
|
+
symbol: '>=',
|
|
2224
|
+
name: 'Greater or Equal',
|
|
2225
|
+
type: 'comparison',
|
|
2226
|
+
description: 'Greater than or equal',
|
|
2227
|
+
precedence: 3,
|
|
2228
|
+
},
|
|
2229
|
+
{
|
|
2230
|
+
symbol: '<=',
|
|
2231
|
+
name: 'Less or Equal',
|
|
2232
|
+
type: 'comparison',
|
|
2233
|
+
description: 'Less than or equal',
|
|
2234
|
+
precedence: 3,
|
|
2235
|
+
},
|
|
2236
|
+
// Logical
|
|
2237
|
+
{
|
|
2238
|
+
symbol: '&&',
|
|
2239
|
+
name: 'And',
|
|
2240
|
+
type: 'logical',
|
|
2241
|
+
description: 'Logical AND',
|
|
2242
|
+
precedence: 2,
|
|
2243
|
+
},
|
|
2244
|
+
{
|
|
2245
|
+
symbol: '||',
|
|
2246
|
+
name: 'Or',
|
|
2247
|
+
type: 'logical',
|
|
2248
|
+
description: 'Logical OR',
|
|
2249
|
+
precedence: 1,
|
|
2250
|
+
},
|
|
2251
|
+
];
|
|
2252
|
+
|
|
2253
|
+
class FBValidationRuleForm {
|
|
2254
|
+
modalService = inject(ModalService);
|
|
2255
|
+
ref = inject(ModalRef);
|
|
2256
|
+
facade = inject(FormBuilderFacade);
|
|
2257
|
+
transloco = inject(TranslocoService);
|
|
2258
|
+
destroyRef = inject(DestroyRef);
|
|
2259
|
+
initialData = input(null, ...(ngDevMode ? [{ debugName: "initialData" }] : []));
|
|
2260
|
+
availableFields = input([], ...(ngDevMode ? [{ debugName: "availableFields" }] : []));
|
|
2261
|
+
submitting = signal(false, ...(ngDevMode ? [{ debugName: "submitting" }] : []));
|
|
2262
|
+
activeLang = signal(this.transloco.getActiveLang() ?? 'en', ...(ngDevMode ? [{ debugName: "activeLang" }] : []));
|
|
2263
|
+
messageEnControl = new FormControl('');
|
|
2264
|
+
messageArControl = new FormControl('');
|
|
2265
|
+
severityControl = new FormControl('error', {
|
|
2266
|
+
nonNullable: true,
|
|
2267
|
+
});
|
|
2268
|
+
formulaControl = new FormControl([]);
|
|
2269
|
+
editorRef = viewChild('editor', ...(ngDevMode ? [{ debugName: "editorRef" }] : []));
|
|
2270
|
+
functionCategories = VALIDATION_FUNCTION_CATEGORIES;
|
|
2271
|
+
operators = VALIDATION_OPERATORS;
|
|
2272
|
+
propertyKeys = computed(() => this.availableFields().map((field) => field.key), ...(ngDevMode ? [{ debugName: "propertyKeys" }] : []));
|
|
2273
|
+
toolbarLabels = {
|
|
2274
|
+
functions: 'Functions',
|
|
2275
|
+
properties: 'Fields',
|
|
2276
|
+
operators: 'Operators',
|
|
2277
|
+
noPropertiesAvailable: 'No fields available',
|
|
2278
|
+
};
|
|
2279
|
+
severityOptions = computed(() => {
|
|
2280
|
+
const _lang = this.activeLang();
|
|
2281
|
+
return [
|
|
2282
|
+
{
|
|
2283
|
+
id: 'error',
|
|
2284
|
+
name: this.transloco.translate('formBuilder.severity-error'),
|
|
2285
|
+
},
|
|
2286
|
+
{
|
|
2287
|
+
id: 'warning',
|
|
2288
|
+
name: this.transloco.translate('formBuilder.severity-warning'),
|
|
2289
|
+
},
|
|
2290
|
+
];
|
|
2291
|
+
}, ...(ngDevMode ? [{ debugName: "severityOptions" }] : []));
|
|
2292
|
+
constructor() {
|
|
2293
|
+
this.transloco.langChanges$
|
|
2294
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
2295
|
+
.subscribe((lang) => {
|
|
2296
|
+
this.activeLang.set(lang ?? 'en');
|
|
2297
|
+
});
|
|
2298
|
+
effect(() => {
|
|
2299
|
+
const data = this.initialData();
|
|
2300
|
+
if (!data) {
|
|
2301
|
+
this.messageEnControl.reset('');
|
|
2302
|
+
this.messageArControl.reset('');
|
|
2303
|
+
this.severityControl.setValue('error');
|
|
2304
|
+
this.formulaControl.reset([]);
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
this.messageEnControl.setValue(data.message?.en ?? '');
|
|
2308
|
+
this.messageArControl.setValue(data.message?.ar ?? '');
|
|
2309
|
+
this.severityControl.setValue(data.severity ?? 'error');
|
|
2310
|
+
if (data.formulaTokens) {
|
|
2311
|
+
try {
|
|
2312
|
+
const tokens = JSON.parse(data.formulaTokens);
|
|
2313
|
+
this.formulaControl.patchValue(tokens);
|
|
2314
|
+
}
|
|
2315
|
+
catch {
|
|
2316
|
+
this.formulaControl.reset([]);
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
else {
|
|
2320
|
+
this.formulaControl.reset([]);
|
|
2321
|
+
}
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
onBlockInsert(block) {
|
|
2325
|
+
const editor = this.editorRef();
|
|
2326
|
+
if (editor) {
|
|
2327
|
+
editor.addBlock(block);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
onSeverityChange(item) {
|
|
2331
|
+
this.severityControl.setValue(item.id);
|
|
2332
|
+
}
|
|
2333
|
+
onSave() {
|
|
2334
|
+
const tokens = this.formulaControl.value ?? [];
|
|
2335
|
+
const formulaTokens = tokens.length > 0 ? JSON.stringify(tokens) : '';
|
|
2336
|
+
const formulaText = tokens.length > 0 ? serializeTokens(tokens) : '';
|
|
2337
|
+
const payload = {
|
|
2338
|
+
formulaTokens,
|
|
2339
|
+
formulaText,
|
|
2340
|
+
message: {
|
|
2341
|
+
en: this.messageEnControl.value ?? '',
|
|
2342
|
+
ar: this.messageArControl.value ?? '',
|
|
2343
|
+
},
|
|
2344
|
+
severity: this.severityControl.value ?? 'error',
|
|
2345
|
+
};
|
|
2346
|
+
const data = this.initialData();
|
|
2347
|
+
this.submitting.set(true);
|
|
2348
|
+
if (data?.id !== undefined && data?.id !== null) {
|
|
2349
|
+
this.facade.updateValidation(data.id, payload).subscribe({
|
|
2350
|
+
next: () => this.ref.close(true),
|
|
2351
|
+
error: () => this.submitting.set(false),
|
|
2352
|
+
});
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
this.facade.addValidation(payload).subscribe({
|
|
2356
|
+
next: () => this.ref.close(true),
|
|
2357
|
+
error: () => this.submitting.set(false),
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
onCancel() {
|
|
2361
|
+
this.ref.close(false);
|
|
2362
|
+
}
|
|
2363
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FBValidationRuleForm, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2364
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.0.3", type: FBValidationRuleForm, isStandalone: true, selector: "mt-fb-validation-rule-form", inputs: { initialData: { classPropertyName: "initialData", publicName: "initialData", isSignal: true, isRequired: false, transformFunction: null }, availableFields: { classPropertyName: "availableFields", publicName: "availableFields", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "editorRef", first: true, predicate: ["editor"], descendants: true, isSignal: true }], ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'formBuilder'\">\r\n <div [class]=\"[modalService.contentClass, 'p-4', 'overflow-y-hidden!']\">\r\n <div class=\"mt-4 h-full overflow-y-auto pb-10 flex flex-col gap-4\">\r\n <mt-formula-toolbar\r\n [knownProperties]=\"propertyKeys()\"\r\n [functionCategories]=\"functionCategories\"\r\n [operators]=\"operators\"\r\n [labels]=\"toolbarLabels\"\r\n (onBlockInsert)=\"onBlockInsert($event)\"\r\n />\r\n <mt-formula-editor\r\n #editor\r\n [placeholder]=\"t('validation-formula-placeholder')\"\r\n [formControl]=\"formulaControl\"\r\n />\r\n <div class=\"grid grid-cols-2 gap-4\">\r\n <mt-text-field\r\n [label]=\"t('message-en')\"\r\n [placeholder]=\"t('message-en')\"\r\n [formControl]=\"messageEnControl\"\r\n />\r\n <mt-text-field\r\n [label]=\"t('message-ar')\"\r\n [placeholder]=\"t('message-ar')\"\r\n [formControl]=\"messageArControl\"\r\n />\r\n </div>\r\n <div class=\"grid gap-2\">\r\n <label class=\"text-sm font-medium text-gray-700 dark:text-gray-100\">\r\n {{ t(\"severity\") }}\r\n </label>\r\n <mt-radio-cards\r\n [options]=\"severityOptions()\"\r\n [activeId]=\"severityControl.value\"\r\n (selectionChange)=\"onSeverityChange($event)\"\r\n ></mt-radio-cards>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div [class]=\"modalService.footerClass\">\r\n <mt-button\r\n [label]=\"t('cancel')\"\r\n severity=\"secondary\"\r\n [disabled]=\"submitting()\"\r\n (onClick)=\"onCancel()\"\r\n />\r\n <mt-button\r\n [label]=\"t('save')\"\r\n severity=\"primary\"\r\n [loading]=\"submitting()\"\r\n (onClick)=\"onSave()\"\r\n />\r\n </div>\r\n</ng-container>\r\n", dependencies: [{ kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: TextField, selector: "mt-text-field", inputs: ["field", "hint", "label", "placeholder", "class", "type", "readonly", "pInputs", "required", "icon", "iconPosition"] }, { kind: "component", type: RadioCards, selector: "mt-radio-cards", inputs: ["circle", "color", "size", "columns", "options", "activeId", "itemTemplate"], outputs: ["optionsChange", "activeIdChange", "selectionChange"] }, { kind: "component", type: FormulaToolbar, selector: "mt-formula-toolbar", inputs: ["knownProperties", "propertiesTemplate", "functionCategories", "operators", "initialTab", "searchPlaceholder", "labels"], outputs: ["onBlockInsert", "onTabChange"] }, { kind: "component", type: FormulaEditor, selector: "mt-formula-editor", inputs: ["placeholder", "initialTokens", "disabled", "borderless"], outputs: ["formulaChange", "tokensChange", "onBlur", "onFocus"] }] });
|
|
2365
|
+
}
|
|
2366
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FBValidationRuleForm, decorators: [{
|
|
2367
|
+
type: Component,
|
|
2368
|
+
args: [{ selector: 'mt-fb-validation-rule-form', standalone: true, imports: [
|
|
2369
|
+
TranslocoDirective,
|
|
2370
|
+
ReactiveFormsModule,
|
|
2371
|
+
Button,
|
|
2372
|
+
TextField,
|
|
2373
|
+
RadioCards,
|
|
2374
|
+
FormulaToolbar,
|
|
2375
|
+
FormulaEditor,
|
|
2376
|
+
], template: "<ng-container *transloco=\"let t; prefix: 'formBuilder'\">\r\n <div [class]=\"[modalService.contentClass, 'p-4', 'overflow-y-hidden!']\">\r\n <div class=\"mt-4 h-full overflow-y-auto pb-10 flex flex-col gap-4\">\r\n <mt-formula-toolbar\r\n [knownProperties]=\"propertyKeys()\"\r\n [functionCategories]=\"functionCategories\"\r\n [operators]=\"operators\"\r\n [labels]=\"toolbarLabels\"\r\n (onBlockInsert)=\"onBlockInsert($event)\"\r\n />\r\n <mt-formula-editor\r\n #editor\r\n [placeholder]=\"t('validation-formula-placeholder')\"\r\n [formControl]=\"formulaControl\"\r\n />\r\n <div class=\"grid grid-cols-2 gap-4\">\r\n <mt-text-field\r\n [label]=\"t('message-en')\"\r\n [placeholder]=\"t('message-en')\"\r\n [formControl]=\"messageEnControl\"\r\n />\r\n <mt-text-field\r\n [label]=\"t('message-ar')\"\r\n [placeholder]=\"t('message-ar')\"\r\n [formControl]=\"messageArControl\"\r\n />\r\n </div>\r\n <div class=\"grid gap-2\">\r\n <label class=\"text-sm font-medium text-gray-700 dark:text-gray-100\">\r\n {{ t(\"severity\") }}\r\n </label>\r\n <mt-radio-cards\r\n [options]=\"severityOptions()\"\r\n [activeId]=\"severityControl.value\"\r\n (selectionChange)=\"onSeverityChange($event)\"\r\n ></mt-radio-cards>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div [class]=\"modalService.footerClass\">\r\n <mt-button\r\n [label]=\"t('cancel')\"\r\n severity=\"secondary\"\r\n [disabled]=\"submitting()\"\r\n (onClick)=\"onCancel()\"\r\n />\r\n <mt-button\r\n [label]=\"t('save')\"\r\n severity=\"primary\"\r\n [loading]=\"submitting()\"\r\n (onClick)=\"onSave()\"\r\n />\r\n </div>\r\n</ng-container>\r\n" }]
|
|
2377
|
+
}], ctorParameters: () => [], propDecorators: { initialData: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialData", required: false }] }], availableFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableFields", required: false }] }], editorRef: [{ type: i0.ViewChild, args: ['editor', { isSignal: true }] }] } });
|
|
2378
|
+
|
|
2379
|
+
class FBValidationRules {
|
|
2380
|
+
modalService = inject(ModalService);
|
|
2381
|
+
facade = inject(FormBuilderFacade);
|
|
2382
|
+
transloco = inject(TranslocoService);
|
|
2383
|
+
destroyRef = inject(DestroyRef);
|
|
2384
|
+
validations = this.facade.validations;
|
|
2385
|
+
sections = this.facade.sections;
|
|
2386
|
+
properties = this.facade.properties;
|
|
2387
|
+
activeLang = signal(this.transloco.getActiveLang() ?? 'en', ...(ngDevMode ? [{ debugName: "activeLang" }] : []));
|
|
2388
|
+
propertiesMap = computed(() => {
|
|
2389
|
+
const map = new Map();
|
|
2390
|
+
for (const prop of this.properties()) {
|
|
2391
|
+
map.set(prop.key, prop);
|
|
2392
|
+
}
|
|
2393
|
+
return map;
|
|
2394
|
+
}, ...(ngDevMode ? [{ debugName: "propertiesMap" }] : []));
|
|
2395
|
+
availableFields = computed(() => {
|
|
2396
|
+
const sections = this.sections();
|
|
2397
|
+
const propsMap = this.propertiesMap();
|
|
2398
|
+
const lang = this.activeLang();
|
|
2399
|
+
const fields = [];
|
|
2400
|
+
for (const section of sections) {
|
|
2401
|
+
for (const field of section.fields) {
|
|
2402
|
+
const prop = field.property ?? propsMap.get(field.propertyKey);
|
|
2403
|
+
const name = this.resolvePropertyName(prop, field.propertyKey, lang);
|
|
2404
|
+
fields.push({
|
|
2405
|
+
key: field.propertyKey,
|
|
2406
|
+
name,
|
|
2407
|
+
type: prop?.viewType || 'text',
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
return fields;
|
|
2412
|
+
}, ...(ngDevMode ? [{ debugName: "availableFields" }] : []));
|
|
2413
|
+
tableRows = computed(() => {
|
|
2414
|
+
const lang = this.activeLang();
|
|
2415
|
+
return this.validations().map((rule) => ({
|
|
2416
|
+
...rule,
|
|
2417
|
+
messageText: rule.message?.[lang] ?? rule.message?.en ?? '',
|
|
2418
|
+
}));
|
|
2419
|
+
}, ...(ngDevMode ? [{ debugName: "tableRows" }] : []));
|
|
2420
|
+
tableColumns = computed(() => {
|
|
2421
|
+
const _lang = this.activeLang();
|
|
2422
|
+
return [
|
|
2423
|
+
{
|
|
2424
|
+
key: 'messageText',
|
|
2425
|
+
label: this.transloco.translate('formBuilder.validation-message'),
|
|
2426
|
+
},
|
|
2427
|
+
{
|
|
2428
|
+
key: 'enabled',
|
|
2429
|
+
label: this.transloco.translate('formBuilder.validation-active'),
|
|
2430
|
+
type: 'boolean',
|
|
2431
|
+
},
|
|
2432
|
+
];
|
|
2433
|
+
}, ...(ngDevMode ? [{ debugName: "tableColumns" }] : []));
|
|
2434
|
+
rowActions = computed(() => {
|
|
2435
|
+
const _lang = this.activeLang();
|
|
2436
|
+
return [
|
|
2437
|
+
{
|
|
2438
|
+
icon: 'custom.pencil',
|
|
2439
|
+
tooltip: this.transloco.translate('formBuilder.edit-validation-rule'),
|
|
2440
|
+
action: (row) => this.openEdit(row),
|
|
2441
|
+
},
|
|
2442
|
+
{
|
|
2443
|
+
icon: 'general.trash-01',
|
|
2444
|
+
tooltip: this.transloco.translate('formBuilder.delete'),
|
|
2445
|
+
action: (row) => this.deleteRule(row),
|
|
2446
|
+
confirmation: {
|
|
2447
|
+
type: 'popup',
|
|
2448
|
+
confirmationType: 'delete',
|
|
2449
|
+
},
|
|
2450
|
+
},
|
|
2451
|
+
];
|
|
2452
|
+
}, ...(ngDevMode ? [{ debugName: "rowActions" }] : []));
|
|
2453
|
+
constructor() {
|
|
2454
|
+
this.transloco.langChanges$
|
|
2455
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
2456
|
+
.subscribe((lang) => {
|
|
2457
|
+
this.activeLang.set(lang ?? 'en');
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
resolvePropertyName(property, fallbackKey, lang) {
|
|
2461
|
+
if (!property) {
|
|
2462
|
+
return `Property ${fallbackKey}`;
|
|
2463
|
+
}
|
|
2464
|
+
const name = property?.name;
|
|
2465
|
+
if (typeof name === 'string') {
|
|
2466
|
+
return name;
|
|
2467
|
+
}
|
|
2468
|
+
const display = name?.['display'];
|
|
2469
|
+
if (typeof display === 'string' && display.trim().length > 0) {
|
|
2470
|
+
return display;
|
|
2471
|
+
}
|
|
2472
|
+
const localized = name?.[lang] ?? name?.['en'] ?? name?.['ar'];
|
|
2473
|
+
if (typeof localized === 'string' && localized.trim().length > 0) {
|
|
2474
|
+
return localized;
|
|
2475
|
+
}
|
|
2476
|
+
return property?.key ?? `Property ${fallbackKey}`;
|
|
2477
|
+
}
|
|
2478
|
+
openCreate() {
|
|
2479
|
+
this.modalService.openModal(FBValidationRuleForm, 'drawer', {
|
|
2480
|
+
header: this.transloco.translate('formBuilder.add-validation-rule'),
|
|
2481
|
+
styleClass: '!w-[65%] !absolute',
|
|
2482
|
+
position: 'start',
|
|
2483
|
+
modal: true,
|
|
2484
|
+
dismissible: true,
|
|
2485
|
+
appendTo: '#page-content',
|
|
2486
|
+
inputValues: {
|
|
2487
|
+
availableFields: this.availableFields(),
|
|
2488
|
+
},
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
openEdit(row) {
|
|
2492
|
+
this.modalService.openModal(FBValidationRuleForm, 'drawer', {
|
|
2493
|
+
header: this.transloco.translate('formBuilder.edit-validation-rule'),
|
|
2494
|
+
styleClass: '!w-[65%] !absolute',
|
|
2495
|
+
position: 'start',
|
|
2496
|
+
modal: true,
|
|
2497
|
+
dismissible: true,
|
|
2498
|
+
appendTo: '#page-content',
|
|
2499
|
+
inputValues: {
|
|
2500
|
+
initialData: row,
|
|
2501
|
+
availableFields: this.availableFields(),
|
|
2502
|
+
},
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
deleteRule(row) {
|
|
2506
|
+
this.facade.deleteValidation(row.id).subscribe();
|
|
2507
|
+
}
|
|
2508
|
+
onCellChange(event) {
|
|
2509
|
+
if (event.column !== 'enabled')
|
|
2510
|
+
return;
|
|
2511
|
+
this.facade
|
|
2512
|
+
.toggleValidationActive(event.row.id, { enabled: event.value })
|
|
2513
|
+
.subscribe();
|
|
2514
|
+
}
|
|
2515
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FBValidationRules, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2516
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.3", type: FBValidationRules, isStandalone: true, selector: "mt-fb-validation-rules", ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'formBuilder'\">\r\n <div [class]=\"[modalService.contentClass, 'p-4', 'overflow-y-auto']\">\r\n <div class=\"flex justify-end pb-4\">\r\n <mt-button\r\n [label]=\"t('add-validation-rule')\"\r\n severity=\"primary\"\r\n variant=\"outlined\"\r\n (onClick)=\"openCreate()\"\r\n ></mt-button>\r\n </div>\r\n <mt-table\r\n [data]=\"tableRows()\"\r\n [columns]=\"tableColumns()\"\r\n [rowActions]=\"rowActions()\"\r\n (cellChange)=\"onCellChange($event)\"\r\n ></mt-table>\r\n </div>\r\n</ng-container>\r\n", dependencies: [{ kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: Table, selector: "mt-table", inputs: ["filters", "data", "columns", "rowActions", "size", "showGridlines", "stripedRows", "selectableRows", "generalSearch", "showFilters", "loading", "updating", "lazy", "lazyTotalRecords", "reorderableColumns", "reorderableRows", "dataKey", "exportable", "exportFilename", "tabs", "tabsOptionLabel", "tabsOptionValue", "activeTab", "actions", "paginatorPosition", "pageSize", "currentPage", "first", "filterTerm"], outputs: ["selectionChange", "cellChange", "lazyLoad", "columnReorder", "rowReorder", "filtersChange", "activeTabChange", "onTabChange", "pageSizeChange", "currentPageChange", "firstChange", "filterTermChange"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }] });
|
|
2517
|
+
}
|
|
2518
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FBValidationRules, decorators: [{
|
|
2519
|
+
type: Component,
|
|
2520
|
+
args: [{ selector: 'mt-fb-validation-rules', standalone: true, imports: [TranslocoDirective, Table, Button], template: "<ng-container *transloco=\"let t; prefix: 'formBuilder'\">\r\n <div [class]=\"[modalService.contentClass, 'p-4', 'overflow-y-auto']\">\r\n <div class=\"flex justify-end pb-4\">\r\n <mt-button\r\n [label]=\"t('add-validation-rule')\"\r\n severity=\"primary\"\r\n variant=\"outlined\"\r\n (onClick)=\"openCreate()\"\r\n ></mt-button>\r\n </div>\r\n <mt-table\r\n [data]=\"tableRows()\"\r\n [columns]=\"tableColumns()\"\r\n [rowActions]=\"rowActions()\"\r\n (cellChange)=\"onCellChange($event)\"\r\n ></mt-table>\r\n </div>\r\n</ng-container>\r\n" }]
|
|
2521
|
+
}], ctorParameters: () => [] });
|
|
2522
|
+
|
|
2523
|
+
class FormBuilderContextService {
|
|
2524
|
+
http = inject(HttpClient);
|
|
2525
|
+
baseUrl = 'context';
|
|
2526
|
+
/**
|
|
2527
|
+
* Resolves context data including navigation paths and properties.
|
|
2528
|
+
* No caching - always fetches fresh data.
|
|
2529
|
+
*/
|
|
2530
|
+
resolveContext(contextKey, options = {}) {
|
|
2531
|
+
if (!contextKey)
|
|
2532
|
+
return of(null);
|
|
2533
|
+
return this.http
|
|
2534
|
+
.post(`${this.baseUrl}/resolve`, {
|
|
2535
|
+
contextKey,
|
|
2536
|
+
displayTags: options.displayTags,
|
|
2537
|
+
propertyFlags: options.propertyFlags,
|
|
2538
|
+
matchAllTags: options.matchAllTags,
|
|
2539
|
+
includeVirtual: options.includeVirtual,
|
|
2540
|
+
includeNavigationPaths: options.includeNavigationPaths,
|
|
2541
|
+
includeConnectionMetadata: options.includeConnectionMetadata,
|
|
2542
|
+
includeRelatedContextValues: options.includeRelatedContextValues,
|
|
2543
|
+
relatedContextsToInclude: options.relatedContextsToInclude,
|
|
2544
|
+
})
|
|
2545
|
+
.pipe(map((response) => this.normalizeResponse(response)), catchError((error) => {
|
|
2546
|
+
console.warn('Failed to resolve context:', error);
|
|
2547
|
+
return of(null);
|
|
2548
|
+
}));
|
|
2549
|
+
}
|
|
2550
|
+
/**
|
|
2551
|
+
* Gets properties for a context.
|
|
2552
|
+
* No caching - always fetches fresh data.
|
|
2553
|
+
*/
|
|
2554
|
+
getProperties(contextKey, options = {}) {
|
|
2555
|
+
if (!contextKey)
|
|
2556
|
+
return of([]);
|
|
2557
|
+
return this.resolveContext(contextKey, options).pipe(map((resolved) => resolved?.properties ?? []));
|
|
2558
|
+
}
|
|
2559
|
+
normalizeResponse(response) {
|
|
2560
|
+
const payload = response?.data ?? response;
|
|
2561
|
+
if (!payload)
|
|
2562
|
+
return null;
|
|
2563
|
+
const properties = this.normalizeProperties(payload.properties ?? []);
|
|
2564
|
+
return {
|
|
2565
|
+
...payload,
|
|
2566
|
+
properties,
|
|
2567
|
+
};
|
|
2568
|
+
}
|
|
2569
|
+
normalizeProperties(raw) {
|
|
2570
|
+
return (raw ?? [])
|
|
2571
|
+
.map((prop) => {
|
|
2572
|
+
const key = prop?.key ?? prop?.propertyKey;
|
|
2573
|
+
if (!key)
|
|
2574
|
+
return null;
|
|
2575
|
+
const name = prop?.name ?? prop?.displayName ?? key;
|
|
2576
|
+
return {
|
|
2577
|
+
...prop,
|
|
2578
|
+
key,
|
|
2579
|
+
name,
|
|
2580
|
+
};
|
|
2581
|
+
})
|
|
2582
|
+
.filter((prop) => Boolean(prop));
|
|
2583
|
+
}
|
|
2584
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormBuilderContextService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2585
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormBuilderContextService, providedIn: 'root' });
|
|
2586
|
+
}
|
|
2587
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormBuilderContextService, decorators: [{
|
|
2588
|
+
type: Injectable,
|
|
2589
|
+
args: [{ providedIn: 'root' }]
|
|
2590
|
+
}] });
|
|
2591
|
+
|
|
2592
|
+
const SCOPE_ORDER = [
|
|
2593
|
+
'Current',
|
|
2594
|
+
'Host',
|
|
2595
|
+
'Parent',
|
|
2596
|
+
'Ancestors',
|
|
2597
|
+
'Children',
|
|
2598
|
+
'Descendants',
|
|
2599
|
+
'Related',
|
|
2600
|
+
'Siblings',
|
|
2601
|
+
];
|
|
2602
|
+
const PATH_TOKENS = [
|
|
2603
|
+
'Parent',
|
|
2604
|
+
'Ancestors',
|
|
2605
|
+
'Children',
|
|
2606
|
+
'Descendants',
|
|
2607
|
+
'Related',
|
|
2608
|
+
'Siblings',
|
|
2609
|
+
'Host',
|
|
2610
|
+
];
|
|
2611
|
+
const SCOPE_LABELS = {
|
|
2612
|
+
Current: 'Current',
|
|
2613
|
+
Host: 'Host',
|
|
2614
|
+
Parent: 'Parent',
|
|
2615
|
+
Ancestors: 'Ancestors',
|
|
2616
|
+
Children: 'Children',
|
|
2617
|
+
Descendants: 'Descendants',
|
|
2618
|
+
Related: 'Related',
|
|
2619
|
+
Siblings: 'Siblings',
|
|
2620
|
+
};
|
|
2621
|
+
// ============================================================================
|
|
2622
|
+
// Component
|
|
2623
|
+
// ============================================================================
|
|
2624
|
+
class FormBuilder {
|
|
2625
|
+
modalService = inject(ModalService);
|
|
2626
|
+
confirmationService = inject(ConfirmationService);
|
|
2627
|
+
translocoService = inject(TranslocoService);
|
|
2628
|
+
facade = inject(FormBuilderFacade);
|
|
2629
|
+
contextService = inject(FormBuilderContextService);
|
|
2630
|
+
popovers = viewChildren(Popover, ...(ngDevMode ? [{ debugName: "popovers" }] : []));
|
|
2631
|
+
dialogRef;
|
|
2632
|
+
// ============================================================================
|
|
2633
|
+
// Facade State
|
|
2634
|
+
// ============================================================================
|
|
2635
|
+
sections = this.facade.sections;
|
|
2636
|
+
properties = this.facade.properties;
|
|
2637
|
+
isLoading = this.facade.isLoadingFormConfiguration;
|
|
2638
|
+
error = this.facade.formConfigurationError;
|
|
2639
|
+
moduleType = this.facade.moduleType;
|
|
2640
|
+
moduleId = this.facade.moduleId;
|
|
2641
|
+
parentPath = this.facade.parentPath;
|
|
2642
|
+
// ============================================================================
|
|
2643
|
+
// UI State (Simple Signals)
|
|
2644
|
+
// ============================================================================
|
|
2645
|
+
activeScope = signal('Current', ...(ngDevMode ? [{ debugName: "activeScope" }] : []));
|
|
2646
|
+
searchQuery = signal('', ...(ngDevMode ? [{ debugName: "searchQuery" }] : []));
|
|
2647
|
+
isContextLoading = signal(false, ...(ngDevMode ? [{ debugName: "isContextLoading" }] : []));
|
|
2648
|
+
// Initial context data (from first load)
|
|
2649
|
+
initialContext = signal(null, ...(ngDevMode ? [{ debugName: "initialContext" }] : []));
|
|
2650
|
+
currentProperties = signal([], ...(ngDevMode ? [{ debugName: "currentProperties" }] : []));
|
|
2651
|
+
// Non-current scope state (reset on tab change)
|
|
2652
|
+
scopeLoading = signal(false, ...(ngDevMode ? [{ debugName: "scopeLoading" }] : []));
|
|
2653
|
+
scopeProperties = signal([], ...(ngDevMode ? [{ debugName: "scopeProperties" }] : []));
|
|
2654
|
+
scopePath = signal([], ...(ngDevMode ? [{ debugName: "scopePath" }] : []));
|
|
2655
|
+
scopeSelectedKey = signal('', ...(ngDevMode ? [{ debugName: "scopeSelectedKey" }] : []));
|
|
2656
|
+
scopeNavigation = signal(null, ...(ngDevMode ? [{ debugName: "scopeNavigation" }] : []));
|
|
2657
|
+
// Request tracking
|
|
2658
|
+
initialRequestId = 0;
|
|
2659
|
+
scopeRequestId = 0;
|
|
2660
|
+
lastContextKey = '';
|
|
2661
|
+
// ============================================================================
|
|
2662
|
+
// Computed Values
|
|
2663
|
+
// ============================================================================
|
|
2664
|
+
contextKey = computed(() => {
|
|
2665
|
+
const type = this.moduleType();
|
|
2666
|
+
const id = this.moduleId();
|
|
2667
|
+
const parent = this.parentPath();
|
|
2668
|
+
if (!type || id === null || id === undefined)
|
|
2669
|
+
return '';
|
|
2670
|
+
const segments = [];
|
|
2671
|
+
const raw = parent.split('/').filter(Boolean);
|
|
2672
|
+
for (let i = 0; i < raw.length; i += 2) {
|
|
2673
|
+
if (raw[i] && raw[i + 1] !== undefined) {
|
|
2674
|
+
segments.push(`${raw[i]}:${raw[i + 1]}`);
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
segments.push(`${type}:${id}`);
|
|
2678
|
+
return segments.join('/');
|
|
2679
|
+
}, ...(ngDevMode ? [{ debugName: "contextKey" }] : []));
|
|
2680
|
+
usedPropertyKeys = computed(() => {
|
|
2681
|
+
return new Set(this.sections().flatMap((s) => s.fields.map((f) => f.propertyKey)));
|
|
2682
|
+
}, ...(ngDevMode ? [{ debugName: "usedPropertyKeys" }] : []));
|
|
2683
|
+
propertiesMap = computed(() => {
|
|
2684
|
+
const map = new Map();
|
|
2685
|
+
for (const prop of this.properties()) {
|
|
2686
|
+
map.set(prop.key, prop);
|
|
2687
|
+
}
|
|
2688
|
+
return map;
|
|
2689
|
+
}, ...(ngDevMode ? [{ debugName: "propertiesMap" }] : []));
|
|
2690
|
+
/** Navigation options from initial context */
|
|
2691
|
+
baseScopeContexts = computed(() => {
|
|
2692
|
+
const ctx = this.initialContext();
|
|
2693
|
+
const nav = ctx?.navigationPaths;
|
|
2694
|
+
const currentKey = ctx?.currentState?.contextPath ?? this.contextKey();
|
|
2695
|
+
return {
|
|
2696
|
+
Current: currentKey
|
|
2697
|
+
? [this.createContextOption(currentKey, ctx?.currentState)]
|
|
2698
|
+
: [],
|
|
2699
|
+
Host: nav?.hostContext
|
|
2700
|
+
? [this.createContextOption(nav.hostContext)]
|
|
2701
|
+
: [],
|
|
2702
|
+
Parent: this.mergeContextOptions(nav?.parent),
|
|
2703
|
+
Ancestors: this.mergeContextOptions(nav?.ancestors),
|
|
2704
|
+
Children: this.mergeContextOptions(nav?.children),
|
|
2705
|
+
Descendants: this.mergeContextOptions(nav?.descendants),
|
|
2706
|
+
Related: this.mergeContextOptions(nav?.related),
|
|
2707
|
+
Siblings: this.mergeContextOptions(nav?.siblingContexts),
|
|
2708
|
+
};
|
|
2709
|
+
}, ...(ngDevMode ? [{ debugName: "baseScopeContexts" }] : []));
|
|
2710
|
+
/** Available scope tabs */
|
|
2711
|
+
scopeOptions = computed(() => {
|
|
2712
|
+
const contexts = this.baseScopeContexts();
|
|
2713
|
+
return SCOPE_ORDER.filter((s) => (contexts[s] ?? []).length > 0).map((s) => ({ key: s, label: SCOPE_LABELS[s] }));
|
|
2714
|
+
}, ...(ngDevMode ? [{ debugName: "scopeOptions" }] : []));
|
|
2715
|
+
/** Enriched sections for display */
|
|
2716
|
+
enrichedSections = computed(() => {
|
|
2717
|
+
const sections = this.sections();
|
|
2718
|
+
const propsMap = this.propertiesMap();
|
|
2719
|
+
const lang = document.documentElement.lang;
|
|
2720
|
+
return sections.map((section) => ({
|
|
2721
|
+
...section,
|
|
2722
|
+
fields: section.fields.map((field) => {
|
|
2723
|
+
const prop = field.property ?? propsMap.get(field.propertyKey);
|
|
2724
|
+
return {
|
|
2725
|
+
...field,
|
|
2726
|
+
name: this.resolvePropertyName(prop, field.propertyKey, lang),
|
|
2727
|
+
type: prop?.viewType || 'text',
|
|
2728
|
+
data: prop,
|
|
2729
|
+
};
|
|
2730
|
+
}),
|
|
2731
|
+
}));
|
|
2732
|
+
}, ...(ngDevMode ? [{ debugName: "enrichedSections" }] : []));
|
|
2733
|
+
// ============================================================================
|
|
2734
|
+
// Lifecycle
|
|
2735
|
+
// ============================================================================
|
|
2736
|
+
constructor() {
|
|
2737
|
+
// Effect: watch for context key changes and load initial data
|
|
2738
|
+
effect(() => {
|
|
2739
|
+
const key = this.contextKey();
|
|
2740
|
+
if (key && key !== this.lastContextKey) {
|
|
2741
|
+
this.lastContextKey = key;
|
|
2742
|
+
this.loadInitialContext(key);
|
|
2743
|
+
}
|
|
2744
|
+
}, { allowSignalWrites: true });
|
|
2745
|
+
}
|
|
2746
|
+
ngOnInit() {
|
|
2747
|
+
// Trigger initial load if context key is already available
|
|
2748
|
+
const key = this.contextKey();
|
|
2749
|
+
if (key && key !== this.lastContextKey) {
|
|
2750
|
+
this.lastContextKey = key;
|
|
2751
|
+
this.loadInitialContext(key);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
// ============================================================================
|
|
2755
|
+
// Initial Context Loading
|
|
2756
|
+
// ============================================================================
|
|
2757
|
+
loadInitialContext(contextKey) {
|
|
2758
|
+
const requestId = ++this.initialRequestId;
|
|
2759
|
+
this.isContextLoading.set(true);
|
|
2760
|
+
this.initialContext.set(null);
|
|
2761
|
+
this.currentProperties.set([]);
|
|
2762
|
+
this.resetScopeState();
|
|
2763
|
+
this.activeScope.set('Current');
|
|
2764
|
+
this.contextService
|
|
2765
|
+
.resolveContext(contextKey, {
|
|
2766
|
+
propertyFlags: 'None',
|
|
2767
|
+
includeVirtual: true,
|
|
2768
|
+
includeNavigationPaths: true,
|
|
2769
|
+
includeConnectionMetadata: true,
|
|
2770
|
+
matchAllTags: true,
|
|
2771
|
+
includeRelatedContextValues: true,
|
|
2772
|
+
})
|
|
2773
|
+
.subscribe((response) => {
|
|
2774
|
+
if (requestId !== this.initialRequestId)
|
|
2775
|
+
return;
|
|
2776
|
+
this.isContextLoading.set(false);
|
|
2777
|
+
this.initialContext.set(response);
|
|
2778
|
+
this.currentProperties.set(response?.properties ?? []);
|
|
2779
|
+
this.facade.setProperties(response?.properties ?? []);
|
|
2780
|
+
});
|
|
2781
|
+
}
|
|
2782
|
+
// ============================================================================
|
|
2783
|
+
// Tab Change Handler (Called from template)
|
|
2784
|
+
// ============================================================================
|
|
2785
|
+
onScopeChange(scope) {
|
|
2786
|
+
if (scope === this.activeScope())
|
|
2787
|
+
return;
|
|
2788
|
+
this.activeScope.set(scope);
|
|
2789
|
+
this.resetScopeState();
|
|
2790
|
+
if (scope === 'Current') {
|
|
2791
|
+
return;
|
|
2792
|
+
}
|
|
2793
|
+
// Get first context option for this scope
|
|
2794
|
+
const options = this.baseScopeContexts()[scope] ?? [];
|
|
2795
|
+
if (options.length === 0)
|
|
2796
|
+
return;
|
|
2797
|
+
const firstOption = options[0];
|
|
2798
|
+
this.scopePath.set([
|
|
2799
|
+
{
|
|
2800
|
+
token: scope,
|
|
2801
|
+
value: firstOption.contextKey,
|
|
2802
|
+
label: firstOption.label,
|
|
2803
|
+
},
|
|
2804
|
+
]);
|
|
2805
|
+
this.loadScopeContext(firstOption.contextKey);
|
|
2806
|
+
}
|
|
2807
|
+
// ============================================================================
|
|
2808
|
+
// Scope Context Loading
|
|
2809
|
+
// ============================================================================
|
|
2810
|
+
loadScopeContext(contextKey) {
|
|
2811
|
+
const requestId = ++this.scopeRequestId;
|
|
2812
|
+
this.scopeLoading.set(true);
|
|
2813
|
+
this.contextService
|
|
2814
|
+
.resolveContext(contextKey, {
|
|
2815
|
+
propertyFlags: 'None',
|
|
2816
|
+
includeVirtual: true,
|
|
2817
|
+
includeNavigationPaths: true,
|
|
2818
|
+
includeConnectionMetadata: true,
|
|
2819
|
+
matchAllTags: true,
|
|
2820
|
+
includeRelatedContextValues: true,
|
|
2821
|
+
})
|
|
2822
|
+
.subscribe((response) => {
|
|
2823
|
+
if (requestId !== this.scopeRequestId)
|
|
2824
|
+
return;
|
|
2825
|
+
this.scopeLoading.set(false);
|
|
2826
|
+
this.scopeNavigation.set(response);
|
|
2827
|
+
this.scopeProperties.set(response?.properties ?? []);
|
|
2828
|
+
// Auto-select first available property
|
|
2829
|
+
const used = this.usedPropertyKeys();
|
|
2830
|
+
const available = (response?.properties ?? []).filter((p) => !used.has(p.key));
|
|
2831
|
+
if (available.length > 0) {
|
|
2832
|
+
this.scopeSelectedKey.set(available[0].key);
|
|
2833
|
+
}
|
|
2834
|
+
});
|
|
2835
|
+
}
|
|
2836
|
+
resetScopeState() {
|
|
2837
|
+
this.scopeLoading.set(false);
|
|
2838
|
+
this.scopeProperties.set([]);
|
|
2839
|
+
this.scopePath.set([]);
|
|
2840
|
+
this.scopeSelectedKey.set('');
|
|
2841
|
+
this.scopeNavigation.set(null);
|
|
2842
|
+
this.hideAllPopovers();
|
|
2843
|
+
}
|
|
2844
|
+
// ============================================================================
|
|
2845
|
+
// Path Segment Methods (accept scope parameter for template compatibility)
|
|
2846
|
+
// ============================================================================
|
|
2847
|
+
getScopeBaseContexts(scope) {
|
|
2848
|
+
return this.baseScopeContexts()[scope] ?? [];
|
|
2849
|
+
}
|
|
2850
|
+
/** Template calls this with scope.key */
|
|
2851
|
+
getScopePath(_scope) {
|
|
2852
|
+
return this.scopePath();
|
|
2853
|
+
}
|
|
2854
|
+
getTokenLabel(token) {
|
|
2855
|
+
return SCOPE_LABELS[token] ?? token;
|
|
2856
|
+
}
|
|
2857
|
+
getScopeSegmentLabel(_scope, index) {
|
|
2858
|
+
const path = this.scopePath();
|
|
2859
|
+
const segment = path[index];
|
|
2860
|
+
if (!segment?.value)
|
|
2861
|
+
return `Select ${this.getTokenLabel(segment?.token)}`;
|
|
2862
|
+
// Use stored label - don't look up from options (which change on resolve)
|
|
2863
|
+
return segment.label || this.formatContextKey(segment.value);
|
|
2864
|
+
}
|
|
2865
|
+
getScopeSegmentOptions(_scope, index) {
|
|
2866
|
+
const path = this.scopePath();
|
|
2867
|
+
const segment = path[index];
|
|
2868
|
+
if (!segment)
|
|
2869
|
+
return [];
|
|
2870
|
+
if (index === 0) {
|
|
2871
|
+
return this.getScopeBaseContexts(this.activeScope());
|
|
2872
|
+
}
|
|
2873
|
+
const nav = this.scopeNavigation();
|
|
2874
|
+
return this.getNavigationOptionsByToken(nav, segment.token);
|
|
2875
|
+
}
|
|
2876
|
+
setScopeSegmentValue(_scope, index, contextKey, label) {
|
|
2877
|
+
const path = this.scopePath();
|
|
2878
|
+
const newPath = path.slice(0, index + 1);
|
|
2879
|
+
if (newPath[index]) {
|
|
2880
|
+
// Store label at selection time so it doesn't change on resolve
|
|
2881
|
+
const segmentLabel = label || this.findOptionLabel(_scope, index, contextKey);
|
|
2882
|
+
newPath[index] = {
|
|
2883
|
+
...newPath[index],
|
|
2884
|
+
value: contextKey,
|
|
2885
|
+
label: segmentLabel,
|
|
2886
|
+
};
|
|
2887
|
+
}
|
|
2888
|
+
this.scopePath.set(newPath);
|
|
2889
|
+
this.loadScopeContext(contextKey);
|
|
2890
|
+
}
|
|
2891
|
+
findOptionLabel(_scope, index, contextKey) {
|
|
2892
|
+
const options = this.getScopeSegmentOptions(_scope, index);
|
|
2893
|
+
const option = options.find((o) => o.contextKey === contextKey);
|
|
2894
|
+
return option?.label ?? this.formatContextKey(contextKey);
|
|
2895
|
+
}
|
|
2896
|
+
/** Template handler: hide popover first, then set value (like formula-builder) */
|
|
2897
|
+
onSetScopeSegmentValue(scope, index, contextKey, popover) {
|
|
2898
|
+
popover.hide();
|
|
2899
|
+
this.setScopeSegmentValue(scope, index, contextKey);
|
|
2900
|
+
}
|
|
2901
|
+
addScopeSegment(_scope, token) {
|
|
2902
|
+
const path = this.scopePath();
|
|
2903
|
+
this.scopePath.set([...path, { token, value: '', label: '' }]);
|
|
2904
|
+
}
|
|
2905
|
+
/** Template handler: hide popover first, then add segment (like formula-builder) */
|
|
2906
|
+
onAddScopeSegment(scope, token, popover) {
|
|
2907
|
+
popover.hide();
|
|
2908
|
+
this.addScopeSegment(scope, token);
|
|
2909
|
+
}
|
|
2910
|
+
getScopeNextTokens(_scope) {
|
|
2911
|
+
const path = this.scopePath();
|
|
2912
|
+
const last = path[path.length - 1];
|
|
2913
|
+
if (!last?.value)
|
|
2914
|
+
return [];
|
|
2915
|
+
const nav = this.scopeNavigation();
|
|
2916
|
+
if (!nav)
|
|
2917
|
+
return [];
|
|
2918
|
+
return PATH_TOKENS.filter((t) => this.getNavigationOptionsByToken(nav, t).length > 0);
|
|
2919
|
+
}
|
|
2920
|
+
canAddNextScopeSegment(_scope) {
|
|
2921
|
+
const path = this.scopePath();
|
|
2922
|
+
if (path.length === 0)
|
|
2923
|
+
return false;
|
|
2924
|
+
if (!path.every((s) => Boolean(s.value)))
|
|
2925
|
+
return false;
|
|
2926
|
+
return this.getScopeNextTokens().length > 0;
|
|
2927
|
+
}
|
|
2928
|
+
canRemoveScopePath(_scope) {
|
|
2929
|
+
return this.scopePath().length > 1;
|
|
2930
|
+
}
|
|
2931
|
+
removeScopePath(_scope) {
|
|
2932
|
+
const path = this.scopePath();
|
|
2933
|
+
if (path.length <= 1)
|
|
2934
|
+
return;
|
|
2935
|
+
const newPath = path.slice(0, -1);
|
|
2936
|
+
this.scopePath.set(newPath);
|
|
2937
|
+
const last = newPath[newPath.length - 1];
|
|
2938
|
+
if (last?.value) {
|
|
2939
|
+
this.loadScopeContext(last.value);
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
/** Template-compatible version (returns properties array for the scope) */
|
|
2943
|
+
getScopeProperties(_scope) {
|
|
2944
|
+
const scope = _scope ?? this.activeScope();
|
|
2945
|
+
return scope === 'Current'
|
|
2946
|
+
? this.currentProperties()
|
|
2947
|
+
: this.scopeProperties();
|
|
2948
|
+
}
|
|
2949
|
+
// ============================================================================
|
|
2950
|
+
// Property Methods (accept scope parameter for template compatibility)
|
|
2951
|
+
// ============================================================================
|
|
2952
|
+
getScopePropertyOptions(_scope) {
|
|
2953
|
+
const scope = _scope ?? this.activeScope();
|
|
2954
|
+
const props = scope === 'Current' ? this.currentProperties() : this.scopeProperties();
|
|
2955
|
+
const used = this.usedPropertyKeys();
|
|
2956
|
+
return props
|
|
2957
|
+
.filter((p) => !used.has(p.key))
|
|
2958
|
+
.map((p) => ({ key: p.key, name: this.getPropertyLabel(p) }));
|
|
2959
|
+
}
|
|
2960
|
+
getScopeSelectedPropertyKey(_scope) {
|
|
2961
|
+
return this.scopeSelectedKey();
|
|
2962
|
+
}
|
|
2963
|
+
setScopeSelectedProperty(_scope, key) {
|
|
2964
|
+
this.scopeSelectedKey.set(key);
|
|
2965
|
+
}
|
|
2966
|
+
getScopeSelectedProperty(_scope) {
|
|
2967
|
+
const key = this.scopeSelectedKey();
|
|
2968
|
+
if (!key)
|
|
2969
|
+
return null;
|
|
2970
|
+
return this.scopeProperties().find((p) => p.key === key) ?? null;
|
|
2971
|
+
}
|
|
2972
|
+
getFilteredProperties(_scope) {
|
|
2973
|
+
const scope = _scope ?? this.activeScope();
|
|
2974
|
+
const used = this.usedPropertyKeys();
|
|
2975
|
+
const query = this.searchQuery().toLowerCase().trim();
|
|
2976
|
+
if (scope !== 'Current') {
|
|
2977
|
+
const selected = this.getScopeSelectedProperty();
|
|
2978
|
+
if (!selected || used.has(selected.key))
|
|
2979
|
+
return [];
|
|
2980
|
+
return [selected];
|
|
2981
|
+
}
|
|
2982
|
+
let available = this.currentProperties().filter((p) => !used.has(p.key));
|
|
2983
|
+
if (query) {
|
|
2984
|
+
available = available.filter((p) => this.getPropertyLabel(p).toLowerCase().includes(query));
|
|
2985
|
+
}
|
|
2986
|
+
return available;
|
|
2987
|
+
}
|
|
2988
|
+
isScopePropertiesLoading(_scope) {
|
|
2989
|
+
const scope = _scope ?? this.activeScope();
|
|
2990
|
+
return scope !== 'Current' && this.scopeLoading();
|
|
2991
|
+
}
|
|
2992
|
+
getPropertyLabel(property) {
|
|
2993
|
+
const lang = document.documentElement.lang;
|
|
2994
|
+
return this.resolvePropertyName(property, property.key, lang);
|
|
2995
|
+
}
|
|
2996
|
+
// ============================================================================
|
|
2997
|
+
// Helper Methods
|
|
2998
|
+
// ============================================================================
|
|
2999
|
+
hideAllPopovers() {
|
|
3000
|
+
for (const popover of this.popovers()) {
|
|
3001
|
+
popover.hide();
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
getNavigationOptionsByToken(response, token) {
|
|
3005
|
+
const nav = response?.navigationPaths;
|
|
3006
|
+
if (!nav)
|
|
3007
|
+
return [];
|
|
3008
|
+
switch (token) {
|
|
3009
|
+
case 'Parent':
|
|
3010
|
+
return this.mergeContextOptions(nav.parent);
|
|
3011
|
+
case 'Ancestors':
|
|
3012
|
+
return this.mergeContextOptions(nav.ancestors);
|
|
3013
|
+
case 'Children':
|
|
3014
|
+
return this.mergeContextOptions(nav.children);
|
|
3015
|
+
case 'Descendants':
|
|
3016
|
+
return this.mergeContextOptions(nav.descendants);
|
|
3017
|
+
case 'Related':
|
|
3018
|
+
return this.mergeContextOptions(nav.related);
|
|
3019
|
+
case 'Siblings':
|
|
3020
|
+
return this.mergeContextOptions(nav.siblingContexts);
|
|
3021
|
+
case 'Host':
|
|
3022
|
+
return nav.hostContext
|
|
3023
|
+
? [this.createContextOption(nav.hostContext)]
|
|
3024
|
+
: [];
|
|
3025
|
+
default:
|
|
3026
|
+
return [];
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
resolvePropertyName(property, fallbackKey, lang) {
|
|
3030
|
+
if (!property)
|
|
3031
|
+
return `Property ${fallbackKey}`;
|
|
3032
|
+
const name = property.name;
|
|
3033
|
+
if (typeof name === 'string')
|
|
3034
|
+
return name;
|
|
3035
|
+
const display = name?.['display'];
|
|
3036
|
+
if (typeof display === 'string' && display.trim())
|
|
3037
|
+
return display;
|
|
3038
|
+
const localized = name?.[lang] ?? name?.['en'] ?? name?.['ar'];
|
|
3039
|
+
if (typeof localized === 'string' && localized.trim())
|
|
3040
|
+
return localized;
|
|
3041
|
+
return property.key ?? `Property ${fallbackKey}`;
|
|
3042
|
+
}
|
|
3043
|
+
createContextOption(pathOrKey, currentState) {
|
|
3044
|
+
if (typeof pathOrKey === 'string') {
|
|
3045
|
+
return {
|
|
3046
|
+
contextKey: pathOrKey,
|
|
3047
|
+
label: this.formatContextLabel({
|
|
3048
|
+
contextKey: pathOrKey,
|
|
3049
|
+
contextType: currentState?.contextType,
|
|
3050
|
+
contextId: currentState?.contextId,
|
|
3051
|
+
}),
|
|
3052
|
+
};
|
|
3053
|
+
}
|
|
3054
|
+
return {
|
|
3055
|
+
contextKey: pathOrKey.contextKey,
|
|
3056
|
+
label: this.formatContextLabel(pathOrKey),
|
|
3057
|
+
};
|
|
3058
|
+
}
|
|
3059
|
+
formatContextLabel(path) {
|
|
3060
|
+
// Just return the name - no path formatting needed
|
|
3061
|
+
if (path.name)
|
|
3062
|
+
return path.name;
|
|
3063
|
+
if (path.key)
|
|
3064
|
+
return path.key;
|
|
3065
|
+
if (path.contextType && path.contextId !== undefined)
|
|
3066
|
+
return `${path.contextType} ${path.contextId}`;
|
|
3067
|
+
return path.contextKey || 'Context';
|
|
3068
|
+
}
|
|
3069
|
+
formatContextKey(contextKey) {
|
|
3070
|
+
return contextKey
|
|
3071
|
+
.split('/')
|
|
3072
|
+
.filter(Boolean)
|
|
3073
|
+
.map((seg) => {
|
|
3074
|
+
const [type, id] = seg.split(':');
|
|
3075
|
+
return id !== undefined ? `${type} ${id}` : type;
|
|
3076
|
+
})
|
|
3077
|
+
.join(' / ');
|
|
3078
|
+
}
|
|
3079
|
+
mergeContextOptions(items) {
|
|
3080
|
+
if (!items?.length)
|
|
3081
|
+
return [];
|
|
3082
|
+
const seen = new Set();
|
|
3083
|
+
return items
|
|
3084
|
+
.slice()
|
|
3085
|
+
.sort((a, b) => (a.distance ?? 0) - (b.distance ?? 0))
|
|
3086
|
+
.filter((item) => {
|
|
3087
|
+
if (!item?.contextKey || seen.has(item.contextKey))
|
|
3088
|
+
return false;
|
|
3089
|
+
seen.add(item.contextKey);
|
|
3090
|
+
return true;
|
|
3091
|
+
})
|
|
3092
|
+
.map((item) => this.createContextOption(item));
|
|
3093
|
+
}
|
|
3094
|
+
// ============================================================================
|
|
3095
|
+
// Drag & Drop
|
|
3096
|
+
// ============================================================================
|
|
3097
|
+
drop(event) {
|
|
3098
|
+
const targetSectionId = event.container.id;
|
|
3099
|
+
if (event.previousContainer === event.container) {
|
|
3100
|
+
const fields = [...event.container.data];
|
|
3101
|
+
const [movedField] = fields.splice(event.previousIndex, 1);
|
|
3102
|
+
fields.splice(event.currentIndex, 0, movedField);
|
|
3103
|
+
this.facade.reorderFields(targetSectionId, fields.map((f, i) => ({ id: f.id, order: i })));
|
|
3104
|
+
}
|
|
3105
|
+
else if (event.previousContainer.id.startsWith('toolbox-')) {
|
|
3106
|
+
const prop = event.item.data;
|
|
3107
|
+
this.facade.addField(targetSectionId, {
|
|
3108
|
+
propertyKey: prop.key,
|
|
3109
|
+
width: '100',
|
|
3110
|
+
order: event.currentIndex,
|
|
3111
|
+
hiddenInCreation: false,
|
|
3112
|
+
});
|
|
3113
|
+
}
|
|
3114
|
+
else {
|
|
3115
|
+
const field = event.item.data;
|
|
3116
|
+
this.facade.moveField(event.previousContainer.id, field.id, {
|
|
3117
|
+
targetSectionId,
|
|
3118
|
+
order: event.currentIndex,
|
|
3119
|
+
});
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
// ============================================================================
|
|
3123
|
+
// Modal Actions
|
|
3124
|
+
// ============================================================================
|
|
3125
|
+
addSection() {
|
|
3126
|
+
this.dialogRef = this.modalService.openModal(FBSectionForm, 'drawer', {
|
|
3127
|
+
header: this.translocoService.translate('formBuilder.add-section'),
|
|
3128
|
+
height: '20vw',
|
|
3129
|
+
styleClass: '!w-[27%] !absolute !shadow-none',
|
|
3130
|
+
position: 'end',
|
|
3131
|
+
appendTo: '#page-content',
|
|
3132
|
+
dismissible: true,
|
|
3133
|
+
inputValues: { sectionsCount: this.sections().length },
|
|
3134
|
+
});
|
|
3135
|
+
}
|
|
3136
|
+
openPreview() {
|
|
3137
|
+
this.dialogRef = this.modalService.openModal(FBPreviewForm, 'drawer', {
|
|
3138
|
+
header: this.translocoService.translate('formBuilder.preview'),
|
|
3139
|
+
styleClass: '!w-[79%] !absolute !shadow-none',
|
|
3140
|
+
position: 'end',
|
|
3141
|
+
appendTo: 'page-content',
|
|
3142
|
+
dismissible: true,
|
|
3143
|
+
inputValues: { sections: this.enrichedSections() },
|
|
3144
|
+
});
|
|
3145
|
+
}
|
|
3146
|
+
openValidationRules() {
|
|
3147
|
+
this.dialogRef = this.modalService.openModal(FBValidationRules, 'drawer', {
|
|
3148
|
+
header: this.translocoService.translate('formBuilder.validation-rules'),
|
|
3149
|
+
styleClass: '!w-[35%] !absolute !shadow-none',
|
|
3150
|
+
position: 'end',
|
|
3151
|
+
appendTo: '#page-content',
|
|
3152
|
+
dismissible: true,
|
|
3153
|
+
});
|
|
3154
|
+
}
|
|
3155
|
+
resetFormConfiguration() {
|
|
3156
|
+
this.confirmationService.confirm({
|
|
3157
|
+
type: 'dialog',
|
|
3158
|
+
acceptButtonStyleClass: 'p-button-danger',
|
|
3159
|
+
accept: () => this.facade.resetFormConfiguration(),
|
|
3160
|
+
});
|
|
3161
|
+
}
|
|
3162
|
+
noReturnPredicate = () => false;
|
|
3163
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormBuilder, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3164
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.3", type: FormBuilder, isStandalone: true, selector: "mt-form-builder", viewQueries: [{ propertyName: "popovers", predicate: Popover, descendants: true, isSignal: true }], ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'formBuilder'\">\r\n <div class=\"flex gap-7 h-full w-full overflow-hidden\" cdkDropListGroup>\r\n <!-- Properties Sidebar -->\r\n <mt-card class=\"w-1/5 h-full flex flex-col overflow-hidden\">\r\n <ng-template #headless>\r\n <!-- Header -->\r\n <h3 class=\"text-xl font-semibold px-4 pt-5\">\r\n {{ t(\"form-elements\") }}\r\n </h3>\r\n\r\n @if (isContextLoading()) {\r\n <!-- Properties Loading Skeleton -->\r\n <div class=\"flex gap-4 py-3 px-5 mt-4\">\r\n <p-skeleton height=\"2rem\" styleClass=\"rounded-lg\" />\r\n <p-skeleton height=\"2rem\" styleClass=\"rounded-lg\" />\r\n </div>\r\n <div class=\"py-4 px-5 space-y-5\">\r\n @for (i of [1, 2, 3, 4, 5, 6]; track i) {\r\n <p-skeleton height=\"3rem\" styleClass=\"rounded-lg\" />\r\n }\r\n </div>\r\n } @else if (scopeOptions().length === 0) {\r\n <!-- No Properties State -->\r\n <div class=\"flex-1 flex items-center justify-center p-4\">\r\n <p class=\"text-sm font-semibold text-gray-500\">\r\n {{ t(\"no-data-found\") }}\r\n </p>\r\n </div>\r\n } @else {\r\n <!-- Scope Tabs using PrimeNG -->\r\n <p-tabs\r\n [value]=\"activeScope()\"\r\n (valueChange)=\"onScopeChange($event)\"\r\n styleClass=\"structure-tabs\"\r\n class=\"flex flex-1 flex-col min-h-0\"\r\n >\r\n <p-tablist class=\"shrink-0\">\r\n @for (scope of scopeOptions(); track scope.key) {\r\n <p-tab [value]=\"scope.key\">{{ scope.label }}</p-tab>\r\n }\r\n </p-tablist>\r\n <p-tabpanels\r\n class=\"!bg-transparent !p-0 !pb-3 flex-1 overflow-hidden\"\r\n >\r\n @for (scope of scopeOptions(); track scope.key) {\r\n <p-tabpanel [value]=\"scope.key\" class=\"h-full flex flex-col\">\r\n @if (scope.key !== \"Current\") {\r\n <div class=\"px-4 pt-3 pb-2\">\r\n <div\r\n class=\"flex flex-col gap-3 rounded-lg bg-slate-50 px-3 py-2 dark:bg-slate-800/50\"\r\n >\r\n <div class=\"flex items-center gap-2\">\r\n <span\r\n class=\"shrink-0 text-xs font-semibold uppercase tracking-wider text-slate-400\"\r\n >\r\n Path\r\n </span>\r\n <div\r\n class=\"h-4 w-px shrink-0 bg-slate-200 dark:bg-slate-700\"\r\n ></div>\r\n <span class=\"text-xs font-semibold text-slate-600\">\r\n {{ scope.label }}\r\n </span>\r\n </div>\r\n @if (getScopeBaseContexts(scope.key).length === 0) {\r\n <span class=\"text-xs italic text-slate-400\">\r\n No context available\r\n </span>\r\n } @else {\r\n <div class=\"flex flex-col gap-1.5\">\r\n @for (\r\n segment of getScopePath(scope.key);\r\n track $index;\r\n let segmentIndex = $index;\r\n let isLast = $last\r\n ) {\r\n <div\r\n class=\"flex items-center gap-1 py-0.5 px-2 border border-gray-200 rounded bg-white dark:bg-slate-800 dark:border-slate-600\"\r\n >\r\n <div\r\n class=\"flex-1 min-w-0 text-xs font-medium text-slate-600 dark:text-slate-300 truncate cursor-pointer\"\r\n (click)=\"segmentPopover.toggle($event)\"\r\n >\r\n {{\r\n getScopeSegmentLabel(\r\n scope.key,\r\n segmentIndex\r\n )\r\n }}\r\n </div>\r\n <mt-button\r\n type=\"button\"\r\n icon=\"arrow.chevron-down\"\r\n [outlined]=\"true\"\r\n size=\"small\"\r\n severity=\"secondary\"\r\n styleClass=\"!p-0.5 !min-w-0\"\r\n (onClick)=\"segmentPopover.toggle($event)\"\r\n ></mt-button>\r\n @if (isLast && canRemoveScopePath(scope.key)) {\r\n <mt-button\r\n type=\"button\"\r\n icon=\"general.trash-01\"\r\n [outlined]=\"true\"\r\n size=\"small\"\r\n severity=\"danger\"\r\n styleClass=\"!p-0.5 !min-w-0\"\r\n (onClick)=\"removeScopePath(scope.key)\"\r\n ></mt-button>\r\n }\r\n </div>\r\n <p-popover\r\n #segmentPopover\r\n [style]=\"{ width: 'max-content' }\"\r\n [dismissable]=\"true\"\r\n >\r\n <div class=\"p-2\">\r\n <div\r\n class=\"mb-2 text-xs font-semibold uppercase text-slate-400\"\r\n >\r\n Select {{ getTokenLabel(segment.token) }}\r\n </div>\r\n @if (\r\n getScopeSegmentOptions(\r\n scope.key,\r\n segmentIndex\r\n ).length === 0\r\n ) {\r\n <div\r\n class=\"text-xs font-medium text-slate-400\"\r\n >\r\n No options\r\n </div>\r\n } @else {\r\n <div class=\"flex flex-col gap-1\">\r\n @for (\r\n option of getScopeSegmentOptions(\r\n scope.key,\r\n segmentIndex\r\n );\r\n track option.contextKey\r\n ) {\r\n <mt-button\r\n type=\"button\"\r\n [label]=\"option.label\"\r\n [icon]=\"\r\n option.contextKey === segment.value\r\n ? 'general.check'\r\n : 'general.minus'\r\n \"\r\n [iconPos]=\"'end'\"\r\n size=\"small\"\r\n severity=\"secondary\"\r\n styleClass=\"w-full justify-start rounded-md px-2.5 py-1.5 text-left text-xs font-medium transition-colors text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-700\"\r\n (onClick)=\"\r\n onSetScopeSegmentValue(\r\n scope.key,\r\n segmentIndex,\r\n option.contextKey,\r\n segmentPopover\r\n )\r\n \"\r\n ></mt-button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n </p-popover>\r\n }\r\n @if (canAddNextScopeSegment(scope.key)) {\r\n <div class=\"flex items-center gap-2\">\r\n <mt-button\r\n type=\"button\"\r\n icon=\"general.plus\"\r\n size=\"small\"\r\n severity=\"primary\"\r\n styleClass=\"flex size-7 shrink-0 items-center justify-center rounded-md bg-primary text-xs font-bold text-white hover:opacity-90\"\r\n (onClick)=\"nextContextPopover.toggle($event)\"\r\n ></mt-button>\r\n <span class=\"text-xs text-slate-400\">\r\n Add segment\r\n </span>\r\n </div>\r\n }\r\n <!-- Popover OUTSIDE the @if block like formula-builder -->\r\n <p-popover\r\n #nextContextPopover\r\n [style]=\"{ width: 'max-content' }\"\r\n [dismissable]=\"true\"\r\n appendTo=\"body\"\r\n >\r\n <div class=\"p-2\">\r\n <div\r\n class=\"mb-2 text-xs font-semibold uppercase text-slate-400\"\r\n >\r\n Add Segment\r\n </div>\r\n <div class=\"flex flex-col gap-1\">\r\n @for (\r\n token of getScopeNextTokens(scope.key);\r\n track token\r\n ) {\r\n <mt-button\r\n type=\"button\"\r\n [label]=\"getTokenLabel(token)\"\r\n size=\"small\"\r\n severity=\"secondary\"\r\n styleClass=\"w-full justify-start rounded-md px-2.5 py-1.5 text-left text-xs font-medium transition-colors text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-700\"\r\n (onClick)=\"\r\n onAddScopeSegment(\r\n scope.key,\r\n token,\r\n nextContextPopover\r\n )\r\n \"\r\n ></mt-button>\r\n }\r\n </div>\r\n </div>\r\n </p-popover>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Node List -->\r\n <div\r\n class=\"space-y-4 px-4 pb-4 [&_.cdk-drag-placeholder]:hidden flex-1 overflow-y-auto\"\r\n [id]=\"'toolbox-' + scope.key\"\r\n cdkDropList\r\n cdkDropListSortingDisabled\r\n [cdkDropListData]=\"getFilteredProperties(scope.key)\"\r\n [cdkDropListEnterPredicate]=\"noReturnPredicate\"\r\n >\r\n @if (scope.key === \"Current\") {\r\n <!-- Search Field (Sticky) -->\r\n <div\r\n class=\"sticky top-0 bg-surface-0 mb-0 py-3 pb-2 mb-1\"\r\n >\r\n <mt-text-field\r\n [placeholder]=\"t('search-properties')\"\r\n [(ngModel)]=\"searchQuery\"\r\n icon=\"general.search-lg\"\r\n />\r\n </div>\r\n } @else {\r\n <div class=\"sticky top-0 bg-surface-0 py-3\">\r\n @if (isScopePropertiesLoading(scope.key)) {\r\n <p-skeleton height=\"2.5rem\" styleClass=\"rounded-lg\" />\r\n } @else {\r\n <mt-select-field\r\n class=\"w-full\"\r\n [label]=\"''\"\r\n [filter]=\"true\"\r\n [hasPlaceholderPrefix]=\"false\"\r\n placeholder=\"Select...\"\r\n [ngModel]=\"getScopeSelectedPropertyKey(scope.key)\"\r\n (ngModelChange)=\"\r\n setScopeSelectedProperty(scope.key, $event)\r\n \"\r\n [options]=\"getScopePropertyOptions(scope.key)\"\r\n optionLabel=\"name\"\r\n optionValue=\"key\"\r\n [size]=\"'small'\"\r\n />\r\n }\r\n </div>\r\n }\r\n\r\n @if (isScopePropertiesLoading(scope.key)) {\r\n <div class=\"space-y-4\">\r\n @for (i of [1, 2, 3, 4]; track i) {\r\n <p-skeleton height=\"3rem\" styleClass=\"rounded-lg\" />\r\n }\r\n </div>\r\n } @else {\r\n @for (\r\n node of getFilteredProperties(scope.key);\r\n track node.key\r\n ) {\r\n <div\r\n cdkDrag\r\n [cdkDragData]=\"node\"\r\n class=\"group cursor-move select-none flex items-center gap-3 py-3 px-3 rounded-lg border border-dashed border-surface-300 hover:bg-emphasis dark:border-surface-500 transition-colors\"\r\n >\r\n <div\r\n *cdkDragPlaceholder\r\n class=\"col-span-12 min-h-27 w-full rounded-2xl bg-black/10 z-1\"\r\n ></div>\r\n <span\r\n class=\"flex-1 text-start text-base font-medium\"\r\n >{{ getPropertyLabel(node) }}</span\r\n >\r\n\r\n <mt-icon\r\n class=\"text-lg\"\r\n icon=\"general.menu-05\"\r\n ></mt-icon>\r\n </div>\r\n }\r\n\r\n @if (getFilteredProperties(scope.key).length === 0) {\r\n <div class=\"py-8 text-center text-muted-color\">\r\n <p class=\"text-sm\">\r\n @if (scope.key === \"Current\" && searchQuery()) {\r\n {{ t(\"no-data-found\") }}\r\n } @else if (scope.key !== \"Current\") {\r\n Select a property\r\n } @else if (\r\n getScopeProperties(scope.key).length === 0\r\n ) {\r\n No properties available\r\n } @else {\r\n All items are in use\r\n }\r\n </p>\r\n </div>\r\n }\r\n }\r\n </div>\r\n </p-tabpanel>\r\n }\r\n </p-tabpanels>\r\n </p-tabs>\r\n }\r\n </ng-template>\r\n </mt-card>\r\n\r\n <!-- Main Canvas Area -->\r\n <div class=\"flex flex-1 gap-4 h-full overflow-y-auto\">\r\n <div class=\"flex flex-col w-2/3 gap-4 h-full\">\r\n <mt-card>\r\n <ng-template #headless>\r\n <div class=\"p-4 flex items-center gap-3\">\r\n <mt-button\r\n icon=\"layout.layout-top\"\r\n [label]=\"t('add-section')\"\r\n variant=\"outlined\"\r\n severity=\"primary\"\r\n (onClick)=\"addSection()\"\r\n [disabled]=\"isLoading()\"\r\n ></mt-button>\r\n <mt-button\r\n icon=\"general.eye\"\r\n [label]=\"t('preview')\"\r\n variant=\"outlined\"\r\n severity=\"primary\"\r\n (onClick)=\"openPreview()\"\r\n [disabled]=\"isLoading()\"\r\n ></mt-button>\r\n <mt-button\r\n [label]=\"t('validation-rules')\"\r\n variant=\"outlined\"\r\n severity=\"primary\"\r\n (onClick)=\"openValidationRules()\"\r\n [disabled]=\"isLoading()\"\r\n ></mt-button>\r\n <mt-button\r\n icon=\"finance.credit-card-plus\"\r\n [label]=\"t('reset')\"\r\n variant=\"outlined\"\r\n severity=\"primary\"\r\n (onClick)=\"resetFormConfiguration()\"\r\n [disabled]=\"isLoading()\"\r\n ></mt-button>\r\n </div>\r\n </ng-template>\r\n </mt-card>\r\n\r\n @if (isLoading()) {\r\n <!-- Form Loading Skeleton -->\r\n @for (i of [1, 2]; track i) {\r\n <mt-card>\r\n <ng-template #headless>\r\n <div class=\"p-4 space-y-4\">\r\n <!-- Section header skeleton -->\r\n <div class=\"flex items-center justify-between\">\r\n <p-skeleton width=\"10rem\" height=\"1.5rem\" />\r\n <div class=\"flex gap-2\">\r\n <p-skeleton width=\"2rem\" height=\"2rem\" shape=\"circle\" />\r\n <p-skeleton width=\"2rem\" height=\"2rem\" shape=\"circle\" />\r\n </div>\r\n </div>\r\n <!-- Fields skeleton -->\r\n <div class=\"grid grid-cols-12 gap-4\">\r\n <div class=\"col-span-6\">\r\n <p-skeleton height=\"4rem\" styleClass=\"rounded-lg\" />\r\n </div>\r\n <div class=\"col-span-6\">\r\n <p-skeleton height=\"4rem\" styleClass=\"rounded-lg\" />\r\n </div>\r\n <div class=\"col-span-12\">\r\n <p-skeleton height=\"4rem\" styleClass=\"rounded-lg\" />\r\n </div>\r\n </div>\r\n </div>\r\n </ng-template>\r\n </mt-card>\r\n }\r\n } @else {\r\n @for (section of enrichedSections(); track section.id) {\r\n <mt-fb-section\r\n [section]=\"section\"\r\n [sectionsCount]=\"enrichedSections().length\"\r\n [allSections]=\"enrichedSections()\"\r\n (onFieldDrop)=\"drop($event)\"\r\n >\r\n </mt-fb-section>\r\n } @empty {\r\n <mt-card>\r\n <div class=\"h-27 p-4\">\r\n <div\r\n class=\"flex justify-center items-center gap-4 h-full border-1 border-primary rounded-xl bg-primary-50 text-primary\"\r\n >\r\n <span>{{ t(\"no-section\") }}</span>\r\n </div>\r\n </div>\r\n </mt-card>\r\n }\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n</ng-container>\r\n", styles: [".cdk-drag{cursor:grab}.cdk-drag:active,.cdk-drag-preview{cursor:grabbing}.cdk-drag-placeholder{opacity:.5}.cdk-drop-list-dragging .cdk-drag{cursor:grabbing}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TabsModule }, { kind: "component", type: i2.Tabs, selector: "p-tabs", inputs: ["value", "scrollable", "lazy", "selectOnFocus", "showNavigators", "tabindex"], outputs: ["valueChange"] }, { kind: "component", type: i2.TabPanels, selector: "p-tabpanels" }, { kind: "component", type: i2.TabPanel, selector: "p-tabpanel", inputs: ["lazy", "value"], outputs: ["valueChange"] }, { kind: "component", type: i2.TabList, selector: "p-tablist" }, { kind: "component", type: i2.Tab, selector: "p-tab", inputs: ["value", "disabled"], outputs: ["valueChange"] }, { kind: "ngmodule", type: SkeletonModule }, { kind: "component", type: i3.Skeleton, selector: "p-skeleton", inputs: ["styleClass", "shape", "animation", "borderRadius", "size", "width", "height"] }, { kind: "ngmodule", type: PopoverModule }, { kind: "component", type: i4.Popover, selector: "p-popover", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions", "motionOptions"], outputs: ["onShow", "onHide"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: Card, selector: "mt-card", inputs: ["class", "title", "paddingless"] }, { kind: "component", type: TextField, selector: "mt-text-field", inputs: ["field", "hint", "label", "placeholder", "class", "type", "readonly", "pInputs", "required", "icon", "iconPosition"] }, { kind: "component", type: SelectField, selector: "mt-select-field", inputs: ["field", "label", "placeholder", "hasPlaceholderPrefix", "class", "readonly", "pInputs", "options", "optionValue", "optionLabel", "filter", "filterBy", "dataKey", "showClear", "clearAfterSelect", "required", "group", "size", "optionGroupLabel", "optionGroupChildren", "loading"], outputs: ["onChange"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "component", type: FBSection, selector: "mt-fb-section", inputs: ["section", "sectionsCount", "allSections"], outputs: ["onFieldDrop"] }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i5.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i5.CdkDropListGroup, selector: "[cdkDropListGroup]", inputs: ["cdkDropListGroupDisabled"], exportAs: ["cdkDropListGroup"] }, { kind: "directive", type: i5.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i5.CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "component", type: Icon, selector: "mt-icon", inputs: ["icon"] }] });
|
|
3165
|
+
}
|
|
3166
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: FormBuilder, decorators: [{
|
|
3167
|
+
type: Component,
|
|
3168
|
+
args: [{ selector: 'mt-form-builder', standalone: true, imports: [
|
|
3169
|
+
CommonModule,
|
|
3170
|
+
FormsModule,
|
|
3171
|
+
TabsModule,
|
|
3172
|
+
SkeletonModule,
|
|
3173
|
+
PopoverModule,
|
|
3174
|
+
Button,
|
|
3175
|
+
Card,
|
|
3176
|
+
TextField,
|
|
3177
|
+
SelectField,
|
|
3178
|
+
TranslocoDirective,
|
|
3179
|
+
FBSection,
|
|
3180
|
+
DragDropModule,
|
|
3181
|
+
Icon,
|
|
3182
|
+
], template: "<ng-container *transloco=\"let t; prefix: 'formBuilder'\">\r\n <div class=\"flex gap-7 h-full w-full overflow-hidden\" cdkDropListGroup>\r\n <!-- Properties Sidebar -->\r\n <mt-card class=\"w-1/5 h-full flex flex-col overflow-hidden\">\r\n <ng-template #headless>\r\n <!-- Header -->\r\n <h3 class=\"text-xl font-semibold px-4 pt-5\">\r\n {{ t(\"form-elements\") }}\r\n </h3>\r\n\r\n @if (isContextLoading()) {\r\n <!-- Properties Loading Skeleton -->\r\n <div class=\"flex gap-4 py-3 px-5 mt-4\">\r\n <p-skeleton height=\"2rem\" styleClass=\"rounded-lg\" />\r\n <p-skeleton height=\"2rem\" styleClass=\"rounded-lg\" />\r\n </div>\r\n <div class=\"py-4 px-5 space-y-5\">\r\n @for (i of [1, 2, 3, 4, 5, 6]; track i) {\r\n <p-skeleton height=\"3rem\" styleClass=\"rounded-lg\" />\r\n }\r\n </div>\r\n } @else if (scopeOptions().length === 0) {\r\n <!-- No Properties State -->\r\n <div class=\"flex-1 flex items-center justify-center p-4\">\r\n <p class=\"text-sm font-semibold text-gray-500\">\r\n {{ t(\"no-data-found\") }}\r\n </p>\r\n </div>\r\n } @else {\r\n <!-- Scope Tabs using PrimeNG -->\r\n <p-tabs\r\n [value]=\"activeScope()\"\r\n (valueChange)=\"onScopeChange($event)\"\r\n styleClass=\"structure-tabs\"\r\n class=\"flex flex-1 flex-col min-h-0\"\r\n >\r\n <p-tablist class=\"shrink-0\">\r\n @for (scope of scopeOptions(); track scope.key) {\r\n <p-tab [value]=\"scope.key\">{{ scope.label }}</p-tab>\r\n }\r\n </p-tablist>\r\n <p-tabpanels\r\n class=\"!bg-transparent !p-0 !pb-3 flex-1 overflow-hidden\"\r\n >\r\n @for (scope of scopeOptions(); track scope.key) {\r\n <p-tabpanel [value]=\"scope.key\" class=\"h-full flex flex-col\">\r\n @if (scope.key !== \"Current\") {\r\n <div class=\"px-4 pt-3 pb-2\">\r\n <div\r\n class=\"flex flex-col gap-3 rounded-lg bg-slate-50 px-3 py-2 dark:bg-slate-800/50\"\r\n >\r\n <div class=\"flex items-center gap-2\">\r\n <span\r\n class=\"shrink-0 text-xs font-semibold uppercase tracking-wider text-slate-400\"\r\n >\r\n Path\r\n </span>\r\n <div\r\n class=\"h-4 w-px shrink-0 bg-slate-200 dark:bg-slate-700\"\r\n ></div>\r\n <span class=\"text-xs font-semibold text-slate-600\">\r\n {{ scope.label }}\r\n </span>\r\n </div>\r\n @if (getScopeBaseContexts(scope.key).length === 0) {\r\n <span class=\"text-xs italic text-slate-400\">\r\n No context available\r\n </span>\r\n } @else {\r\n <div class=\"flex flex-col gap-1.5\">\r\n @for (\r\n segment of getScopePath(scope.key);\r\n track $index;\r\n let segmentIndex = $index;\r\n let isLast = $last\r\n ) {\r\n <div\r\n class=\"flex items-center gap-1 py-0.5 px-2 border border-gray-200 rounded bg-white dark:bg-slate-800 dark:border-slate-600\"\r\n >\r\n <div\r\n class=\"flex-1 min-w-0 text-xs font-medium text-slate-600 dark:text-slate-300 truncate cursor-pointer\"\r\n (click)=\"segmentPopover.toggle($event)\"\r\n >\r\n {{\r\n getScopeSegmentLabel(\r\n scope.key,\r\n segmentIndex\r\n )\r\n }}\r\n </div>\r\n <mt-button\r\n type=\"button\"\r\n icon=\"arrow.chevron-down\"\r\n [outlined]=\"true\"\r\n size=\"small\"\r\n severity=\"secondary\"\r\n styleClass=\"!p-0.5 !min-w-0\"\r\n (onClick)=\"segmentPopover.toggle($event)\"\r\n ></mt-button>\r\n @if (isLast && canRemoveScopePath(scope.key)) {\r\n <mt-button\r\n type=\"button\"\r\n icon=\"general.trash-01\"\r\n [outlined]=\"true\"\r\n size=\"small\"\r\n severity=\"danger\"\r\n styleClass=\"!p-0.5 !min-w-0\"\r\n (onClick)=\"removeScopePath(scope.key)\"\r\n ></mt-button>\r\n }\r\n </div>\r\n <p-popover\r\n #segmentPopover\r\n [style]=\"{ width: 'max-content' }\"\r\n [dismissable]=\"true\"\r\n >\r\n <div class=\"p-2\">\r\n <div\r\n class=\"mb-2 text-xs font-semibold uppercase text-slate-400\"\r\n >\r\n Select {{ getTokenLabel(segment.token) }}\r\n </div>\r\n @if (\r\n getScopeSegmentOptions(\r\n scope.key,\r\n segmentIndex\r\n ).length === 0\r\n ) {\r\n <div\r\n class=\"text-xs font-medium text-slate-400\"\r\n >\r\n No options\r\n </div>\r\n } @else {\r\n <div class=\"flex flex-col gap-1\">\r\n @for (\r\n option of getScopeSegmentOptions(\r\n scope.key,\r\n segmentIndex\r\n );\r\n track option.contextKey\r\n ) {\r\n <mt-button\r\n type=\"button\"\r\n [label]=\"option.label\"\r\n [icon]=\"\r\n option.contextKey === segment.value\r\n ? 'general.check'\r\n : 'general.minus'\r\n \"\r\n [iconPos]=\"'end'\"\r\n size=\"small\"\r\n severity=\"secondary\"\r\n styleClass=\"w-full justify-start rounded-md px-2.5 py-1.5 text-left text-xs font-medium transition-colors text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-700\"\r\n (onClick)=\"\r\n onSetScopeSegmentValue(\r\n scope.key,\r\n segmentIndex,\r\n option.contextKey,\r\n segmentPopover\r\n )\r\n \"\r\n ></mt-button>\r\n }\r\n </div>\r\n }\r\n </div>\r\n </p-popover>\r\n }\r\n @if (canAddNextScopeSegment(scope.key)) {\r\n <div class=\"flex items-center gap-2\">\r\n <mt-button\r\n type=\"button\"\r\n icon=\"general.plus\"\r\n size=\"small\"\r\n severity=\"primary\"\r\n styleClass=\"flex size-7 shrink-0 items-center justify-center rounded-md bg-primary text-xs font-bold text-white hover:opacity-90\"\r\n (onClick)=\"nextContextPopover.toggle($event)\"\r\n ></mt-button>\r\n <span class=\"text-xs text-slate-400\">\r\n Add segment\r\n </span>\r\n </div>\r\n }\r\n <!-- Popover OUTSIDE the @if block like formula-builder -->\r\n <p-popover\r\n #nextContextPopover\r\n [style]=\"{ width: 'max-content' }\"\r\n [dismissable]=\"true\"\r\n appendTo=\"body\"\r\n >\r\n <div class=\"p-2\">\r\n <div\r\n class=\"mb-2 text-xs font-semibold uppercase text-slate-400\"\r\n >\r\n Add Segment\r\n </div>\r\n <div class=\"flex flex-col gap-1\">\r\n @for (\r\n token of getScopeNextTokens(scope.key);\r\n track token\r\n ) {\r\n <mt-button\r\n type=\"button\"\r\n [label]=\"getTokenLabel(token)\"\r\n size=\"small\"\r\n severity=\"secondary\"\r\n styleClass=\"w-full justify-start rounded-md px-2.5 py-1.5 text-left text-xs font-medium transition-colors text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-700\"\r\n (onClick)=\"\r\n onAddScopeSegment(\r\n scope.key,\r\n token,\r\n nextContextPopover\r\n )\r\n \"\r\n ></mt-button>\r\n }\r\n </div>\r\n </div>\r\n </p-popover>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n }\r\n\r\n <!-- Node List -->\r\n <div\r\n class=\"space-y-4 px-4 pb-4 [&_.cdk-drag-placeholder]:hidden flex-1 overflow-y-auto\"\r\n [id]=\"'toolbox-' + scope.key\"\r\n cdkDropList\r\n cdkDropListSortingDisabled\r\n [cdkDropListData]=\"getFilteredProperties(scope.key)\"\r\n [cdkDropListEnterPredicate]=\"noReturnPredicate\"\r\n >\r\n @if (scope.key === \"Current\") {\r\n <!-- Search Field (Sticky) -->\r\n <div\r\n class=\"sticky top-0 bg-surface-0 mb-0 py-3 pb-2 mb-1\"\r\n >\r\n <mt-text-field\r\n [placeholder]=\"t('search-properties')\"\r\n [(ngModel)]=\"searchQuery\"\r\n icon=\"general.search-lg\"\r\n />\r\n </div>\r\n } @else {\r\n <div class=\"sticky top-0 bg-surface-0 py-3\">\r\n @if (isScopePropertiesLoading(scope.key)) {\r\n <p-skeleton height=\"2.5rem\" styleClass=\"rounded-lg\" />\r\n } @else {\r\n <mt-select-field\r\n class=\"w-full\"\r\n [label]=\"''\"\r\n [filter]=\"true\"\r\n [hasPlaceholderPrefix]=\"false\"\r\n placeholder=\"Select...\"\r\n [ngModel]=\"getScopeSelectedPropertyKey(scope.key)\"\r\n (ngModelChange)=\"\r\n setScopeSelectedProperty(scope.key, $event)\r\n \"\r\n [options]=\"getScopePropertyOptions(scope.key)\"\r\n optionLabel=\"name\"\r\n optionValue=\"key\"\r\n [size]=\"'small'\"\r\n />\r\n }\r\n </div>\r\n }\r\n\r\n @if (isScopePropertiesLoading(scope.key)) {\r\n <div class=\"space-y-4\">\r\n @for (i of [1, 2, 3, 4]; track i) {\r\n <p-skeleton height=\"3rem\" styleClass=\"rounded-lg\" />\r\n }\r\n </div>\r\n } @else {\r\n @for (\r\n node of getFilteredProperties(scope.key);\r\n track node.key\r\n ) {\r\n <div\r\n cdkDrag\r\n [cdkDragData]=\"node\"\r\n class=\"group cursor-move select-none flex items-center gap-3 py-3 px-3 rounded-lg border border-dashed border-surface-300 hover:bg-emphasis dark:border-surface-500 transition-colors\"\r\n >\r\n <div\r\n *cdkDragPlaceholder\r\n class=\"col-span-12 min-h-27 w-full rounded-2xl bg-black/10 z-1\"\r\n ></div>\r\n <span\r\n class=\"flex-1 text-start text-base font-medium\"\r\n >{{ getPropertyLabel(node) }}</span\r\n >\r\n\r\n <mt-icon\r\n class=\"text-lg\"\r\n icon=\"general.menu-05\"\r\n ></mt-icon>\r\n </div>\r\n }\r\n\r\n @if (getFilteredProperties(scope.key).length === 0) {\r\n <div class=\"py-8 text-center text-muted-color\">\r\n <p class=\"text-sm\">\r\n @if (scope.key === \"Current\" && searchQuery()) {\r\n {{ t(\"no-data-found\") }}\r\n } @else if (scope.key !== \"Current\") {\r\n Select a property\r\n } @else if (\r\n getScopeProperties(scope.key).length === 0\r\n ) {\r\n No properties available\r\n } @else {\r\n All items are in use\r\n }\r\n </p>\r\n </div>\r\n }\r\n }\r\n </div>\r\n </p-tabpanel>\r\n }\r\n </p-tabpanels>\r\n </p-tabs>\r\n }\r\n </ng-template>\r\n </mt-card>\r\n\r\n <!-- Main Canvas Area -->\r\n <div class=\"flex flex-1 gap-4 h-full overflow-y-auto\">\r\n <div class=\"flex flex-col w-2/3 gap-4 h-full\">\r\n <mt-card>\r\n <ng-template #headless>\r\n <div class=\"p-4 flex items-center gap-3\">\r\n <mt-button\r\n icon=\"layout.layout-top\"\r\n [label]=\"t('add-section')\"\r\n variant=\"outlined\"\r\n severity=\"primary\"\r\n (onClick)=\"addSection()\"\r\n [disabled]=\"isLoading()\"\r\n ></mt-button>\r\n <mt-button\r\n icon=\"general.eye\"\r\n [label]=\"t('preview')\"\r\n variant=\"outlined\"\r\n severity=\"primary\"\r\n (onClick)=\"openPreview()\"\r\n [disabled]=\"isLoading()\"\r\n ></mt-button>\r\n <mt-button\r\n [label]=\"t('validation-rules')\"\r\n variant=\"outlined\"\r\n severity=\"primary\"\r\n (onClick)=\"openValidationRules()\"\r\n [disabled]=\"isLoading()\"\r\n ></mt-button>\r\n <mt-button\r\n icon=\"finance.credit-card-plus\"\r\n [label]=\"t('reset')\"\r\n variant=\"outlined\"\r\n severity=\"primary\"\r\n (onClick)=\"resetFormConfiguration()\"\r\n [disabled]=\"isLoading()\"\r\n ></mt-button>\r\n </div>\r\n </ng-template>\r\n </mt-card>\r\n\r\n @if (isLoading()) {\r\n <!-- Form Loading Skeleton -->\r\n @for (i of [1, 2]; track i) {\r\n <mt-card>\r\n <ng-template #headless>\r\n <div class=\"p-4 space-y-4\">\r\n <!-- Section header skeleton -->\r\n <div class=\"flex items-center justify-between\">\r\n <p-skeleton width=\"10rem\" height=\"1.5rem\" />\r\n <div class=\"flex gap-2\">\r\n <p-skeleton width=\"2rem\" height=\"2rem\" shape=\"circle\" />\r\n <p-skeleton width=\"2rem\" height=\"2rem\" shape=\"circle\" />\r\n </div>\r\n </div>\r\n <!-- Fields skeleton -->\r\n <div class=\"grid grid-cols-12 gap-4\">\r\n <div class=\"col-span-6\">\r\n <p-skeleton height=\"4rem\" styleClass=\"rounded-lg\" />\r\n </div>\r\n <div class=\"col-span-6\">\r\n <p-skeleton height=\"4rem\" styleClass=\"rounded-lg\" />\r\n </div>\r\n <div class=\"col-span-12\">\r\n <p-skeleton height=\"4rem\" styleClass=\"rounded-lg\" />\r\n </div>\r\n </div>\r\n </div>\r\n </ng-template>\r\n </mt-card>\r\n }\r\n } @else {\r\n @for (section of enrichedSections(); track section.id) {\r\n <mt-fb-section\r\n [section]=\"section\"\r\n [sectionsCount]=\"enrichedSections().length\"\r\n [allSections]=\"enrichedSections()\"\r\n (onFieldDrop)=\"drop($event)\"\r\n >\r\n </mt-fb-section>\r\n } @empty {\r\n <mt-card>\r\n <div class=\"h-27 p-4\">\r\n <div\r\n class=\"flex justify-center items-center gap-4 h-full border-1 border-primary rounded-xl bg-primary-50 text-primary\"\r\n >\r\n <span>{{ t(\"no-section\") }}</span>\r\n </div>\r\n </div>\r\n </mt-card>\r\n }\r\n }\r\n </div>\r\n </div>\r\n </div>\r\n</ng-container>\r\n", styles: [".cdk-drag{cursor:grab}.cdk-drag:active,.cdk-drag-preview{cursor:grabbing}.cdk-drag-placeholder{opacity:.5}.cdk-drop-list-dragging .cdk-drag{cursor:grabbing}\n"] }]
|
|
3183
|
+
}], ctorParameters: () => [], propDecorators: { popovers: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => Popover), { isSignal: true }] }] } });
|
|
1918
3184
|
|
|
1919
3185
|
/*
|
|
1920
3186
|
* Public API Surface of form-builder
|
|
@@ -1924,5 +3190,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImpor
|
|
|
1924
3190
|
* Generated bundle index. Do not edit.
|
|
1925
3191
|
*/
|
|
1926
3192
|
|
|
1927
|
-
export { AddField, AddSection, DeleteField, DeleteSection, FormBuilder, FormBuilderActionKey, FormBuilderFacade, FormBuilderState, GetFormConfiguration, MoveField, ReorderFields, ResetFormBuilderState, ResetFormConfiguration, SetModuleInfo, SetProperties, UpdateField, UpdateSection };
|
|
3193
|
+
export { AddField, AddSection, AddValidation, DeleteField, DeleteSection, DeleteValidation, FormBuilder, FormBuilderActionKey, FormBuilderFacade, FormBuilderState, GetFormConfiguration, MoveField, ReorderFields, ResetFormBuilderState, ResetFormConfiguration, SetModuleInfo, SetProperties, ToggleValidationActive, UpdateField, UpdateSection, UpdateValidation };
|
|
1928
3194
|
//# sourceMappingURL=masterteam-form-builder.mjs.map
|