@masterteam/governance 0.0.7 → 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,21 +1,22 @@
1
1
  import { CommonModule } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
- import { inject, Injectable, computed, input, effect, Component, viewChild, linkedSignal, signal } from '@angular/core';
3
+ import { inject, Injectable, computed, input, viewChild, effect, untracked, ChangeDetectionStrategy, Component, linkedSignal, signal } from '@angular/core';
4
4
  import { Table } from '@masterteam/components/table';
5
- import * as i1 from 'primeng/skeleton';
5
+ import * as i2 from 'primeng/skeleton';
6
6
  import { SkeletonModule } from 'primeng/skeleton';
7
7
  import { Action, Selector, State, Store, select } from '@ngxs/store';
8
8
  import { HttpClient } from '@angular/common/http';
9
- import { CrudStateBase, handleApiRequest, TextFieldConfig, NumberFieldConfig, TextareaFieldConfig, SelectFieldConfig, ToggleFieldConfig, ValidatorConfig, DateFieldConfig, MultiSelectFieldConfig } from '@masterteam/components';
9
+ import { CrudStateBase, handleApiRequest, TextFieldConfig, TextareaFieldConfig, RadioCardsFieldConfig, SelectFieldConfig, NumberFieldConfig, MultiSelectFieldConfig, ToggleFieldConfig, ValidatorConfig, DateFieldConfig } from '@masterteam/components';
10
10
  import { TranslocoService, TranslocoDirective, TranslocoPipe } from '@jsverse/transloco';
11
11
  import { ModalService } from '@masterteam/components/modal';
12
- import * as i2 from '@angular/forms';
12
+ import * as i1 from '@angular/forms';
13
13
  import { FormControl, ReactiveFormsModule } from '@angular/forms';
14
14
  import { toSignal } from '@angular/core/rxjs-interop';
15
+ import { map, finalize } from 'rxjs';
15
16
  import { DynamicForm } from '@masterteam/forms/dynamic-form';
16
17
  import { Button } from '@masterteam/components/button';
17
- import { ModalRef, DialogService } from '@masterteam/components/dialog';
18
- import { finalize } from 'rxjs';
18
+ import { ModalRef } from '@masterteam/components/dialog';
19
+ import { ToastService } from '@masterteam/components/toast';
19
20
  import { Icon } from '@masterteam/icons';
20
21
  import { Chip } from '@masterteam/components/chip';
21
22
 
@@ -593,623 +594,1927 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImpor
593
594
  args: [{ providedIn: 'root' }]
594
595
  }] });
595
596
 
