@masterteam/governance 0.0.7 → 0.0.9

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