597
+ const GOVERNANCE_RULE_RECIPE_DEFINITIONS = [
598
+ {
599
+ key: 'require-module',
600
+ ruleTypeKeys: ['ModuleExists'],
601
+ labelKey: 'wizard.recipes.require-module.label',
602
+ },
603
+ {
604
+ key: 'prevent-module',
605
+ ruleTypeKeys: ['ModuleNotExists', 'ModuleDoesNotExist'],
606
+ labelKey: 'wizard.recipes.prevent-module.label',
607
+ },
608
+ {
609
+ key: 'module-max-count',
610
+ ruleTypeKeys: ['ModuleMaxCount'],
611
+ labelKey: 'wizard.recipes.module-max-count.label',
612
+ },
613
+ {
614
+ key: 'module-min-count',
615
+ ruleTypeKeys: ['ModuleMinCount'],
616
+ labelKey: 'wizard.recipes.module-min-count.label',
617
+ },
618
+ {
619
+ key: 'phase-gate-completed',
620
+ ruleTypeKeys: ['PhaseGateCompleted'],
621
+ labelKey: 'wizard.recipes.phase-gate-completed.label',
622
+ },
623
+ {
624
+ key: 'phase-gate-module-required',
625
+ ruleTypeKeys: ['PhaseGateModuleRequired'],
626
+ labelKey: 'wizard.recipes.phase-gate-module-required.label',
627
+ },
628
+ {
629
+ key: 'no-active-request',
630
+ ruleTypeKeys: ['NoActiveRequest'],
631
+ labelKey: 'wizard.recipes.no-active-request.label',
632
+ },
633
+ {
634
+ key: 'property-value',
635
+ ruleTypeKeys: ['PropertyValueEquals', 'FieldValue'],
636
+ labelKey: 'wizard.recipes.property-value.label',
637
+ },
638
+ ];
639
+ const GOVERNANCE_APPLIES_TO_OPTIONS = [
640
+ {
641
+ value: 'level',
642
+ labelKey: 'wizard.applies-to.level.label',
643
+ },
644
+ {
645
+ value: 'any-module',
646
+ labelKey: 'wizard.applies-to.any-module.label',
647
+ },
648
+ {
649
+ value: 'specific-module',
650
+ labelKey: 'wizard.applies-to.specific-module.label',
651
+ },
652
+ {
653
+ value: 'phase-gate',
654
+ labelKey: 'wizard.applies-to.phase-gate.label',
655
+ },
656
+ ];
657
+ const GOVERNANCE_ACTION_OPTIONS = [
658
+ {
659
+ value: 'create',
660
+ labelKey: 'wizard.actions.create',
661
+ },
662
+ {
663
+ value: 'update',
664
+ labelKey: 'wizard.actions.update',
665
+ },
666
+ {
667
+ value: 'delete',
668
+ labelKey: 'wizard.actions.delete',
669
+ },
670
+ ];
671
+ const GOVERNANCE_OPERATOR_OPTIONS = [
672
+ {
673
+ value: 'Equals',
674
+ labelKey: 'wizard.operators.Equals',
675
+ },
676
+ {
677
+ value: 'NotEquals',
678
+ labelKey: 'wizard.operators.NotEquals',
679
+ },
680
+ {
681
+ value: 'Contains',
682
+ labelKey: 'wizard.operators.Contains',
683
+ },
684
+ {
685
+ value: 'GreaterThan',
686
+ labelKey: 'wizard.operators.GreaterThan',
687
+ },
688
+ {
689
+ value: 'LessThan',
690
+ labelKey: 'wizard.operators.LessThan',
691
+ },
692
+ ];
693
+ const GOVERNANCE_PROPERTY_OPERATORS = [
694
+ 'Equals',
695
+ 'NotEquals',
696
+ 'Contains',
697
+ 'GreaterThan',
698
+ 'LessThan',
699
+ ];
700
+ const DEFAULT_BLOCKING_STATUSES = [
701
+ 'Pending',
702
+ 'Draft',
703
+ 'InProgress',
704
+ ];
705
+ const DEFAULT_GOVERNANCE_RULE_FORM_VALUE = {
706
+ key: '',
707
+ name: { en: '', ar: '' },
708
+ description: { en: '', ar: '' },
709
+ errorMessage: { en: '', ar: '' },
710
+ appliesTo: 'any-module',
711
+ selectedModuleKey: null,
712
+ actionKey: null,
713
+ recipeKey: null,
714
+ requiredModuleKey: null,
715
+ blockedModuleKey: null,
716
+ countedModuleKey: null,
717
+ countValue: 1,
718
+ phaseGateSchemaId: null,
719
+ requestSchemaId: null,
720
+ blockingStatuses: [],
721
+ propertyKey: null,
722
+ operator: 'Equals',
723
+ expectedValue: '',
724
+ priority: 10,
725
+ stopOnFailure: true,
726
+ isActive: true,
727
+ };
728
+ function readGovernanceRuleTypeKey(ruleType) {
729
+ return String(ruleType.key ?? ruleType.type ?? '').trim();
730
+ }
731
+ function readGovernanceRuleKey(rule) {
732
+ return String(rule.ruleTypeKey ?? '').trim();
733
+ }
734
+ function resolveRecipeDefinition(recipeKey) {
735
+ return (GOVERNANCE_RULE_RECIPE_DEFINITIONS.find((definition) => definition.key === recipeKey) ?? null);
736
+ }
737
+ function resolveRecipeByRuleTypeKey(ruleTypeKey) {
738
+ const normalized = String(ruleTypeKey ?? '').trim();
739
+ if (!normalized) {
740
+ return null;
741
+ }
742
+ return (GOVERNANCE_RULE_RECIPE_DEFINITIONS.find((definition) => definition.ruleTypeKeys.some((key) => key === normalized))?.key ?? null);
743
+ }
744
+ function findRuleTypeByRecipe(ruleTypes, recipeKey) {
745
+ const definition = resolveRecipeDefinition(recipeKey);
746
+ if (!definition) {
747
+ return null;
748
+ }
749
+ return (ruleTypes.find((ruleType) => definition.ruleTypeKeys.includes(readGovernanceRuleTypeKey(ruleType))) ?? null);
750
+ }
751
+ function findConfigurationSchemaField(ruleType, fieldKey) {
752
+ if (!ruleType) {
753
+ return null;
754
+ }
755
+ return (ruleType.configurationSchema.find((field) => field.key === fieldKey) ?? null);
756
+ }
757
+
758
+ function mapAppliesToToTargetScope(appliesTo) {
759
+ switch (appliesTo) {
760
+ case 'level':
761
+ return 'Level';
762
+ case 'phase-gate':
763
+ return 'PhaseGate';
764
+ case 'any-module':
765
+ case 'specific-module':
766
+ default:
767
+ return 'Module';
768
+ }
769
+ }
770
+ function mapActionToOperationKey(actionKey) {
771
+ switch (actionKey) {
772
+ case 'update':
773
+ return 'Update';
774
+ case 'delete':
775
+ return 'Delete';
776
+ case 'create':
777
+ default:
778
+ return 'Create';
779
+ }
780
+ }
781
+ function mapOperationKeyToAction(operationKey) {
782
+ switch (String(operationKey ?? '')
783
+ .trim()
784
+ .toLowerCase()) {
785
+ case 'create':
786
+ return 'create';
787
+ case 'update':
788
+ return 'update';
789
+ case 'delete':
790
+ return 'delete';
791
+ default:
792
+ return null;
793
+ }
794
+ }
795
+ function toNullableString(value) {
796
+ if (value === null || value === undefined) {
797
+ return null;
798
+ }
799
+ const text = String(value).trim();
800
+ return text ? text : null;
801
+ }
802
+ function toNullableNumber(value) {
803
+ if (value === null || value === undefined || value === '') {
804
+ return null;
805
+ }
806
+ const numeric = typeof value === 'number'
807
+ ? value
808
+ : Number(String(value).replace(/[^\d.-]/g, ''));
809
+ return Number.isFinite(numeric) ? numeric : null;
810
+ }
811
+ function toStringArray(value) {
812
+ if (!Array.isArray(value)) {
813
+ return [];
814
+ }
815
+ return value
816
+ .map((item) => toNullableString(item))
817
+ .filter((item) => !!item);
818
+ }
819
+ function trimBilingualValue(value) {
820
+ return {
821
+ en: value.en.trim(),
822
+ ar: value.ar.trim(),
823
+ };
824
+ }
825
+ function buildRecipeConfiguration(formValue) {
826
+ switch (formValue.recipeKey) {
827
+ case 'require-module': {
828
+ const requiredModuleKey = toNullableString(formValue.requiredModuleKey);
829
+ if (!requiredModuleKey) {
830
+ return undefined;
831
+ }
832
+ const minCount = toNullableNumber(formValue.countValue);
833
+ return minCount && minCount > 1
834
+ ? { requiredModuleKey, minCount }
835
+ : { requiredModuleKey };
836
+ }
837
+ case 'prevent-module': {
838
+ const moduleKey = toNullableString(formValue.blockedModuleKey);
839
+ return moduleKey ? { moduleKey } : undefined;
840
+ }
841
+ case 'module-max-count': {
842
+ const moduleKey = toNullableString(formValue.countedModuleKey);
843
+ const maxCount = toNullableNumber(formValue.countValue);
844
+ return moduleKey && maxCount !== null
845
+ ? { moduleKey, maxCount }
846
+ : undefined;
847
+ }
848
+ case 'module-min-count': {
849
+ const moduleKey = toNullableString(formValue.countedModuleKey);
850
+ const minCount = toNullableNumber(formValue.countValue);
851
+ return moduleKey && minCount !== null
852
+ ? { moduleKey, minCount }
853
+ : undefined;
854
+ }
855
+ case 'phase-gate-completed': {
856
+ const phaseGateSchemaId = toNullableNumber(formValue.phaseGateSchemaId);
857
+ return phaseGateSchemaId !== null ? { phaseGateSchemaId } : undefined;
858
+ }
859
+ case 'phase-gate-module-required': {
860
+ const requiredModuleKey = toNullableString(formValue.requiredModuleKey);
861
+ if (!requiredModuleKey) {
862
+ return undefined;
863
+ }
864
+ const phaseGateSchemaId = toNullableNumber(formValue.phaseGateSchemaId);
865
+ return phaseGateSchemaId !== null
866
+ ? { requiredModuleKey, phaseGateSchemaId }
867
+ : { requiredModuleKey };
868
+ }
869
+ case 'no-active-request': {
870
+ const requestSchemaId = toNullableNumber(formValue.requestSchemaId);
871
+ const blockingStatuses = toStringArray(formValue.blockingStatuses);
872
+ const configuration = {};
873
+ if (requestSchemaId !== null) {
874
+ configuration['requestSchemaId'] = requestSchemaId;
875
+ }
876
+ if (blockingStatuses.length > 0) {
877
+ configuration['blockingStatuses'] = blockingStatuses;
878
+ }
879
+ return Object.keys(configuration).length > 0 ? configuration : undefined;
880
+ }
881
+ case 'property-value': {
882
+ const propertyKey = toNullableString(formValue.propertyKey);
883
+ const operator = toNullableString(formValue.operator);
884
+ const expectedValue = toNullableString(formValue.expectedValue);
885
+ return propertyKey && operator && expectedValue !== null
886
+ ? { propertyKey, operator, expectedValue }
887
+ : undefined;
888
+ }
889
+ default:
890
+ return undefined;
891
+ }
892
+ }
893
+ function buildCreatePayload(formValue, ruleType) {
894
+ const name = trimBilingualValue(formValue.name);
895
+ const description = trimBilingualValue(formValue.description);
896
+ const errorMessage = trimBilingualValue(formValue.errorMessage);
897
+ return {
898
+ key: formValue.key.trim(),
899
+ name,
900
+ description: description.en || description.ar ? description : undefined,
901
+ errorMessage,
902
+ ruleType: readGovernanceRuleTypeKey(ruleType),
903
+ targetScope: mapAppliesToToTargetScope(formValue.appliesTo),
904
+ targetModuleKey: formValue.appliesTo === 'specific-module'
905
+ ? (toNullableString(formValue.selectedModuleKey) ?? undefined)
906
+ : undefined,
907
+ targetOperationKey: formValue.actionKey !== null
908
+ ? mapActionToOperationKey(formValue.actionKey)
909
+ : undefined,
910
+ configuration: buildRecipeConfiguration(formValue),
911
+ priority: toNullableNumber(formValue.priority) ?? 10,
912
+ stopOnFailure: formValue.stopOnFailure ?? true,
913
+ };
914
+ }
915
+ function buildUpdatePayload(formValue, ruleType) {
916
+ return {
917
+ ...buildCreatePayload(formValue, ruleType),
918
+ isActive: formValue.isActive ?? true,
919
+ };
920
+ }
921
+ function mapRuleToWizardFormValue(rule) {
922
+ const recipeKey = resolveRecipeByRuleTypeKey(readGovernanceRuleKey(rule));
923
+ if (!recipeKey) {
924
+ return null;
925
+ }
926
+ const appliesTo = resolveAppliesTo(rule, recipeKey);
927
+ const actionKey = mapOperationKeyToAction(rule.targetOperationKey);
928
+ if (!actionKey) {
929
+ return null;
930
+ }
931
+ const configuration = rule.configuration ?? {};
932
+ const formValue = {
933
+ ...DEFAULT_GOVERNANCE_RULE_FORM_VALUE,
934
+ key: rule.key ?? '',
935
+ name: {
936
+ en: rule.name?.en ?? '',
937
+ ar: rule.name?.ar ?? '',
938
+ },
939
+ description: {
940
+ en: rule.description?.en ?? '',
941
+ ar: rule.description?.ar ?? '',
942
+ },
943
+ errorMessage: {
944
+ en: rule.errorMessage?.en ?? '',
945
+ ar: rule.errorMessage?.ar ?? '',
946
+ },
947
+ appliesTo,
948
+ selectedModuleKey: appliesTo === 'specific-module'
949
+ ? toNullableString(rule.targetModuleKey)
950
+ : null,
951
+ actionKey,
952
+ recipeKey,
953
+ priority: rule.priority ?? DEFAULT_GOVERNANCE_RULE_FORM_VALUE.priority,
954
+ stopOnFailure: rule.stopOnFailure ?? true,
955
+ isActive: rule.isActive ?? true,
956
+ requiredModuleKey: toNullableString(configuration['requiredModuleKey'] ?? configuration['moduleKey']),
957
+ blockedModuleKey: toNullableString(configuration['moduleKey']),
958
+ countedModuleKey: toNullableString(configuration['moduleKey']),
959
+ countValue: toNullableNumber(configuration['maxCount'] ??
960
+ configuration['minCount'] ??
961
+ DEFAULT_GOVERNANCE_RULE_FORM_VALUE.countValue) ?? DEFAULT_GOVERNANCE_RULE_FORM_VALUE.countValue,
962
+ phaseGateSchemaId: toNullableString(configuration['phaseGateSchemaId']),
963
+ requestSchemaId: toNullableString(configuration['requestSchemaId']),
964
+ blockingStatuses: toStringArray(configuration['blockingStatuses']),
965
+ propertyKey: toNullableString(configuration['propertyKey'] ?? configuration['fieldKey']),
966
+ operator: toNullableString(configuration['operator']) ??
967
+ DEFAULT_GOVERNANCE_RULE_FORM_VALUE.operator,
968
+ expectedValue: toNullableString(configuration['expectedValue'] ?? configuration['value']) ?? '',
969
+ };
970
+ if (recipeKey === 'prevent-module') {
971
+ formValue.blockedModuleKey = toNullableString(configuration['moduleKey']);
972
+ formValue.requiredModuleKey = null;
973
+ }
974
+ if (recipeKey === 'phase-gate-completed') {
975
+ formValue.requiredModuleKey = null;
976
+ }
977
+ if (recipeKey === 'no-active-request') {
978
+ formValue.requiredModuleKey = null;
979
+ formValue.blockedModuleKey = null;
980
+ formValue.countedModuleKey = null;
981
+ }
982
+ if (recipeKey === 'property-value') {
983
+ formValue.requiredModuleKey = null;
984
+ formValue.blockedModuleKey = null;
985
+ formValue.countedModuleKey = null;
986
+ }
987
+ return formValue;
988
+ }
989
+ function resolveAppliesTo(rule, recipeKey) {
990
+ const scope = String(rule.targetScope ?? '').trim();
991
+ const targetModuleKey = String(rule.targetModuleKey ?? '').trim();
992
+ if (scope === 'Level') {
993
+ return 'level';
994
+ }
995
+ if (scope === 'PhaseGate' ||
996
+ ((recipeKey === 'phase-gate-completed' ||
997
+ recipeKey === 'phase-gate-module-required') &&
998
+ targetModuleKey.toLowerCase() === 'phasegate')) {
999
+ return 'phase-gate';
1000
+ }
1001
+ return targetModuleKey ? 'specific-module' : 'any-module';
1002
+ }
1003
+ function mapRuleToLegacyFormValue(rule) {
1004
+ const formData = {
1005
+ key: rule.key,
1006
+ priority: rule.priority,
1007
+ name: {
1008
+ en: rule.name?.en ?? '',
1009
+ ar: rule.name?.ar ?? '',
1010
+ },
1011
+ description: {
1012
+ en: rule.description?.en ?? '',
1013
+ ar: rule.description?.ar ?? '',
1014
+ },
1015
+ errorMessage: {
1016
+ en: rule.errorMessage?.en ?? '',
1017
+ ar: rule.errorMessage?.ar ?? '',
1018
+ },
1019
+ ruleType: rule.ruleTypeKey,
1020
+ targetScope: rule.targetScope,
1021
+ targetModuleKey: rule.targetModuleKey ?? '',
1022
+ targetOperationKey: rule.targetOperationKey ?? '',
1023
+ stopOnFailure: rule.stopOnFailure,
1024
+ isActive: rule.isActive,
1025
+ };
1026
+ Object.entries(rule.configuration ?? {}).forEach(([key, value]) => {
1027
+ formData[`config_${key}`] = value;
1028
+ });
1029
+ return formData;
1030
+ }
1031
+ function packLegacyFormToCreateDto(formValue) {
1032
+ const name = trimBilingualValue(formValue.name);
1033
+ const description = trimBilingualValue(formValue.description);
1034
+ const errorMessage = trimBilingualValue(formValue.errorMessage);
1035
+ return {
1036
+ key: String(formValue.key ?? '').trim(),
1037
+ name,
1038
+ description: description.en || description.ar ? description : undefined,
1039
+ errorMessage,
1040
+ ruleType: String(formValue.ruleType ?? ''),
1041
+ targetScope: String(formValue.targetScope ?? ''),
1042
+ targetModuleKey: String(formValue.targetModuleKey ?? '') || undefined,
1043
+ targetOperationKey: String(formValue.targetOperationKey ?? '') || undefined,
1044
+ configuration: extractLegacyConfiguration(formValue),
1045
+ priority: Number(formValue.priority ?? 0),
1046
+ stopOnFailure: Boolean(formValue.stopOnFailure ?? true),
1047
+ };
1048
+ }
1049
+ function packLegacyFormToUpdateDto(formValue) {
1050
+ return {
1051
+ ...packLegacyFormToCreateDto(formValue),
1052
+ isActive: Boolean(formValue.isActive ?? true),
1053
+ };
1054
+ }
1055
+ function extractLegacyConfiguration(formValue) {
1056
+ const configuration = {};
1057
+ Object.entries(formValue).forEach(([key, value]) => {
1058
+ if (key.startsWith('config_') &&
1059
+ value !== null &&
1060
+ value !== undefined &&
1061
+ value !== '') {
1062
+ configuration[key.replace('config_', '')] = value;
1063
+ }
1064
+ });
1065
+ return Object.keys(configuration).length > 0 ? configuration : undefined;
1066
+ }
1067
+
1068
+ function buildWizardFormConfig({ translate, isEdit, recipeKey, appliesToOptions, actionOptions, recipeOptions, moduleOptions, requestSchemaOptions, propertyOptions, phaseGateOptions, blockingStatuses, }) {
1069
+ const statusOptions = blockingStatuses.map((status) => ({
1070
+ label: status,
1071
+ value: status,
1072
+ }));
1073
+ const operatorOptions = GOVERNANCE_OPERATOR_OPTIONS.map((option) => ({
1074
+ label: translate(option.labelKey),
1075
+ value: option.value,
1076
+ }));
1077
+ const countLabel = recipeKey === 'module-min-count'
1078
+ ? translate('wizard.min-count-label')
1079
+ : translate('wizard.max-count-label');
1080
+ return {
1081
+ sections: [
1082
+ {
1083
+ key: 'basicInfo',
1084
+ type: 'header',
1085
+ label: translate('basic-information'),
1086
+ order: 1,
1087
+ fields: [
1088
+ new TextFieldConfig({
1089
+ key: 'key',
1090
+ label: translate('rule-key'),
1091
+ validators: [
1092
+ ValidatorConfig.required(translate('wizard.required-message')),
1093
+ ValidatorConfig.pattern('^[a-z0-9-]+$', translate('wizard.key-pattern-message')),
1094
+ ],
1095
+ readonly: isEdit,
1096
+ order: 1,
1097
+ colSpan: 12,
1098
+ }),
1099
+ new TextFieldConfig({
1100
+ key: 'name.en',
1101
+ label: translate('name-en'),
1102
+ validators: [
1103
+ ValidatorConfig.required(translate('wizard.required-message')),
1104
+ ],
1105
+ order: 2,
1106
+ colSpan: 6,
1107
+ }),
1108
+ new TextFieldConfig({
1109
+ key: 'name.ar',
1110
+ label: translate('name-ar'),
1111
+ validators: [
1112
+ ValidatorConfig.required(translate('wizard.required-message')),
1113
+ ],
1114
+ order: 3,
1115
+ colSpan: 6,
1116
+ }),
1117
+ new TextareaFieldConfig({
1118
+ key: 'description.en',
1119
+ label: translate('description-en'),
1120
+ rows: 3,
1121
+ order: 4,
1122
+ colSpan: 6,
1123
+ }),
1124
+ new TextareaFieldConfig({
1125
+ key: 'description.ar',
1126
+ label: translate('description-ar'),
1127
+ rows: 3,
1128
+ order: 5,
1129
+ colSpan: 6,
1130
+ }),
1131
+ new TextareaFieldConfig({
1132
+ key: 'errorMessage.en',
1133
+ label: translate('error-message-en'),
1134
+ validators: [
1135
+ ValidatorConfig.required(translate('wizard.required-message')),
1136
+ ],
1137
+ rows: 3,
1138
+ order: 6,
1139
+ colSpan: 6,
1140
+ }),
1141
+ new TextareaFieldConfig({
1142
+ key: 'errorMessage.ar',
1143
+ label: translate('error-message-ar'),
1144
+ validators: [
1145
+ ValidatorConfig.required(translate('wizard.required-message')),
1146
+ ],
1147
+ rows: 3,
1148
+ order: 7,
1149
+ colSpan: 6,
1150
+ }),
1151
+ ],
1152
+ },
1153
+ {
1154
+ key: 'appliesTo',
1155
+ type: 'header',
1156
+ label: translate('wizard.where-question'),
1157
+ order: 2,
1158
+ fields: [
1159
+ new RadioCardsFieldConfig({
1160
+ key: 'appliesTo',
1161
+ options: appliesToOptions,
1162
+ optionLabel: 'name',
1163
+ optionValue: 'id',
1164
+ validators: [
1165
+ ValidatorConfig.required(translate('wizard.required-message')),
1166
+ ],
1167
+ order: 1,
1168
+ colSpan: 12,
1169
+ size: 'small',
1170
+ }),
1171
+ new SelectFieldConfig({
1172
+ key: 'selectedModuleKey',
1173
+ label: translate('wizard.specific-module-label'),
1174
+ placeholder: translate('wizard.select-placeholder'),
1175
+ options: moduleOptions,
1176
+ optionLabel: 'label',
1177
+ optionValue: 'value',
1178
+ validators: [
1179
+ ValidatorConfig.required(translate('wizard.required-message')),
1180
+ ],
1181
+ order: 2,
1182
+ colSpan: 6,
1183
+ relations: [
1184
+ {
1185
+ key: 'appliesTo',
1186
+ value: 'specific-module',
1187
+ action: 'show',
1188
+ },
1189
+ ],
1190
+ }),
1191
+ ],
1192
+ },
1193
+ {
1194
+ key: 'action',
1195
+ type: 'header',
1196
+ label: translate('wizard.when-question'),
1197
+ order: 3,
1198
+ fields: [
1199
+ new RadioCardsFieldConfig({
1200
+ key: 'actionKey',
1201
+ options: actionOptions,
1202
+ optionLabel: 'name',
1203
+ optionValue: 'id',
1204
+ validators: [
1205
+ ValidatorConfig.required(translate('wizard.required-message')),
1206
+ ],
1207
+ order: 1,
1208
+ colSpan: 12,
1209
+ size: 'small',
1210
+ }),
1211
+ ],
1212
+ },
1213
+ {
1214
+ key: 'recipe',
1215
+ type: 'header',
1216
+ label: translate('wizard.what-question'),
1217
+ order: 4,
1218
+ fields: [
1219
+ new RadioCardsFieldConfig({
1220
+ key: 'recipeKey',
1221
+ options: recipeOptions,
1222
+ optionLabel: 'name',
1223
+ optionValue: 'id',
1224
+ validators: [
1225
+ ValidatorConfig.required(translate('wizard.required-message')),
1226
+ ],
1227
+ order: 1,
1228
+ colSpan: 12,
1229
+ size: 'small',
1230
+ }),
1231
+ ],
1232
+ },
1233
+ {
1234
+ key: 'details',
1235
+ type: 'header',
1236
+ label: translate('wizard.details-question'),
1237
+ order: 5,
1238
+ fields: [
1239
+ new SelectFieldConfig({
1240
+ key: 'requiredModuleKey',
1241
+ label: translate('wizard.required-module-label'),
1242
+ placeholder: translate('wizard.select-placeholder'),
1243
+ options: moduleOptions,
1244
+ optionLabel: 'label',
1245
+ optionValue: 'value',
1246
+ validators: [
1247
+ ValidatorConfig.required(translate('wizard.required-message')),
1248
+ ],
1249
+ order: 1,
1250
+ colSpan: 6,
1251
+ relations: [
1252
+ {
1253
+ key: 'recipeKey',
1254
+ value: 'require-module',
1255
+ action: 'show',
1256
+ },
1257
+ {
1258
+ key: 'recipeKey',
1259
+ value: 'phase-gate-module-required',
1260
+ action: 'show',
1261
+ },
1262
+ ],
1263
+ }),
1264
+ new SelectFieldConfig({
1265
+ key: 'blockedModuleKey',
1266
+ label: translate('wizard.blocked-module-label'),
1267
+ placeholder: translate('wizard.select-placeholder'),
1268
+ options: moduleOptions,
1269
+ optionLabel: 'label',
1270
+ optionValue: 'value',
1271
+ validators: [
1272
+ ValidatorConfig.required(translate('wizard.required-message')),
1273
+ ],
1274
+ order: 2,
1275
+ colSpan: 6,
1276
+ relations: [
1277
+ {
1278
+ key: 'recipeKey',
1279
+ value: 'prevent-module',
1280
+ action: 'show',
1281
+ },
1282
+ ],
1283
+ }),
1284
+ new SelectFieldConfig({
1285
+ key: 'countedModuleKey',
1286
+ label: translate('wizard.counted-module-label'),
1287
+ placeholder: translate('wizard.select-placeholder'),
1288
+ options: moduleOptions,
1289
+ optionLabel: 'label',
1290
+ optionValue: 'value',
1291
+ validators: [
1292
+ ValidatorConfig.required(translate('wizard.required-message')),
1293
+ ],
1294
+ order: 3,
1295
+ colSpan: 6,
1296
+ relations: [
1297
+ {
1298
+ key: 'recipeKey',
1299
+ value: 'module-max-count',
1300
+ action: 'show',
1301
+ },
1302
+ {
1303
+ key: 'recipeKey',
1304
+ value: 'module-min-count',
1305
+ action: 'show',
1306
+ },
1307
+ ],
1308
+ }),
1309
+ new NumberFieldConfig({
1310
+ key: 'countValue',
1311
+ label: countLabel,
1312
+ validators: [
1313
+ ValidatorConfig.required(translate('wizard.required-message')),
1314
+ ValidatorConfig.min(1),
1315
+ ],
1316
+ min: 1,
1317
+ order: 4,
1318
+ colSpan: 6,
1319
+ relations: [
1320
+ {
1321
+ key: 'recipeKey',
1322
+ value: 'module-max-count',
1323
+ action: 'show',
1324
+ },
1325
+ {
1326
+ key: 'recipeKey',
1327
+ value: 'module-min-count',
1328
+ action: 'show',
1329
+ },
1330
+ ],
1331
+ }),
1332
+ new SelectFieldConfig({
1333
+ key: 'phaseGateCompletedSchemaId',
1334
+ label: translate('wizard.phase-gate-required-label'),
1335
+ placeholder: translate('wizard.select-placeholder'),
1336
+ options: phaseGateOptions,
1337
+ optionLabel: 'label',
1338
+ optionValue: 'value',
1339
+ validators: [
1340
+ ValidatorConfig.required(translate('wizard.required-message')),
1341
+ ],
1342
+ order: 5,
1343
+ colSpan: 6,
1344
+ relations: [
1345
+ {
1346
+ key: 'recipeKey',
1347
+ value: 'phase-gate-completed',
1348
+ action: 'show',
1349
+ },
1350
+ ],
1351
+ }),
1352
+ new SelectFieldConfig({
1353
+ key: 'phaseGateOptionalSchemaId',
1354
+ label: translate('wizard.phase-gate-optional-label'),
1355
+ placeholder: translate('wizard.select-placeholder'),
1356
+ options: phaseGateOptions,
1357
+ optionLabel: 'label',
1358
+ optionValue: 'value',
1359
+ order: 6,
1360
+ colSpan: 6,
1361
+ relations: [
1362
+ {
1363
+ key: 'recipeKey',
1364
+ value: 'phase-gate-module-required',
1365
+ action: 'show',
1366
+ },
1367
+ ],
1368
+ }),
1369
+ new SelectFieldConfig({
1370
+ key: 'requestSchemaId',
1371
+ label: translate('wizard.request-schema-label'),
1372
+ placeholder: translate('wizard.request-schema-hint'),
1373
+ options: requestSchemaOptions,
1374
+ optionLabel: 'label',
1375
+ optionValue: 'value',
1376
+ order: 7,
1377
+ colSpan: 6,
1378
+ relations: [
1379
+ {
1380
+ key: 'recipeKey',
1381
+ value: 'no-active-request',
1382
+ action: 'show',
1383
+ },
1384
+ ],
1385
+ }),
1386
+ new MultiSelectFieldConfig({
1387
+ key: 'blockingStatuses',
1388
+ label: translate('wizard.blocking-statuses-label'),
1389
+ placeholder: translate('wizard.select-placeholder'),
1390
+ options: statusOptions,
1391
+ optionLabel: 'label',
1392
+ optionValue: 'value',
1393
+ order: 8,
1394
+ colSpan: 6,
1395
+ relations: [
1396
+ {
1397
+ key: 'recipeKey',
1398
+ value: 'no-active-request',
1399
+ action: 'show',
1400
+ },
1401
+ ],
1402
+ }),
1403
+ new SelectFieldConfig({
1404
+ key: 'propertyKey',
1405
+ label: translate('wizard.property-label'),
1406
+ placeholder: translate('wizard.select-placeholder'),
1407
+ options: propertyOptions,
1408
+ optionLabel: 'label',
1409
+ optionValue: 'value',
1410
+ validators: [
1411
+ ValidatorConfig.required(translate('wizard.required-message')),
1412
+ ],
1413
+ order: 9,
1414
+ colSpan: 6,
1415
+ relations: [
1416
+ {
1417
+ key: 'recipeKey',
1418
+ value: 'property-value',
1419
+ action: 'show',
1420
+ },
1421
+ ],
1422
+ }),
1423
+ new SelectFieldConfig({
1424
+ key: 'operator',
1425
+ label: translate('wizard.operator-label'),
1426
+ placeholder: translate('wizard.select-placeholder'),
1427
+ options: operatorOptions,
1428
+ optionLabel: 'label',
1429
+ optionValue: 'value',
1430
+ validators: [
1431
+ ValidatorConfig.required(translate('wizard.required-message')),
1432
+ ],
1433
+ order: 10,
1434
+ colSpan: 6,
1435
+ relations: [
1436
+ {
1437
+ key: 'recipeKey',
1438
+ value: 'property-value',
1439
+ action: 'show',
1440
+ },
1441
+ ],
1442
+ }),
1443
+ new TextFieldConfig({
1444
+ key: 'expectedValue',
1445
+ label: translate('wizard.expected-value-label'),
1446
+ validators: [
1447
+ ValidatorConfig.required(translate('wizard.required-message')),
1448
+ ],
1449
+ order: 11,
1450
+ colSpan: 12,
1451
+ relations: [
1452
+ {
1453
+ key: 'recipeKey',
1454
+ value: 'property-value',
1455
+ action: 'show',
1456
+ },
1457
+ ],
1458
+ }),
1459
+ ],
1460
+ },
1461
+ {
1462
+ key: 'advanced',
1463
+ type: 'header',
1464
+ label: translate('execution-settings'),
1465
+ order: 6,
1466
+ fields: [
1467
+ new NumberFieldConfig({
1468
+ key: 'priority',
1469
+ label: translate('priority'),
1470
+ validators: [
1471
+ ValidatorConfig.required(translate('wizard.required-message')),
1472
+ ValidatorConfig.min(0),
1473
+ ],
1474
+ min: 0,
1475
+ max: 1000,
1476
+ order: 1,
1477
+ colSpan: 6,
1478
+ }),
1479
+ new ToggleFieldConfig({
1480
+ key: 'stopOnFailure',
1481
+ label: translate('stop-on-failure'),
1482
+ order: 2,
1483
+ colSpan: 12,
1484
+ }),
1485
+ new ToggleFieldConfig({
1486
+ key: 'isActive',
1487
+ label: translate('active-rule'),
1488
+ hidden: !isEdit,
1489
+ order: 3,
1490
+ colSpan: 12,
1491
+ }),
1492
+ ],
1493
+ },
1494
+ ],
1495
+ };
1496
+ }
1497
+ function buildLegacyFormConfig({ translate, isEdit, langCode, selectedRuleType, ruleTypeOptions, scopeOptions, operationOptions, moduleOptions, requestSchemaOptions, propertyOptions, }) {
1498
+ return {
1499
+ sections: [
1500
+ {
1501
+ key: 'basicInfo',
1502
+ type: 'header',
1503
+ label: translate('basic-information'),
1504
+ order: 1,
1505
+ fields: [
1506
+ new TextFieldConfig({
1507
+ key: 'key',
1508
+ label: translate('rule-key'),
1509
+ placeholder: translate('wizard.rule-key-placeholder'),
1510
+ validators: [
1511
+ ValidatorConfig.required(),
1512
+ ValidatorConfig.pattern('^[a-z0-9-]+$', translate('wizard.key-pattern-message')),
1513
+ ],
1514
+ order: 1,
1515
+ colSpan: 12,
1516
+ readonly: isEdit,
1517
+ }),
1518
+ new NumberFieldConfig({
1519
+ key: 'priority',
1520
+ label: translate('priority'),
1521
+ placeholder: '10',
1522
+ validators: [ValidatorConfig.required(), ValidatorConfig.min(0)],
1523
+ min: 0,
1524
+ max: 1000,
1525
+ order: 2,
1526
+ colSpan: 6,
1527
+ }),
1528
+ new TextFieldConfig({
1529
+ key: 'name.en',
1530
+ label: translate('name-en'),
1531
+ placeholder: translate('name-en-placeholder'),
1532
+ validators: [ValidatorConfig.required()],
1533
+ order: 3,
1534
+ colSpan: 6,
1535
+ }),
1536
+ new TextFieldConfig({
1537
+ key: 'name.ar',
1538
+ label: translate('name-ar'),
1539
+ placeholder: translate('name-ar-placeholder'),
1540
+ validators: [ValidatorConfig.required()],
1541
+ order: 4,
1542
+ colSpan: 6,
1543
+ }),
1544
+ new TextareaFieldConfig({
1545
+ key: 'description.en',
1546
+ label: translate('description-en'),
1547
+ placeholder: translate('description-en-placeholder'),
1548
+ order: 5,
1549
+ colSpan: 6,
1550
+ }),
1551
+ new TextareaFieldConfig({
1552
+ key: 'description.ar',
1553
+ label: translate('description-ar'),
1554
+ placeholder: translate('description-ar-placeholder'),
1555
+ order: 6,
1556
+ colSpan: 6,
1557
+ }),
1558
+ new TextareaFieldConfig({
1559
+ key: 'errorMessage.en',
1560
+ label: translate('error-message-en'),
1561
+ placeholder: translate('error-message-en-placeholder'),
1562
+ validators: [ValidatorConfig.required()],
1563
+ order: 7,
1564
+ colSpan: 6,
1565
+ }),
1566
+ new TextareaFieldConfig({
1567
+ key: 'errorMessage.ar',
1568
+ label: translate('error-message-ar'),
1569
+ placeholder: translate('error-message-ar-placeholder'),
1570
+ validators: [ValidatorConfig.required()],
1571
+ order: 8,
1572
+ colSpan: 6,
1573
+ }),
1574
+ ],
1575
+ },
1576
+ {
1577
+ key: 'ruleTarget',
1578
+ type: 'header',
1579
+ label: translate('rule-target'),
1580
+ order: 2,
1581
+ fields: [
1582
+ new SelectFieldConfig({
1583
+ key: 'ruleType',
1584
+ label: translate('rule-type'),
1585
+ options: ruleTypeOptions,
1586
+ optionLabel: 'label',
1587
+ optionValue: 'value',
1588
+ validators: [ValidatorConfig.required()],
1589
+ order: 1,
1590
+ colSpan: 6,
1591
+ readonly: isEdit,
1592
+ }),
1593
+ new SelectFieldConfig({
1594
+ key: 'targetScope',
1595
+ label: translate('target-scope'),
1596
+ options: scopeOptions,
1597
+ optionLabel: 'label',
1598
+ optionValue: 'value',
1599
+ validators: [ValidatorConfig.required()],
1600
+ order: 2,
1601
+ colSpan: 6,
1602
+ }),
1603
+ new SelectFieldConfig({
1604
+ key: 'targetModuleKey',
1605
+ label: translate('target-module'),
1606
+ options: moduleOptions,
1607
+ optionLabel: 'label',
1608
+ optionValue: 'value',
1609
+ order: 3,
1610
+ colSpan: 6,
1611
+ relations: [
1612
+ {
1613
+ key: 'targetScope',
1614
+ value: 'Module',
1615
+ action: 'show',
1616
+ },
1617
+ ],
1618
+ }),
1619
+ new SelectFieldConfig({
1620
+ key: 'targetOperationKey',
1621
+ label: translate('target-operation'),
1622
+ options: operationOptions,
1623
+ optionLabel: 'label',
1624
+ optionValue: 'value',
1625
+ order: 4,
1626
+ colSpan: 6,
1627
+ }),
1628
+ ],
1629
+ },
1630
+ ...(selectedRuleType
1631
+ ? [
1632
+ buildLegacyConfigurationSection({
1633
+ translate,
1634
+ ruleType: selectedRuleType,
1635
+ langCode,
1636
+ requestSchemaOptions,
1637
+ propertyOptions,
1638
+ }),
1639
+ ]
1640
+ : []),
1641
+ {
1642
+ key: 'executionSettings',
1643
+ type: 'header',
1644
+ label: translate('execution-settings'),
1645
+ order: 4,
1646
+ fields: [
1647
+ new ToggleFieldConfig({
1648
+ key: 'stopOnFailure',
1649
+ label: translate('stop-on-failure'),
1650
+ order: 1,
1651
+ colSpan: 12,
1652
+ }),
1653
+ new ToggleFieldConfig({
1654
+ key: 'isActive',
1655
+ label: translate('active-rule'),
1656
+ order: 2,
1657
+ colSpan: 12,
1658
+ }),
1659
+ ],
1660
+ },
1661
+ ],
1662
+ };
1663
+ }
1664
+ function buildLegacyConfigurationSection({ translate, ruleType, langCode, requestSchemaOptions, propertyOptions, }) {
1665
+ return {
1666
+ key: 'configuration',
1667
+ type: 'header',
1668
+ label: translate('rule-configuration'),
1669
+ order: 3,
1670
+ fields: ruleType.configurationSchema.map((field, index) => schemaFieldToFormField({
1671
+ field,
1672
+ order: index + 1,
1673
+ langCode,
1674
+ requestSchemaOptions,
1675
+ propertyOptions,
1676
+ })),
1677
+ };
1678
+ }
1679
+ function schemaFieldToFormField({ field, order, langCode, requestSchemaOptions, propertyOptions, }) {
1680
+ const label = field.label[langCode] || field.label.en;
1681
+ const colSpan = field.colSpan ?? 6;
1682
+ const validators = field.required ? [ValidatorConfig.required()] : [];
1683
+ switch (field.type) {
1684
+ case 'text':
1685
+ return new TextFieldConfig({
1686
+ key: `config_${field.key}`,
1687
+ label,
1688
+ validators,
1689
+ order,
1690
+ colSpan,
1691
+ });
1692
+ case 'textarea':
1693
+ return new TextareaFieldConfig({
1694
+ key: `config_${field.key}`,
1695
+ label,
1696
+ validators,
1697
+ order,
1698
+ colSpan,
1699
+ });
1700
+ case 'number':
1701
+ return new NumberFieldConfig({
1702
+ key: `config_${field.key}`,
1703
+ label,
1704
+ validators,
1705
+ min: field.min,
1706
+ max: field.max,
1707
+ order,
1708
+ colSpan,
1709
+ });
1710
+ case 'select':
1711
+ return new SelectFieldConfig({
1712
+ key: `config_${field.key}`,
1713
+ label,
1714
+ options: getLegacyOptionsForField(field, requestSchemaOptions, propertyOptions),
1715
+ optionLabel: 'label',
1716
+ optionValue: 'value',
1717
+ validators,
1718
+ order,
1719
+ colSpan,
1720
+ });
1721
+ case 'multi-select':
1722
+ return new MultiSelectFieldConfig({
1723
+ key: `config_${field.key}`,
1724
+ label,
1725
+ options: getLegacyOptionsForField(field, requestSchemaOptions, propertyOptions),
1726
+ optionLabel: 'label',
1727
+ optionValue: 'value',
1728
+ validators,
1729
+ order,
1730
+ colSpan,
1731
+ });
1732
+ case 'checkbox':
1733
+ return new ToggleFieldConfig({
1734
+ key: `config_${field.key}`,
1735
+ label,
1736
+ order,
1737
+ colSpan,
1738
+ });
1739
+ case 'date':
1740
+ return new DateFieldConfig({
1741
+ key: `config_${field.key}`,
1742
+ label,
1743
+ validators,
1744
+ order,
1745
+ colSpan,
1746
+ });
1747
+ default:
1748
+ return new TextFieldConfig({
1749
+ key: `config_${field.key}`,
1750
+ label,
1751
+ validators,
1752
+ order,
1753
+ colSpan,
1754
+ });
1755
+ }
1756
+ }
1757
+ function getLegacyOptionsForField(field, requestSchemaOptions, propertyOptions) {
1758
+ switch (field.optionsSource) {
1759
+ case 'requestSchema':
1760
+ return requestSchemaOptions;
1761
+ case 'properties':
1762
+ return propertyOptions;
1763
+ case 'auto-populated':
1764
+ case 'static':
1765
+ default:
1766
+ return field.options ?? [];
1767
+ }
1768
+ }
1769
+
1770
+ function buildActionPrompt(appliesTo, actionKey, translate) {
1771
+ return translate('wizard.summary.action', {
1772
+ object: translate(`wizard.summary.objects.${appliesTo}`),
1773
+ action: translate(`wizard.summary.actions.${actionKey}`),
1774
+ });
1775
+ }
1776
+ function buildSummary({ value, translate, ...context }) {
1777
+ if (!value.actionKey || !value.recipeKey) {
1778
+ return translate('wizard.summary-empty');
1779
+ }
1780
+ return translate('wizard.summary.full', {
1781
+ action: buildActionPrompt(value.appliesTo, value.actionKey, translate),
1782
+ detail: resolveRecipeSummary({ value, translate, ...context }),
1783
+ });
1784
+ }
1785
+ function resolveRecipeSummary({ value, moduleOptions, requestSchemaOptions, propertyOptions, phaseGateOptions, translate, }) {
1786
+ switch (value.recipeKey) {
1787
+ case 'require-module': {
1788
+ const moduleLabel = findOptionLabel(moduleOptions, value.requiredModuleKey);
1789
+ return translate('wizard.summary.require-module', {
1790
+ module: moduleLabel ??
1791
+ value.requiredModuleKey ??
1792
+ translate('wizard.summary.fallbacks.module'),
1793
+ });
1794
+ }
1795
+ case 'prevent-module': {
1796
+ const moduleLabel = findOptionLabel(moduleOptions, value.blockedModuleKey);
1797
+ return translate('wizard.summary.prevent-module', {
1798
+ module: moduleLabel ??
1799
+ value.blockedModuleKey ??
1800
+ translate('wizard.summary.fallbacks.selected-module'),
1801
+ });
1802
+ }
1803
+ case 'module-max-count':
1804
+ return translate('wizard.summary.module-max-count', {
1805
+ count: value.countValue ?? 0,
1806
+ module: findOptionLabel(moduleOptions, value.countedModuleKey) ??
1807
+ value.countedModuleKey ??
1808
+ translate('wizard.summary.fallbacks.modules'),
1809
+ });
1810
+ case 'module-min-count':
1811
+ return translate('wizard.summary.module-min-count', {
1812
+ count: value.countValue ?? 0,
1813
+ module: findOptionLabel(moduleOptions, value.countedModuleKey) ??
1814
+ value.countedModuleKey ??
1815
+ translate('wizard.summary.fallbacks.modules'),
1816
+ });
1817
+ case 'phase-gate-completed': {
1818
+ const phaseGateLabel = findOptionLabel(phaseGateOptions, value.phaseGateSchemaId);
1819
+ return translate('wizard.summary.phase-gate-completed', {
1820
+ phaseGate: phaseGateLabel ?? translate('wizard.summary.fallbacks.phase-gate'),
1821
+ });
1822
+ }
1823
+ case 'phase-gate-module-required': {
1824
+ const moduleLabel = findOptionLabel(moduleOptions, value.requiredModuleKey);
1825
+ return translate('wizard.summary.phase-gate-module-required', {
1826
+ module: moduleLabel ??
1827
+ value.requiredModuleKey ??
1828
+ translate('wizard.summary.fallbacks.required-module'),
1829
+ });
1830
+ }
1831
+ case 'no-active-request': {
1832
+ const requestLabel = findOptionLabel(requestSchemaOptions, value.requestSchemaId);
1833
+ return translate('wizard.summary.no-active-request', {
1834
+ request: requestLabel ?? translate('wizard.summary.fallbacks.request'),
1835
+ });
1836
+ }
1837
+ case 'property-value': {
1838
+ const propertyLabel = findOptionLabel(propertyOptions, value.propertyKey) ??
1839
+ value.propertyKey ??
1840
+ translate('wizard.summary.fallbacks.property');
1841
+ return translate('wizard.summary.property-value', {
1842
+ property: propertyLabel,
1843
+ operator: resolveOperatorText(value.operator, translate),
1844
+ expectedValue: value.expectedValue || '',
1845
+ }).trim();
1846
+ }
1847
+ default:
1848
+ return translate('wizard.summary-empty');
1849
+ }
1850
+ }
1851
+ function resolveOperatorText(operator, translate) {
1852
+ const option = GOVERNANCE_OPERATOR_OPTIONS.find((item) => item.value === operator);
1853
+ return option
1854
+ ? translate(option.labelKey).toLowerCase()
1855
+ : translate('wizard.summary.fallbacks.operator');
1856
+ }
1857
+ function findOptionLabel(options, value) {
1858
+ if (!value) {
1859
+ return null;
1860
+ }
1861
+ return options.find((option) => option.value === value)?.label ?? null;
1862
+ }
1863
+
1864
+ const DEFAULT_WIZARD_FORM_STATE = {
1865
+ ...DEFAULT_GOVERNANCE_RULE_FORM_VALUE,
1866
+ phaseGateCompletedSchemaId: null,
1867
+ phaseGateOptionalSchemaId: null,
1868
+ };
1869
+ function mapWizardFormValueToState(value) {
1870
+ return {
1871
+ ...DEFAULT_WIZARD_FORM_STATE,
1872
+ ...value,
1873
+ phaseGateCompletedSchemaId: value.recipeKey === 'phase-gate-completed'
1874
+ ? value.phaseGateSchemaId
1875
+ : null,
1876
+ phaseGateOptionalSchemaId: value.recipeKey === 'phase-gate-module-required'
1877
+ ? value.phaseGateSchemaId
1878
+ : null,
1879
+ };
1880
+ }
1881
+ function normalizeWizardFormState(value) {
1882
+ return {
1883
+ ...DEFAULT_WIZARD_FORM_STATE,
1884
+ ...value,
1885
+ name: {
1886
+ ...DEFAULT_GOVERNANCE_RULE_FORM_VALUE.name,
1887
+ ...value?.name,
1888
+ },
1889
+ description: {
1890
+ ...DEFAULT_GOVERNANCE_RULE_FORM_VALUE.description,
1891
+ ...value?.description,
1892
+ },
1893
+ errorMessage: {
1894
+ ...DEFAULT_GOVERNANCE_RULE_FORM_VALUE.errorMessage,
1895
+ ...value?.errorMessage,
1896
+ },
1897
+ selectedModuleKey: value?.selectedModuleKey ?? null,
1898
+ actionKey: value?.actionKey ?? DEFAULT_GOVERNANCE_RULE_FORM_VALUE.actionKey,
1899
+ recipeKey: value?.recipeKey ?? null,
1900
+ requiredModuleKey: value?.requiredModuleKey ?? null,
1901
+ blockedModuleKey: value?.blockedModuleKey ?? null,
1902
+ countedModuleKey: value?.countedModuleKey ?? null,
1903
+ countValue: value?.countValue ?? DEFAULT_GOVERNANCE_RULE_FORM_VALUE.countValue,
1904
+ phaseGateCompletedSchemaId: value?.phaseGateCompletedSchemaId ?? null,
1905
+ phaseGateOptionalSchemaId: value?.phaseGateOptionalSchemaId ?? null,
1906
+ phaseGateSchemaId: value?.phaseGateSchemaId ?? null,
1907
+ requestSchemaId: value?.requestSchemaId ?? null,
1908
+ blockingStatuses: Array.isArray(value?.blockingStatuses)
1909
+ ? value.blockingStatuses
1910
+ : [...DEFAULT_GOVERNANCE_RULE_FORM_VALUE.blockingStatuses],
1911
+ propertyKey: value?.propertyKey ?? null,
1912
+ operator: value?.operator ?? DEFAULT_GOVERNANCE_RULE_FORM_VALUE.operator,
1913
+ expectedValue: value?.expectedValue ?? DEFAULT_GOVERNANCE_RULE_FORM_VALUE.expectedValue,
1914
+ priority: value?.priority ?? DEFAULT_GOVERNANCE_RULE_FORM_VALUE.priority,
1915
+ stopOnFailure: value?.stopOnFailure ?? DEFAULT_GOVERNANCE_RULE_FORM_VALUE.stopOnFailure,
1916
+ isActive: value?.isActive ?? DEFAULT_GOVERNANCE_RULE_FORM_VALUE.isActive,
1917
+ };
1918
+ }
1919
+ function mapWizardStateToWizardValue(state) {
1920
+ return {
1921
+ key: state.key,
1922
+ name: {
1923
+ en: state.name.en,
1924
+ ar: state.name.ar,
1925
+ },
1926
+ description: {
1927
+ en: state.description.en,
1928
+ ar: state.description.ar,
1929
+ },
1930
+ errorMessage: {
1931
+ en: state.errorMessage.en,
1932
+ ar: state.errorMessage.ar,
1933
+ },
1934
+ appliesTo: state.appliesTo,
1935
+ selectedModuleKey: state.selectedModuleKey,
1936
+ actionKey: state.actionKey,
1937
+ recipeKey: state.recipeKey,
1938
+ requiredModuleKey: state.requiredModuleKey,
1939
+ blockedModuleKey: state.blockedModuleKey,
1940
+ countedModuleKey: state.countedModuleKey,
1941
+ countValue: state.countValue,
1942
+ phaseGateSchemaId: state.phaseGateCompletedSchemaId ??
1943
+ state.phaseGateOptionalSchemaId ??
1944
+ null,
1945
+ requestSchemaId: state.requestSchemaId,
1946
+ blockingStatuses: [...state.blockingStatuses],
1947
+ propertyKey: state.propertyKey,
1948
+ operator: state.operator,
1949
+ expectedValue: state.expectedValue,
1950
+ priority: state.priority,
1951
+ stopOnFailure: state.stopOnFailure,
1952
+ isActive: state.isActive,
1953
+ };
1954
+ }
1955
+ function resolveRecipeDefinitionForRuleType(ruleType) {
1956
+ return resolveRecipeDefinition(resolveRecipeDefinitionKeyForRuleType(ruleType));
1957
+ }
1958
+ function resolveRecipeDefinitionKeyForRuleType(ruleType) {
1959
+ switch (readGovernanceRuleTypeKey(ruleType)) {
1960
+ case 'ModuleExists':
1961
+ return 'require-module';
1962
+ case 'ModuleNotExists':
1963
+ case 'ModuleDoesNotExist':
1964
+ return 'prevent-module';
1965
+ case 'ModuleMaxCount':
1966
+ return 'module-max-count';
1967
+ case 'ModuleMinCount':
1968
+ return 'module-min-count';
1969
+ case 'PhaseGateCompleted':
1970
+ return 'phase-gate-completed';
1971
+ case 'PhaseGateModuleRequired':
1972
+ return 'phase-gate-module-required';
1973
+ case 'NoActiveRequest':
1974
+ return 'no-active-request';
1975
+ case 'PropertyValueEquals':
1976
+ case 'FieldValue':
1977
+ return 'property-value';
1978
+ default:
1979
+ return null;
1980
+ }
1981
+ }
1982
+ function hasCompatibleRecipe(ruleTypes, appliesTo, actionKey) {
1983
+ return ruleTypes.some((ruleType) => !!resolveRecipeDefinitionForRuleType(ruleType) &&
1984
+ isRuleTypeCompatible(ruleType, appliesTo, actionKey));
1985
+ }
1986
+ function isRuleTypeCompatible(ruleType, appliesTo, actionKey) {
1987
+ return (ruleType.supportedScopes.includes(mapAppliesToToTargetScope(appliesTo)) &&
1988
+ ruleType.supportedOperations.includes(mapActionToOperationKey(actionKey)));
1989
+ }
1990
+ function areWizardStatesEqual(current, next) {
1991
+ return (current.key === next.key &&
1992
+ current.name.en === next.name.en &&
1993
+ current.name.ar === next.name.ar &&
1994
+ current.description.en === next.description.en &&
1995
+ current.description.ar === next.description.ar &&
1996
+ current.errorMessage.en === next.errorMessage.en &&
1997
+ current.errorMessage.ar === next.errorMessage.ar &&
1998
+ current.appliesTo === next.appliesTo &&
1999
+ current.selectedModuleKey === next.selectedModuleKey &&
2000
+ current.actionKey === next.actionKey &&
2001
+ current.recipeKey === next.recipeKey &&
2002
+ current.requiredModuleKey === next.requiredModuleKey &&
2003
+ current.blockedModuleKey === next.blockedModuleKey &&
2004
+ current.countedModuleKey === next.countedModuleKey &&
2005
+ current.countValue === next.countValue &&
2006
+ current.phaseGateCompletedSchemaId === next.phaseGateCompletedSchemaId &&
2007
+ current.phaseGateOptionalSchemaId === next.phaseGateOptionalSchemaId &&
2008
+ current.phaseGateSchemaId === next.phaseGateSchemaId &&
2009
+ current.requestSchemaId === next.requestSchemaId &&
2010
+ current.propertyKey === next.propertyKey &&
2011
+ current.operator === next.operator &&
2012
+ current.expectedValue === next.expectedValue &&
2013
+ current.priority === next.priority &&
2014
+ current.stopOnFailure === next.stopOnFailure &&
2015
+ current.isActive === next.isActive &&
2016
+ areStringArraysEqual(current.blockingStatuses, next.blockingStatuses));
2017
+ }
2018
+ function areLegacyFormValuesEqual(current, next) {
2019
+ if (current === next) {
2020
+ return true;
2021
+ }
2022
+ if (!current || !next) {
2023
+ return current === next;
2024
+ }
2025
+ if (current.key !== next.key ||
2026
+ current.priority !== next.priority ||
2027
+ current.name.en !== next.name.en ||
2028
+ current.name.ar !== next.name.ar ||
2029
+ current.description.en !== next.description.en ||
2030
+ current.description.ar !== next.description.ar ||
2031
+ current.errorMessage.en !== next.errorMessage.en ||
2032
+ current.errorMessage.ar !== next.errorMessage.ar ||
2033
+ current.ruleType !== next.ruleType ||
2034
+ current.targetScope !== next.targetScope ||
2035
+ current.targetModuleKey !== next.targetModuleKey ||
2036
+ current.targetOperationKey !== next.targetOperationKey ||
2037
+ current.stopOnFailure !== next.stopOnFailure ||
2038
+ current.isActive !== next.isActive) {
2039
+ return false;
2040
+ }
2041
+ const currentKeys = Object.keys(current);
2042
+ const nextKeys = Object.keys(next);
2043
+ if (currentKeys.length !== nextKeys.length) {
2044
+ return false;
2045
+ }
2046
+ return currentKeys.every((key) => {
2047
+ if (key === 'key' ||
2048
+ key === 'priority' ||
2049
+ key === 'name' ||
2050
+ key === 'description' ||
2051
+ key === 'errorMessage' ||
2052
+ key === 'ruleType' ||
2053
+ key === 'targetScope' ||
2054
+ key === 'targetModuleKey' ||
2055
+ key === 'targetOperationKey' ||
2056
+ key === 'stopOnFailure' ||
2057
+ key === 'isActive') {
2058
+ return true;
2059
+ }
2060
+ const currentValue = current[key];
2061
+ const nextValue = next[key];
2062
+ if (Array.isArray(currentValue) || Array.isArray(nextValue)) {
2063
+ return (Array.isArray(currentValue) &&
2064
+ Array.isArray(nextValue) &&
2065
+ areUnknownArraysEqual(currentValue, nextValue));
2066
+ }
2067
+ return currentValue === nextValue;
2068
+ });
2069
+ }
2070
+ function areStringArraysEqual(current, next) {
2071
+ if (current.length !== next.length) {
2072
+ return false;
2073
+ }
2074
+ return current.every((value, index) => value === next[index]);
2075
+ }
2076
+ function areUnknownArraysEqual(current, next) {
2077
+ if (current.length !== next.length) {
2078
+ return false;
2079
+ }
2080
+ return current.every((value, index) => value === next[index]);
2081
+ }
2082
+
596
2083
  class GovernanceRuleForm {
597
- // ============================================================================
598
- // Injected Services
599
- // ============================================================================
600
2084
  modal = inject(ModalService);
601
2085
  ref = inject(ModalRef);
602
2086
  translocoService = inject(TranslocoService);
603
2087
  facade = inject(GovernanceFacade);
604
- // ============================================================================
605
- // Input - rule to edit (null for create)
606
- // ============================================================================
2088
+ toast = inject(ToastService);
607
2089
  ruleForEdit = input(null, ...(ngDevMode ? [{ debugName: "ruleForEdit" }] : []));
608
- // ============================================================================
609
- // Facade Data
610
- // ============================================================================
2090
+ dynamicFormRef = viewChild(DynamicForm, ...(ngDevMode ? [{ debugName: "dynamicFormRef" }] : []));
611
2091
  selectedRule = this.facade.selectedRule;
612
2092
  ruleTypes = this.facade.ruleTypes;
613
2093
  isLoadingRule = this.facade.isLoadingRule;
2094
+ isLoadingRuleTypes = this.facade.isLoadingRuleTypes;
614
2095
  isAddingRule = this.facade.isAddingRule;
615
2096
  isUpdatingRule = this.facade.isUpdatingRule;
616
- // For optionsSource: 'auto-populated' (modules from API)
617
2097
  modules = this.facade.modules;
618
- // For optionsSource: 'requestSchema' and 'properties'
619
2098
  requestSchemas = this.facade.requestSchemas;
620
2099
  properties = this.facade.properties;
621
- isLoadingRequestSchemas = this.facade.isLoadingRequestSchemas;
622
- isLoadingProperties = this.facade.isLoadingProperties;
623
- // ============================================================================
624
- // Form Control
625
- // ============================================================================
626
- ruleFormControl = new FormControl();
627
- formValue = toSignal(this.ruleFormControl.valueChanges);
628
- // ============================================================================
629
- // Selected Rule Type (from form value)
630
- // ============================================================================
631
- selectedRuleType = computed(() => {
632
- const formValue = this.formValue();
633
- const ruleTypeKey = formValue?.ruleType;
634
- if (!ruleTypeKey)
635
- return null;
636
- return this.ruleTypes().find((rt) => rt.type === ruleTypeKey) ?? null;
637
- }, ...(ngDevMode ? [{ debugName: "selectedRuleType" }] : []));
638
- // ============================================================================
639
- // Target Scope and Module Key (from form value)
640
- // ============================================================================
641
- targetScope = computed(() => this.formValue()?.targetScope ?? 'Level', ...(ngDevMode ? [{ debugName: "targetScope" }] : []));
642
- targetModuleKey = computed(() => this.formValue()?.targetModuleKey ?? null, ...(ngDevMode ? [{ debugName: "targetModuleKey" }] : []));
643
- // ============================================================================
644
- // Options from state for optionsSource: 'requestSchema'
645
- // ============================================================================
2100
+ wizardFormControl = new FormControl(DEFAULT_WIZARD_FORM_STATE, { nonNullable: true });
2101
+ legacyFormControl = new FormControl(null);
2102
+ wizardStateRaw = toSignal(this.wizardFormControl.valueChanges.pipe(map((value) => value ?? DEFAULT_WIZARD_FORM_STATE)), { initialValue: this.wizardFormControl.value });
2103
+ legacyFormValue = toSignal(this.legacyFormControl.valueChanges.pipe(map(() => this.legacyFormControl.value)), { initialValue: this.legacyFormControl.value });
2104
+ activeLang = toSignal(this.translocoService.langChanges$, {
2105
+ initialValue: this.translocoService.getActiveLang(),
2106
+ });
2107
+ langCode = computed(() => this.activeLang() === 'ar' ? 'ar' : 'en', ...(ngDevMode ? [{ debugName: "langCode" }] : []));
2108
+ isEditMode = computed(() => !!this.ruleForEdit()?.id, ...(ngDevMode ? [{ debugName: "isEditMode" }] : []));
2109
+ wizardState = computed(() => normalizeWizardFormState(this.wizardStateRaw()), ...(ngDevMode ? [{ debugName: "wizardState" }] : []));
2110
+ wizardValue = computed(() => mapWizardStateToWizardValue(this.wizardState()), ...(ngDevMode ? [{ debugName: "wizardValue" }] : []));
2111
+ moduleOptions = computed(() => {
2112
+ const lang = this.langCode();
2113
+ return this.modules().map((module) => ({
2114
+ label: module.name[lang] || module.name.en,
2115
+ value: module.key,
2116
+ }));
2117
+ }, ...(ngDevMode ? [{ debugName: "moduleOptions" }] : []));
646
2118
  requestSchemaOptions = computed(() => {
647
- const lang = this.translocoService.getActiveLang();
2119
+ const lang = this.langCode();
648
2120
  return this.requestSchemas().map((schema) => ({
649
2121
  label: schema.name[lang] || schema.name.en,
650
- value: schema.id.toString(),
2122
+ value: String(schema.id),
651
2123
  }));
652
2124
  }, ...(ngDevMode ? [{ debugName: "requestSchemaOptions" }] : []));
653
- // ============================================================================
654
- // Options from state for optionsSource: 'properties'
655
- // ============================================================================
656
2125
  propertyOptions = computed(() => {
657
- const lang = this.translocoService.getActiveLang();
658
- return this.properties().map((prop) => ({
659
- label: prop.name[lang] || prop.name.en,
660
- value: prop.key,
2126
+ const lang = this.langCode();
2127
+ return this.properties().map((property) => ({
2128
+ label: property.name[lang] || property.name.en,
2129
+ value: property.key,
661
2130
  }));
662
2131
  }, ...(ngDevMode ? [{ debugName: "propertyOptions" }] : []));
663
- // ============================================================================
664
- // Module Options for optionsSource: 'auto-populated' (moduleKey, requiredModuleKey)
665
- // ============================================================================
666
- moduleOptions = computed(() => {
667
- const lang = this.translocoService.getActiveLang();
668
- return this.modules().map((mod) => ({
669
- label: mod.name[lang] || mod.name.en,
670
- value: mod.key,
671
- }));
672
- }, ...(ngDevMode ? [{ debugName: "moduleOptions" }] : []));
673
- // ============================================================================
674
- // Scope Options based on selected rule type
675
- // ============================================================================
2132
+ appliesToCardOptions = computed(() => GOVERNANCE_APPLIES_TO_OPTIONS.map((option) => ({
2133
+ id: option.value,
2134
+ name: this.translate(option.labelKey),
2135
+ })), ...(ngDevMode ? [{ debugName: "appliesToCardOptions" }] : []));
2136
+ availableActionOptions = computed(() => {
2137
+ return GOVERNANCE_ACTION_OPTIONS.filter((option) => hasCompatibleRecipe(this.ruleTypes(), this.wizardValue().appliesTo, option.value));
2138
+ }, ...(ngDevMode ? [{ debugName: "availableActionOptions" }] : []));
2139
+ actionCardOptions = computed(() => this.availableActionOptions().map((option) => ({
2140
+ id: option.value,
2141
+ name: buildActionPrompt(this.wizardValue().appliesTo, option.value, this.translate.bind(this)),
2142
+ })), ...(ngDevMode ? [{ debugName: "actionCardOptions" }] : []));
2143
+ availableRecipeDefinitions = computed(() => {
2144
+ const actionKey = this.wizardValue().actionKey;
2145
+ if (!actionKey) {
2146
+ return [];
2147
+ }
2148
+ return this.ruleTypes()
2149
+ .map((ruleType) => {
2150
+ const recipe = resolveRecipeDefinitionForRuleType(ruleType);
2151
+ if (!recipe) {
2152
+ return null;
2153
+ }
2154
+ return isRuleTypeCompatible(ruleType, this.wizardValue().appliesTo, actionKey)
2155
+ ? recipe
2156
+ : null;
2157
+ })
2158
+ .filter((recipe, index, items) => !!recipe &&
2159
+ items.findIndex((item) => item?.key === recipe.key) === index);
2160
+ }, ...(ngDevMode ? [{ debugName: "availableRecipeDefinitions" }] : []));
2161
+ recipeCardOptions = computed(() => this.availableRecipeDefinitions().map((recipe) => ({
2162
+ id: recipe.key,
2163
+ name: this.translate(recipe.labelKey),
2164
+ })), ...(ngDevMode ? [{ debugName: "recipeCardOptions" }] : []));
2165
+ selectedRuleType = computed(() => findRuleTypeByRecipe(this.ruleTypes(), this.wizardValue().recipeKey), ...(ngDevMode ? [{ debugName: "selectedRuleType" }] : []));
2166
+ useLegacyEditor = computed(() => {
2167
+ if (!this.ruleForEdit()?.id) {
2168
+ return false;
2169
+ }
2170
+ const rule = this.selectedRule();
2171
+ return !!rule && !mapRuleToWizardFormValue(rule);
2172
+ }, ...(ngDevMode ? [{ debugName: "useLegacyEditor" }] : []));
2173
+ isInitializing = computed(() => {
2174
+ if (this.ruleForEdit()?.id && this.isLoadingRule()) {
2175
+ return true;
2176
+ }
2177
+ return this.ruleTypes().length === 0 && this.isLoadingRuleTypes();
2178
+ }, ...(ngDevMode ? [{ debugName: "isInitializing" }] : []));
2179
+ noRecipesAvailable = computed(() => !this.useLegacyEditor() &&
2180
+ !!this.wizardValue().actionKey &&
2181
+ this.recipeCardOptions().length === 0, ...(ngDevMode ? [{ debugName: "noRecipesAvailable" }] : []));
2182
+ hasReviewSummary = computed(() => {
2183
+ if (this.useLegacyEditor()) {
2184
+ return false;
2185
+ }
2186
+ const value = this.wizardValue();
2187
+ return !!value.actionKey && !!value.recipeKey;
2188
+ }, ...(ngDevMode ? [{ debugName: "hasReviewSummary" }] : []));
2189
+ canSubmit = computed(() => {
2190
+ if (this.useLegacyEditor()) {
2191
+ return this.legacyFormControl.valid;
2192
+ }
2193
+ return (this.wizardFormControl.valid &&
2194
+ !!this.selectedRuleType() &&
2195
+ !!this.wizardValue().actionKey &&
2196
+ !!this.wizardValue().recipeKey);
2197
+ }, ...(ngDevMode ? [{ debugName: "canSubmit" }] : []));
2198
+ reviewSummary = computed(() => buildSummary({
2199
+ value: this.wizardValue(),
2200
+ moduleOptions: this.moduleOptions(),
2201
+ requestSchemaOptions: this.requestSchemaOptions(),
2202
+ propertyOptions: this.propertyOptions(),
2203
+ phaseGateOptions: this.getPhaseGateOptions(),
2204
+ translate: this.translate.bind(this),
2205
+ }), ...(ngDevMode ? [{ debugName: "reviewSummary" }] : []));
2206
+ wizardFormConfig = computed(() => buildWizardFormConfig({
2207
+ translate: this.translate.bind(this),
2208
+ isEdit: this.isEditMode(),
2209
+ recipeKey: this.wizardValue().recipeKey,
2210
+ appliesToOptions: this.appliesToCardOptions(),
2211
+ actionOptions: this.actionCardOptions(),
2212
+ recipeOptions: this.recipeCardOptions(),
2213
+ moduleOptions: this.moduleOptions(),
2214
+ requestSchemaOptions: this.requestSchemaOptions(),
2215
+ propertyOptions: this.propertyOptions(),
2216
+ phaseGateOptions: this.getPhaseGateOptions(),
2217
+ blockingStatuses: this.getBlockingStatusOptions(),
2218
+ }), ...(ngDevMode ? [{ debugName: "wizardFormConfig" }] : []));
2219
+ selectedLegacyRuleType = computed(() => {
2220
+ const ruleTypeKey = String(this.legacyFormValue()?.ruleType ?? '').trim();
2221
+ if (!ruleTypeKey) {
2222
+ return null;
2223
+ }
2224
+ return (this.ruleTypes().find((ruleType) => {
2225
+ const catalogKey = readGovernanceRuleTypeKey(ruleType);
2226
+ return (catalogKey === ruleTypeKey || String(ruleType.type) === ruleTypeKey);
2227
+ }) ?? null);
2228
+ }, ...(ngDevMode ? [{ debugName: "selectedLegacyRuleType" }] : []));
676
2229
  scopeOptions = computed(() => {
677
- const ruleType = this.selectedRuleType();
2230
+ const ruleType = this.selectedLegacyRuleType();
678
2231
  if (!ruleType) {
679
2232
  return [
680
- { label: 'Level', value: 'Level' },
681
- { label: 'Module', value: 'Module' },
682
- { label: 'PhaseGate', value: 'PhaseGate' },
2233
+ { label: this.translateScopeLabel('Level'), value: 'Level' },
2234
+ { label: this.translateScopeLabel('Module'), value: 'Module' },
2235
+ { label: this.translateScopeLabel('PhaseGate'), value: 'PhaseGate' },
683
2236
  ];
684
2237
  }
685
2238
  return ruleType.supportedScopes.map((scope) => ({
686
- label: scope,
2239
+ label: this.translateScopeLabel(scope),
687
2240
  value: scope,
688
2241
  }));
689
2242
  }, ...(ngDevMode ? [{ debugName: "scopeOptions" }] : []));
690
- // ============================================================================
691
- // Operation Options based on selected rule type
692
- // ============================================================================
693
2243
  operationOptions = computed(() => {
694
- const ruleType = this.selectedRuleType();
2244
+ const ruleType = this.selectedLegacyRuleType();
695
2245
  if (!ruleType) {
696
2246
  return [
697
- { label: 'All', value: '' },
698
- { label: 'Create', value: 'Create' },
699
- { label: 'Update', value: 'Update' },
700
- { label: 'Delete', value: 'Delete' },
2247
+ { label: this.translate('all'), value: '' },
2248
+ { label: this.translateOperationLabel('Create'), value: 'Create' },
2249
+ { label: this.translateOperationLabel('Update'), value: 'Update' },
2250
+ { label: this.translateOperationLabel('Delete'), value: 'Delete' },
701
2251
  ];
702
2252
  }
703
2253
  return [
704
- { label: 'All', value: '' },
705
- ...ruleType.supportedOperations.map((op) => ({
706
- label: op,
707
- value: op,
2254
+ { label: this.translate('all'), value: '' },
2255
+ ...ruleType.supportedOperations.map((operation) => ({
2256
+ label: this.translateOperationLabel(operation),
2257
+ value: operation,
708
2258
  })),
709
2259
  ];
710
2260
  }, ...(ngDevMode ? [{ debugName: "operationOptions" }] : []));
711
- // ============================================================================
712
- // Rule Type Options
713
- // ============================================================================
714
- ruleTypeOptions = computed(() => {
715
- const lang = this.translocoService.getActiveLang();
716
- return this.ruleTypes().map((rt) => ({
717
- label: rt.name[lang] || rt.name.en,
718
- value: rt.type,
719
- }));
720
- }, ...(ngDevMode ? [{ debugName: "ruleTypeOptions" }] : []));
721
- // ============================================================================
722
- // Dynamic Form Configuration
723
- // ============================================================================
724
- formConfig = computed(() => {
725
- const t = (key) => this.translocoService.translate(`governance.${key}`);
726
- const ruleType = this.selectedRuleType();
727
- const isEdit = !!this.ruleForEdit()?.id;
728
- return {
729
- sections: [
730
- // Section 1: Basic Information
731
- {
732
- key: 'basicInfo',
733
- type: 'header',
734
- label: t('basic-information'),
735
- order: 1,
736
- fields: [
737
- new TextFieldConfig({
738
- key: 'key',
739
- label: t('rule-key'),
740
- placeholder: 'e.g., charter-required',
741
- validators: [
742
- ValidatorConfig.required(),
743
- ValidatorConfig.pattern('^[a-z0-9-]+$', 'Only lowercase letters, numbers and hyphens allowed'),
744
- ],
745
- order: 1,
746
- colSpan: 6,
747
- disabled: isEdit,
748
- }),
749
- new NumberFieldConfig({
750
- key: 'priority',
751
- label: t('priority'),
752
- placeholder: '10',
753
- validators: [ValidatorConfig.required()],
754
- min: 0,
755
- max: 1000,
756
- order: 2,
757
- colSpan: 6,
758
- }),
759
- new TextFieldConfig({
760
- key: 'name_en',
761
- label: t('name-en'),
762
- placeholder: t('name-en-placeholder'),
763
- validators: [ValidatorConfig.required()],
764
- order: 3,
765
- colSpan: 6,
766
- }),
767
- new TextFieldConfig({
768
- key: 'name_ar',
769
- label: t('name-ar'),
770
- placeholder: t('name-ar-placeholder'),
771
- validators: [ValidatorConfig.required()],
772
- order: 4,
773
- colSpan: 6,
774
- }),
775
- new TextareaFieldConfig({
776
- key: 'description_en',
777
- label: t('description-en'),
778
- placeholder: t('description-en-placeholder'),
779
- order: 5,
780
- colSpan: 6,
781
- }),
782
- new TextareaFieldConfig({
783
- key: 'description_ar',
784
- label: t('description-ar'),
785
- placeholder: t('description-ar-placeholder'),
786
- order: 6,
787
- colSpan: 6,
788
- }),
789
- new TextareaFieldConfig({
790
- key: 'errorMessage_en',
791
- label: t('error-message-en'),
792
- placeholder: t('error-message-en-placeholder'),
793
- validators: [ValidatorConfig.required()],
794
- order: 7,
795
- colSpan: 6,
796
- }),
797
- new TextareaFieldConfig({
798
- key: 'errorMessage_ar',
799
- label: t('error-message-ar'),
800
- placeholder: t('error-message-ar-placeholder'),
801
- validators: [ValidatorConfig.required()],
802
- order: 8,
803
- colSpan: 6,
804
- }),
805
- ],
806
- },
807
- // Section 2: Rule Target
808
- {
809
- key: 'ruleTarget',
810
- type: 'header',
811
- label: t('rule-target'),
812
- order: 2,
813
- fields: [
814
- new SelectFieldConfig({
815
- key: 'ruleType',
816
- label: t('rule-type'),
817
- options: this.ruleTypeOptions(),
818
- optionLabel: 'label',
819
- optionValue: 'value',
820
- validators: [ValidatorConfig.required()],
821
- order: 1,
822
- colSpan: 6,
823
- disabled: isEdit,
824
- }),
825
- new SelectFieldConfig({
826
- key: 'targetScope',
827
- label: t('target-scope'),
828
- options: this.scopeOptions(),
829
- optionLabel: 'label',
830
- optionValue: 'value',
831
- validators: [ValidatorConfig.required()],
832
- order: 2,
833
- colSpan: 6,
834
- }),
835
- new SelectFieldConfig({
836
- key: 'targetModuleKey',
837
- label: t('target-module'),
838
- options: this.moduleOptions(),
839
- optionLabel: 'label',
840
- optionValue: 'value',
841
- order: 3,
842
- colSpan: 6,
843
- relations: [
844
- {
845
- key: 'targetScope',
846
- value: 'Module',
847
- action: 'show',
848
- },
849
- ],
850
- }),
851
- new SelectFieldConfig({
852
- key: 'targetOperationKey',
853
- label: t('target-operation'),
854
- options: this.operationOptions(),
855
- optionLabel: 'label',
856
- optionValue: 'value',
857
- order: 4,
858
- colSpan: 6,
859
- }),
860
- ],
861
- },
862
- // Section 3: Rule-Specific Configuration (dynamic)
863
- ...(ruleType ? [this.buildConfigurationSection(ruleType)] : []),
864
- // Section 4: Execution Settings
865
- {
866
- key: 'executionSettings',
867
- type: 'header',
868
- label: t('execution-settings'),
869
- order: 4,
870
- fields: [
871
- new ToggleFieldConfig({
872
- key: 'stopOnFailure',
873
- label: t('stop-on-failure'),
874
- order: 1,
875
- colSpan: 6,
876
- }),
877
- new ToggleFieldConfig({
878
- key: 'isActive',
879
- label: t('active-rule'),
880
- order: 2,
881
- colSpan: 6,
882
- }),
883
- ],
884
- },
885
- ],
886
- };
887
- }, ...(ngDevMode ? [{ debugName: "formConfig" }] : []));
888
- // ============================================================================
889
- // Constructor with Effect for Form Population
890
- // ============================================================================
2261
+ legacyFormConfig = computed(() => buildLegacyFormConfig({
2262
+ translate: this.translate.bind(this),
2263
+ isEdit: this.isEditMode(),
2264
+ langCode: this.langCode(),
2265
+ selectedRuleType: this.selectedLegacyRuleType(),
2266
+ ruleTypeOptions: this.ruleTypeOptions(),
2267
+ scopeOptions: this.scopeOptions(),
2268
+ operationOptions: this.operationOptions(),
2269
+ moduleOptions: this.moduleOptions(),
2270
+ requestSchemaOptions: this.requestSchemaOptions(),
2271
+ propertyOptions: this.propertyOptions(),
2272
+ }), ...(ngDevMode ? [{ debugName: "legacyFormConfig" }] : []));
891
2273
  constructor() {
892
- // Effect to populate form when editing existing rule
893
2274
  effect(() => {
894
- const ruleId = this.ruleForEdit()?.id;
895
- if (this.selectedRule() && ruleId) {
896
- const rule = this.selectedRule();
897
- if (!rule)
898
- return;
899
- // Unpack rule data to form format
900
- const formData = this.unpackRuleToForm(rule);
901
- this.ruleFormControl.patchValue(formData);
902
- }
903
- else {
904
- // Default values for new rule
905
- this.ruleFormControl.reset({
906
- priority: 10,
907
- stopOnFailure: true,
908
- isActive: true,
909
- targetScope: 'Module',
910
- });
2275
+ if (this.wizardValue().appliesTo !== 'specific-module' &&
2276
+ this.wizardValue().selectedModuleKey) {
2277
+ this.patchWizardState({ selectedModuleKey: null });
911
2278
  }
912
2279
  });
913
- // Effect to apply default values when rule type changes (for new rules only)
914
2280
  effect(() => {
915
- const ruleType = this.selectedRuleType();
916
- const ruleId = this.ruleForEdit()?.id;
917
- // Only apply defaults for new rules (not editing)
918
- if (ruleType && !ruleId) {
919
- const defaults = this.getDefaultsFromSchema(ruleType);
920
- if (Object.keys(defaults).length > 0) {
921
- const currentValue = this.ruleFormControl.value ?? {};
922
- this.ruleFormControl.patchValue({ ...currentValue, ...defaults });
923
- }
2281
+ const currentAction = this.wizardValue().actionKey;
2282
+ if (!currentAction ||
2283
+ this.availableActionOptions().some((option) => option.value === currentAction)) {
2284
+ return;
924
2285
  }
2286
+ this.patchWizardState({ actionKey: null, recipeKey: null });
925
2287
  });
926
- // Effect to fetch requestSchemas and properties when scope/module changes
927
- // This runs when configurationSchema has fields with optionsSource = 'requestSchema' or 'properties'
928
2288
  effect(() => {
929
- const ruleType = this.selectedRuleType();
930
- const scope = this.targetScope();
931
- const moduleKey = this.targetModuleKey();
932
- if (!ruleType)
2289
+ const currentRecipe = this.wizardValue().recipeKey;
2290
+ if (!currentRecipe ||
2291
+ this.availableRecipeDefinitions().some((definition) => definition.key === currentRecipe)) {
933
2292
  return;
934
- // Check if any field needs requestSchema options
935
- const needsRequestSchemas = ruleType.configurationSchema.some((f) => f.optionsSource === 'requestSchema');
936
- // Check if any field needs properties options
937
- const needsProperties = ruleType.configurationSchema.some((f) => f.optionsSource === 'properties');
938
- // For Module scope, we need a target module key
939
- if (scope === 'Module' && !moduleKey) {
940
- // Clear options when module is not selected
941
- if (needsRequestSchemas) {
942
- this.facade.clearRequestSchemas();
2293
+ }
2294
+ this.patchWizardState({ recipeKey: null });
2295
+ });
2296
+ effect(() => {
2297
+ const editing = !!this.ruleForEdit()?.id;
2298
+ if (!editing) {
2299
+ this.resetWizardForm();
2300
+ if (this.legacyFormControl.value !== null) {
2301
+ this.legacyFormControl.reset(null);
943
2302
  }
944
- if (needsProperties) {
945
- this.facade.clearProperties();
2303
+ return;
2304
+ }
2305
+ const rule = this.selectedRule();
2306
+ if (!rule) {
2307
+ return;
2308
+ }
2309
+ const wizardValue = mapRuleToWizardFormValue(rule);
2310
+ if (wizardValue) {
2311
+ this.setWizardState(mapWizardFormValueToState(wizardValue));
2312
+ if (this.legacyFormControl.value !== null) {
2313
+ this.legacyFormControl.reset(null);
946
2314
  }
947
2315
  return;
948
2316
  }
949
- // Fetch options based on scope
2317
+ this.resetWizardForm();
2318
+ const nextLegacyValue = mapRuleToLegacyFormValue(rule);
2319
+ if (!areLegacyFormValuesEqual(this.legacyFormControl.value, nextLegacyValue)) {
2320
+ this.legacyFormControl.setValue(nextLegacyValue);
2321
+ }
2322
+ });
2323
+ effect(() => {
2324
+ const recipeKey = this.wizardValue().recipeKey;
2325
+ const appliesTo = this.wizardValue().appliesTo;
2326
+ const targetModuleKey = appliesTo === 'specific-module'
2327
+ ? (this.wizardValue().selectedModuleKey ?? undefined)
2328
+ : undefined;
2329
+ const targetScope = appliesTo === 'level'
2330
+ ? 'Level'
2331
+ : appliesTo === 'phase-gate'
2332
+ ? 'PhaseGate'
2333
+ : 'Module';
2334
+ const needsRequestSchemas = recipeKey === 'no-active-request';
2335
+ const needsProperties = recipeKey === 'property-value';
2336
+ if (!needsRequestSchemas && !needsProperties) {
2337
+ this.clearReferenceData();
2338
+ return;
2339
+ }
2340
+ if (targetScope === 'Module' &&
2341
+ appliesTo === 'specific-module' &&
2342
+ !targetModuleKey) {
2343
+ this.clearReferenceData();
2344
+ return;
2345
+ }
950
2346
  if (needsRequestSchemas) {
951
- this.facade.getRequestSchemas(scope, moduleKey ?? undefined);
2347
+ this.facade.getRequestSchemas(targetScope, targetModuleKey);
952
2348
  }
953
2349
  if (needsProperties) {
954
- this.facade.getProperties(scope, moduleKey ?? undefined);
2350
+ this.facade.getProperties(targetScope, targetModuleKey);
955
2351
  }
956
2352
  });
957
2353
  }
958
- /**
959
- * Extract default values from rule type's configuration schema
960
- */
961
- getDefaultsFromSchema(ruleType) {
962
- const defaults = {};
963
- for (const field of ruleType.configurationSchema) {
964
- if (field.defaultValue !== undefined) {
965
- defaults[`config_${field.key}`] = field.defaultValue;
966
- }
967
- }
968
- return defaults;
969
- }
970
2354
  ngOnInit() {
2355
+ if (!this.ruleTypes().length) {
2356
+ this.facade.getRuleTypes();
2357
+ }
2358
+ if (!this.modules().length) {
2359
+ this.facade.getModules();
2360
+ }
971
2361
  const ruleId = this.ruleForEdit()?.id;
972
2362
  if (ruleId) {
973
2363
  this.facade.getRule(ruleId);
974
2364
  }
975
2365
  }
976
- // ============================================================================
977
- // Submit Handler
978
- // ============================================================================
979
2366
  onSubmit() {
980
- if (!this.ruleFormControl.valid) {
2367
+ this.dynamicFormRef()?.form.markAllAsTouched();
2368
+ if (this.useLegacyEditor()) {
2369
+ if (!this.legacyFormControl.valid) {
2370
+ this.toast.warn(this.translate('wizard.complete-required-fields'), this.translate('warning'));
2371
+ return;
2372
+ }
2373
+ this.submitLegacyForm();
2374
+ return;
2375
+ }
2376
+ if (!this.wizardValue().actionKey || !this.wizardValue().recipeKey) {
2377
+ this.toast.warn(this.translate('wizard.summary-empty'), this.translate('warning'));
2378
+ return;
2379
+ }
2380
+ if (!this.canSubmit()) {
2381
+ this.toast.warn(this.translate('wizard.complete-required-fields'), this.translate('warning'));
2382
+ return;
2383
+ }
2384
+ const ruleType = this.selectedRuleType();
2385
+ if (!ruleType) {
981
2386
  return;
982
2387
  }
983
- const formValue = this.ruleFormControl.value;
984
2388
  const ruleId = this.ruleForEdit()?.id;
2389
+ const payloadValue = this.wizardValue();
985
2390
  if (ruleId) {
986
- // Update
987
- const payload = this.packFormToUpdateDto(formValue);
988
- this.facade.updateRule(ruleId, payload).subscribe({
989
- next: () => {
990
- this.ref.close(true);
991
- },
992
- error: () => { },
993
- });
994
- }
995
- else {
996
- // Create
997
- const payload = this.packFormToCreateDto(formValue);
998
- this.facade.addRule(payload).subscribe({
999
- next: () => {
1000
- this.ref.close(true);
1001
- },
1002
- error: () => { },
1003
- });
2391
+ this.saveUpdate(ruleId, buildUpdatePayload(payloadValue, ruleType));
2392
+ return;
1004
2393
  }
2394
+ this.saveCreate(buildCreatePayload(payloadValue, ruleType));
1005
2395
  }
1006
- // ============================================================================
1007
- // Build Configuration Section Based on Rule Type
1008
- // ============================================================================
1009
- buildConfigurationSection(ruleType) {
1010
- const t = (key) => this.translocoService.translate(`governance.${key}`);
1011
- return {
1012
- key: 'configuration',
1013
- type: 'header',
1014
- label: t('rule-configuration'),
1015
- order: 3,
1016
- fields: this.buildConfigFieldsForType(ruleType),
1017
- };
2396
+ getPhaseGateOptions() {
2397
+ return (findConfigurationSchemaField(this.selectedRuleType(), 'phaseGateSchemaId')
2398
+ ?.options ?? []);
1018
2399
  }
1019
- buildConfigFieldsForType(ruleType) {
1020
- const lang = this.translocoService.getActiveLang();
1021
- // Build fields dynamically from configurationSchema array
1022
- return ruleType.configurationSchema.map((field, index) => this.schemaFieldToFormField(field, index + 1, lang));
1023
- }
1024
- /**
1025
- * Convert a ConfigurationFieldSchema to DynamicForm BaseFieldConfig
1026
- */
1027
- schemaFieldToFormField(field, order, lang) {
1028
- const label = field.label[lang] || field.label.en;
1029
- const colSpan = field.colSpan ?? 6;
1030
- const validators = field.required ? [ValidatorConfig.required()] : [];
1031
- switch (field.type) {
1032
- case 'text':
1033
- return new TextFieldConfig({
1034
- key: `config_${field.key}`,
1035
- label,
1036
- validators,
1037
- order,
1038
- colSpan,
1039
- });
1040
- case 'textarea':
1041
- return new TextareaFieldConfig({
1042
- key: `config_${field.key}`,
1043
- label,
1044
- validators,
1045
- order,
1046
- colSpan,
1047
- });
1048
- case 'number':
1049
- return new NumberFieldConfig({
1050
- key: `config_${field.key}`,
1051
- label,
1052
- validators,
1053
- min: field.min,
1054
- max: field.max,
1055
- order,
1056
- colSpan,
1057
- });
1058
- case 'select':
1059
- return new SelectFieldConfig({
1060
- key: `config_${field.key}`,
1061
- label,
1062
- options: this.getOptionsForField(field),
1063
- optionLabel: 'label',
1064
- optionValue: 'value',
1065
- validators,
1066
- order,
1067
- colSpan,
1068
- });
1069
- case 'multi-select':
1070
- return new MultiSelectFieldConfig({
1071
- key: `config_${field.key}`,
1072
- label,
1073
- options: this.getOptionsForField(field),
1074
- optionLabel: 'label',
1075
- optionValue: 'value',
1076
- validators,
1077
- order,
1078
- colSpan,
1079
- });
1080
- case 'checkbox':
1081
- return new ToggleFieldConfig({
1082
- key: `config_${field.key}`,
1083
- label,
1084
- order,
1085
- colSpan,
1086
- });
1087
- case 'date':
1088
- return new DateFieldConfig({
1089
- key: `config_${field.key}`,
1090
- label,
1091
- validators,
1092
- order,
1093
- colSpan,
1094
- });
2400
+ getBlockingStatusOptions() {
2401
+ const field = findConfigurationSchemaField(this.selectedRuleType(), 'blockingStatuses');
2402
+ if (field?.options?.length) {
2403
+ return field.options.map((option) => option.value);
2404
+ }
2405
+ return [...DEFAULT_BLOCKING_STATUSES];
2406
+ }
2407
+ translate(key, params) {
2408
+ const lang = this.activeLang();
2409
+ return this.translocoService.translate(`governance.${key}`, params, lang);
2410
+ }
2411
+ translateScopeLabel(scope) {
2412
+ switch (scope) {
2413
+ case 'Level':
2414
+ return this.translate('scope-level');
2415
+ case 'Module':
2416
+ return this.translate('scope-module');
2417
+ case 'PhaseGate':
2418
+ return this.translate('scope-phase-gate');
1095
2419
  default:
1096
- // Fallback to text field for unknown types
1097
- return new TextFieldConfig({
1098
- key: `config_${field.key}`,
1099
- label,
1100
- validators,
1101
- order,
1102
- colSpan,
1103
- });
2420
+ return scope;
1104
2421
  }
1105
2422
  }
1106
- /**
1107
- * Get options for a field based on its optionsSource
1108
- * - 'static' or 'auto-populated': Use options from schema (already in API response)
1109
- * - 'requestSchema': Use fetched request schemas from state
1110
- * - 'properties': Use fetched properties from state
1111
- */
1112
- getOptionsForField(field) {
1113
- switch (field.optionsSource) {
1114
- case 'requestSchema':
1115
- return this.requestSchemaOptions();
1116
- case 'properties':
1117
- return this.propertyOptions();
1118
- case 'auto-populated':
1119
- case 'static':
2423
+ translateOperationLabel(operation) {
2424
+ switch (operation) {
2425
+ case 'Create':
2426
+ return this.translate('create');
2427
+ case 'Update':
2428
+ return this.translate('update');
2429
+ case 'Delete':
2430
+ return this.translate('delete');
1120
2431
  default:
1121
- // Use options directly from schema
1122
- return field.options ?? [];
2432
+ return operation;
1123
2433
  }
1124
2434
  }
1125
- // ============================================================================
1126
- // Data Transformation Helpers
1127
- // ============================================================================
1128
- unpackRuleToForm(rule) {
1129
- const formData = {
1130
- key: rule.key,
1131
- priority: rule.priority,
1132
- name_en: rule.name?.en ?? '',
1133
- name_ar: rule.name?.ar ?? '',
1134
- description_en: rule.description?.en ?? '',
1135
- description_ar: rule.description?.ar ?? '',
1136
- errorMessage_en: rule.errorMessage?.en ?? '',
1137
- errorMessage_ar: rule.errorMessage?.ar ?? '',
1138
- ruleType: rule.ruleTypeKey,
1139
- targetScope: rule.targetScope,
1140
- targetModuleKey: rule.targetModuleKey ?? '',
1141
- targetOperationKey: rule.targetOperationKey ?? '',
1142
- stopOnFailure: rule.stopOnFailure,
1143
- isActive: rule.isActive,
1144
- };
1145
- // Unpack configuration fields with config_ prefix
1146
- if (rule.configuration) {
1147
- Object.entries(rule.configuration).forEach(([key, value]) => {
1148
- formData[`config_${key}`] = value;
1149
- });
2435
+ patchWizardState(patch) {
2436
+ const currentState = normalizeWizardFormState(this.wizardFormControl.getRawValue());
2437
+ const nextState = normalizeWizardFormState({
2438
+ ...currentState,
2439
+ ...patch,
2440
+ });
2441
+ if (areWizardStatesEqual(currentState, nextState)) {
2442
+ return;
1150
2443
  }
1151
- return formData;
1152
- }
1153
- packFormToCreateDto(formValue) {
1154
- return {
1155
- key: formValue['key'],
1156
- name: {
1157
- en: formValue['name_en'],
1158
- ar: formValue['name_ar'],
1159
- },
1160
- description: formValue['description_en'] || formValue['description_ar']
1161
- ? {
1162
- en: formValue['description_en'] ?? '',
1163
- ar: formValue['description_ar'] ?? '',
1164
- }
1165
- : undefined,
1166
- errorMessage: {
1167
- en: formValue['errorMessage_en'],
1168
- ar: formValue['errorMessage_ar'],
1169
- },
1170
- ruleType: formValue['ruleType'],
1171
- targetScope: formValue['targetScope'],
1172
- targetModuleKey: formValue['targetModuleKey'] || undefined,
1173
- targetOperationKey: formValue['targetOperationKey'] || undefined,
1174
- configuration: this.extractConfiguration(formValue),
1175
- priority: formValue['priority'],
1176
- stopOnFailure: formValue['stopOnFailure'] ?? true,
1177
- };
2444
+ this.wizardFormControl.setValue(nextState);
1178
2445
  }
1179
- packFormToUpdateDto(formValue) {
1180
- return {
1181
- ...this.packFormToCreateDto(formValue),
1182
- isActive: formValue['isActive'] ?? true,
1183
- };
2446
+ setWizardState(state) {
2447
+ const currentState = normalizeWizardFormState(this.wizardFormControl.getRawValue());
2448
+ const nextState = normalizeWizardFormState(state);
2449
+ if (areWizardStatesEqual(currentState, nextState)) {
2450
+ return;
2451
+ }
2452
+ this.wizardFormControl.setValue(nextState);
1184
2453
  }
1185
- extractConfiguration(formValue) {
1186
- const config = {};
1187
- Object.entries(formValue).forEach(([key, value]) => {
1188
- if (key.startsWith('config_') &&
1189
- value !== null &&
1190
- value !== undefined &&
1191
- value !== '') {
1192
- const configKey = key.replace('config_', '');
1193
- config[configKey] = value;
1194
- }
2454
+ resetWizardForm() {
2455
+ this.setWizardState(DEFAULT_WIZARD_FORM_STATE);
2456
+ }
2457
+ clearReferenceData() {
2458
+ const hasRequestSchemas = untracked(() => this.requestSchemas().length > 0);
2459
+ const hasProperties = untracked(() => this.properties().length > 0);
2460
+ if (hasRequestSchemas) {
2461
+ this.facade.clearRequestSchemas();
2462
+ }
2463
+ if (hasProperties) {
2464
+ this.facade.clearProperties();
2465
+ }
2466
+ }
2467
+ saveCreate(payload) {
2468
+ this.facade.addRule(payload).subscribe({
2469
+ next: () => this.ref.close(true),
2470
+ error: () => { },
1195
2471
  });
1196
- return Object.keys(config).length > 0 ? config : undefined;
2472
+ }
2473
+ saveUpdate(id, payload) {
2474
+ this.facade.updateRule(id, payload).subscribe({
2475
+ next: () => this.ref.close(true),
2476
+ error: () => { },
2477
+ });
2478
+ }
2479
+ submitLegacyForm() {
2480
+ if (!this.legacyFormControl.valid) {
2481
+ return;
2482
+ }
2483
+ const formValue = this.legacyFormControl.value;
2484
+ if (!formValue) {
2485
+ return;
2486
+ }
2487
+ const ruleId = this.ruleForEdit()?.id;
2488
+ if (ruleId) {
2489
+ this.saveUpdate(ruleId, packLegacyFormToUpdateDto(formValue));
2490
+ return;
2491
+ }
2492
+ this.saveCreate(packLegacyFormToCreateDto(formValue));
2493
+ }
2494
+ ruleTypeOptions() {
2495
+ const lang = this.langCode();
2496
+ return this.ruleTypes().map((ruleType) => ({
2497
+ label: ruleType.name[lang] || ruleType.name.en,
2498
+ value: readGovernanceRuleTypeKey(ruleType),
2499
+ }));
1197
2500
  }
1198
2501
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: GovernanceRuleForm, deps: [], target: i0.ɵɵFactoryTarget.Component });
1199
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.3", type: GovernanceRuleForm, isStandalone: true, selector: "mt-governance-rule-form", inputs: { ruleForEdit: { classPropertyName: "ruleForEdit", publicName: "ruleForEdit", isSignal: true, isRequired: false, transformFunction: null } }, providers: [DialogService], ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'governance'\">\r\n <div\r\n [class]=\"'flex flex-col gap-3 p-4 overflow-y-auto ' + modal.contentClass\"\r\n >\r\n @if (isLoadingRule()) {\r\n <p-skeleton class=\"my-4 mt-7\" height=\"3rem\" />\r\n <p-skeleton class=\"my-4\" height=\"3rem\" />\r\n <p-skeleton class=\"my-4\" height=\"6rem\" />\r\n <p-skeleton class=\"my-4\" height=\"3rem\" />\r\n <p-skeleton class=\"my-4\" height=\"3rem\" />\r\n } @else {\r\n <mt-dynamic-form\r\n [formConfig]=\"formConfig()\"\r\n [formControl]=\"ruleFormControl\"\r\n />\r\n }\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n [label]=\"'cancel' | transloco\"\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n (click)=\"ref.close()\"\r\n />\r\n <mt-button\r\n [label]=\"ruleForEdit() ? t('update') : t('create')\"\r\n [loading]=\"isAddingRule() || isUpdatingRule()\"\r\n [disabled]=\"!ruleFormControl.valid\"\r\n (click)=\"onSubmit()\"\r\n />\r\n </div>\r\n</ng-container>\r\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "ngmodule", type: SkeletonModule }, { kind: "component", type: i1.Skeleton, selector: "p-skeleton", inputs: ["styleClass", "shape", "animation", "borderRadius", "size", "width", "height"] }, { kind: "component", type: DynamicForm, selector: "mt-dynamic-form", inputs: ["formConfig", "forcedHiddenFieldKeys", "preserveForcedHiddenValues", "visibleSectionKeys"], outputs: ["runtimeMessagesChange"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }] });
2502
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.3", type: GovernanceRuleForm, isStandalone: true, selector: "mt-governance-rule-form", inputs: { ruleForEdit: { classPropertyName: "ruleForEdit", publicName: "ruleForEdit", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block h-full" }, viewQueries: [{ propertyName: "dynamicFormRef", first: true, predicate: DynamicForm, descendants: true, isSignal: true }], ngImport: i0, template: "<ng-container *transloco=\"let t; prefix: 'governance'\">\n <div\n [class]=\"\n 'governance-rule-form-content flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4 sm:p-5 ' +\n modal.contentClass\n \"\n >\n @if (isInitializing()) {\n <div class=\"space-y-4\">\n <p-skeleton height=\"4rem\" />\n <p-skeleton height=\"12rem\" />\n <p-skeleton height=\"12rem\" />\n </div>\n } @else if (useLegacyEditor()) {\n <section class=\"rounded-2xl border border-amber-200 bg-amber-50/70 p-4\">\n <p class=\"m-0 text-sm font-semibold text-slate-900\">\n {{ t(\"wizard.legacy-title\") }}\n </p>\n <p class=\"mt-1 mb-0 text-sm leading-6 text-slate-600\">\n {{ t(\"wizard.legacy-description\") }}\n </p>\n </section>\n\n <mt-dynamic-form\n [formConfig]=\"legacyFormConfig()\"\n [formControl]=\"legacyFormControl\"\n />\n } @else {\n @if (noRecipesAvailable()) {\n <section\n class=\"rounded-2xl border border-orange-200 bg-orange-50/80 p-4\"\n >\n <p class=\"m-0 text-sm font-semibold text-slate-900\">\n {{ t(\"wizard.no-recipes-title\") }}\n </p>\n <p class=\"mt-1 mb-0 text-sm leading-6 text-slate-600\">\n {{ t(\"wizard.no-recipes-description\") }}\n </p>\n </section>\n }\n\n <mt-dynamic-form\n [formConfig]=\"wizardFormConfig()\"\n [formControl]=\"wizardFormControl\"\n />\n\n @if (hasReviewSummary()) {\n <section class=\"rounded-2xl border border-slate-900 bg-slate-900 p-4\">\n <p\n class=\"m-0 text-xs font-semibold uppercase tracking-[0.18em] text-white/70\"\n >\n {{ t(\"wizard.summary-title\") }}\n </p>\n <p class=\"mt-2 mb-0 text-sm leading-6 text-white\">\n {{ reviewSummary() }}\n </p>\n </section>\n }\n }\n </div>\n\n <div\n [class]=\"\n 'governance-rule-form-footer ' +\n modal.footerClass +\n ' flex-col gap-2 sm:flex-row sm:items-center sm:justify-end'\n \"\n >\n <mt-button\n variant=\"outlined\"\n [label]=\"'cancel' | transloco\"\n (click)=\"ref.close()\"\n styleClass=\"w-full sm:w-auto\"\n />\n\n <mt-button\n [label]=\"ruleForEdit() ? t('update') : t('create')\"\n [loading]=\"isAddingRule() || isUpdatingRule()\"\n [disabled]=\"isAddingRule() || isUpdatingRule()\"\n (click)=\"onSubmit()\"\n styleClass=\"w-full sm:w-auto\"\n />\n </div>\n</ng-container>\n", styles: [":host{display:block;min-height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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: DynamicForm, selector: "mt-dynamic-form", inputs: ["formConfig", "forcedHiddenFieldKeys", "preserveForcedHiddenValues", "visibleSectionKeys"], outputs: ["runtimeMessagesChange"] }, { 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: "ngmodule", type: SkeletonModule }, { kind: "component", type: i2.Skeleton, selector: "p-skeleton", inputs: ["styleClass", "shape", "animation", "borderRadius", "size", "width", "height"] }, { kind: "directive", type: TranslocoDirective, selector: "[transloco]", inputs: ["transloco", "translocoParams", "translocoScope", "translocoRead", "translocoPrefix", "translocoLang", "translocoLoadingTpl"] }, { kind: "pipe", type: TranslocoPipe, name: "transloco" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1200
2503
  }
1201
2504
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: GovernanceRuleForm, decorators: [{
1202
2505
  type: Component,
1203
- args: [{ selector: 'mt-governance-rule-form', standalone: true, imports: [
2506
+ args: [{ selector: 'mt-governance-rule-form', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
1204
2507
  CommonModule,
1205
2508
  Button,
1206
- TranslocoDirective,
1207
- SkeletonModule,
1208
2509
  DynamicForm,
1209
2510
  ReactiveFormsModule,
2511
+ SkeletonModule,
2512
+ TranslocoDirective,
1210
2513
  TranslocoPipe,
1211
- ], providers: [DialogService], template: "<ng-container *transloco=\"let t; prefix: 'governance'\">\r\n <div\r\n [class]=\"'flex flex-col gap-3 p-4 overflow-y-auto ' + modal.contentClass\"\r\n >\r\n @if (isLoadingRule()) {\r\n <p-skeleton class=\"my-4 mt-7\" height=\"3rem\" />\r\n <p-skeleton class=\"my-4\" height=\"3rem\" />\r\n <p-skeleton class=\"my-4\" height=\"6rem\" />\r\n <p-skeleton class=\"my-4\" height=\"3rem\" />\r\n <p-skeleton class=\"my-4\" height=\"3rem\" />\r\n } @else {\r\n <mt-dynamic-form\r\n [formConfig]=\"formConfig()\"\r\n [formControl]=\"ruleFormControl\"\r\n />\r\n }\r\n </div>\r\n\r\n <div [class]=\"modal.footerClass\">\r\n <mt-button\r\n [label]=\"'cancel' | transloco\"\r\n severity=\"secondary\"\r\n variant=\"outlined\"\r\n (click)=\"ref.close()\"\r\n />\r\n <mt-button\r\n [label]=\"ruleForEdit() ? t('update') : t('create')\"\r\n [loading]=\"isAddingRule() || isUpdatingRule()\"\r\n [disabled]=\"!ruleFormControl.valid\"\r\n (click)=\"onSubmit()\"\r\n />\r\n </div>\r\n</ng-container>\r\n" }]
1212
- }], ctorParameters: () => [], propDecorators: { ruleForEdit: [{ type: i0.Input, args: [{ isSignal: true, alias: "ruleForEdit", required: false }] }] } });
2514
+ ], host: {
2515
+ class: 'block h-full',
2516
+ }, template: "<ng-container *transloco=\"let t; prefix: 'governance'\">\n <div\n [class]=\"\n 'governance-rule-form-content flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4 sm:p-5 ' +\n modal.contentClass\n \"\n >\n @if (isInitializing()) {\n <div class=\"space-y-4\">\n <p-skeleton height=\"4rem\" />\n <p-skeleton height=\"12rem\" />\n <p-skeleton height=\"12rem\" />\n </div>\n } @else if (useLegacyEditor()) {\n <section class=\"rounded-2xl border border-amber-200 bg-amber-50/70 p-4\">\n <p class=\"m-0 text-sm font-semibold text-slate-900\">\n {{ t(\"wizard.legacy-title\") }}\n </p>\n <p class=\"mt-1 mb-0 text-sm leading-6 text-slate-600\">\n {{ t(\"wizard.legacy-description\") }}\n </p>\n </section>\n\n <mt-dynamic-form\n [formConfig]=\"legacyFormConfig()\"\n [formControl]=\"legacyFormControl\"\n />\n } @else {\n @if (noRecipesAvailable()) {\n <section\n class=\"rounded-2xl border border-orange-200 bg-orange-50/80 p-4\"\n >\n <p class=\"m-0 text-sm font-semibold text-slate-900\">\n {{ t(\"wizard.no-recipes-title\") }}\n </p>\n <p class=\"mt-1 mb-0 text-sm leading-6 text-slate-600\">\n {{ t(\"wizard.no-recipes-description\") }}\n </p>\n </section>\n }\n\n <mt-dynamic-form\n [formConfig]=\"wizardFormConfig()\"\n [formControl]=\"wizardFormControl\"\n />\n\n @if (hasReviewSummary()) {\n <section class=\"rounded-2xl border border-slate-900 bg-slate-900 p-4\">\n <p\n class=\"m-0 text-xs font-semibold uppercase tracking-[0.18em] text-white/70\"\n >\n {{ t(\"wizard.summary-title\") }}\n </p>\n <p class=\"mt-2 mb-0 text-sm leading-6 text-white\">\n {{ reviewSummary() }}\n </p>\n </section>\n }\n }\n </div>\n\n <div\n [class]=\"\n 'governance-rule-form-footer ' +\n modal.footerClass +\n ' flex-col gap-2 sm:flex-row sm:items-center sm:justify-end'\n \"\n >\n <mt-button\n variant=\"outlined\"\n [label]=\"'cancel' | transloco\"\n (click)=\"ref.close()\"\n styleClass=\"w-full sm:w-auto\"\n />\n\n <mt-button\n [label]=\"ruleForEdit() ? t('update') : t('create')\"\n [loading]=\"isAddingRule() || isUpdatingRule()\"\n [disabled]=\"isAddingRule() || isUpdatingRule()\"\n (click)=\"onSubmit()\"\n styleClass=\"w-full sm:w-auto\"\n />\n </div>\n</ng-container>\n", styles: [":host{display:block;min-height:100%}\n"] }]
2517
+ }], ctorParameters: () => [], propDecorators: { ruleForEdit: [{ type: i0.Input, args: [{ isSignal: true, alias: "ruleForEdit", required: false }] }], dynamicFormRef: [{ type: i0.ViewChild, args: [i0.forwardRef(() => DynamicForm), { isSignal: true }] }] } });
1213
2518
 
1214
2519
  class GovernanceRulesList {
1215
2520
  // ============================================================================