@masterteam/forms 0.0.59 → 0.0.60

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.
@@ -4,6 +4,7 @@ import * as i2 from '@angular/forms';
4
4
  import { FormControl, ReactiveFormsModule } from '@angular/forms';
5
5
  import * as i1 from '@angular/common';
6
6
  import { CommonModule } from '@angular/common';
7
+ import { switchMap, EMPTY, map } from 'rxjs';
7
8
  import { Skeleton } from 'primeng/skeleton';
8
9
  import { Button } from '@masterteam/components/button';
9
10
  import { EntitiesPreview } from '@masterteam/components/entities';
@@ -35,6 +36,13 @@ class ClientFormApiService {
35
36
  submit(request) {
36
37
  return this.http.post('process-submit', request);
37
38
  }
39
+ /**
40
+ * Validate the exact submit payload against backend-owned form rules
41
+ * before the real submit call.
42
+ */
43
+ validate(request) {
44
+ return this.http.post('process-submit/validate', request);
45
+ }
38
46
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: ClientFormApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
39
47
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: ClientFormApiService, providedIn: 'root' });
40
48
  }
@@ -151,7 +159,6 @@ const WIDTH_TO_ENTITY_SIZE = {
151
159
  * @param lookups Available lookup definitions for resolving Lookup/LookupMultiSelect options
152
160
  */
153
161
  function mapToDynamicFormConfig(config, lang = 'en', mode = 'create', lookups = [], context = null, readonly = false) {
154
- const validationRules = mapValidationRules(config, lang);
155
162
  return {
156
163
  sections: config.sections
157
164
  .slice()
@@ -178,7 +185,6 @@ function mapToDynamicFormConfig(config, lang = 'en', mode = 'create', lookups =
178
185
  };
179
186
  })
180
187
  .filter((section) => section.fields.length > 0),
181
- validationRules,
182
188
  };
183
189
  }
184
190
  /**
@@ -454,19 +460,6 @@ function buildFormulaCondition(field) {
454
460
  mode: 'auto',
455
461
  };
456
462
  }
457
- function mapValidationRules(config, lang) {
458
- return (config.validations ?? []).map((rule) => ({
459
- id: rule.id,
460
- formulaTokens: rule.formulaTokens,
461
- formulaText: rule.formulaText,
462
- message: rule.message?.[lang] ??
463
- rule.message?.en ??
464
- rule.message?.ar ??
465
- 'Validation rule failed',
466
- severity: rule.severity,
467
- enabled: rule.enabled,
468
- }));
469
- }
470
463
  /**
471
464
  * Resolve lookup items for Lookup / LookupMultiSelect viewTypes.
472
465
  *
@@ -717,8 +710,14 @@ class ClientForm {
717
710
  state = inject(ClientFormStateService);
718
711
  loadSub;
719
712
  submitSub;
713
+ formValueSub;
720
714
  hasStartedLoad = signal(false, ...(ngDevMode ? [{ debugName: "hasStartedLoad" }] : []));
721
- runtimeMessages = signal([], ...(ngDevMode ? [{ debugName: "runtimeMessages" }] : []));
715
+ formRuntimeMessages = signal([], ...(ngDevMode ? [{ debugName: "formRuntimeMessages" }] : []));
716
+ submitValidationMessages = signal([], ...(ngDevMode ? [{ debugName: "submitValidationMessages" }] : []));
717
+ runtimeMessages = computed(() => [
718
+ ...this.formRuntimeMessages(),
719
+ ...this.submitValidationMessages(),
720
+ ], ...(ngDevMode ? [{ debugName: "runtimeMessages" }] : []));
722
721
  translocoService = inject(TranslocoService);
723
722
  // ============================================================================
724
723
  // Public State Signals (for parent access via viewChild)
@@ -950,6 +949,11 @@ class ClientForm {
950
949
  // Effects
951
950
  // ============================================================================
952
951
  constructor() {
952
+ this.formValueSub = this.formControl.valueChanges.subscribe(() => {
953
+ if (this.submitValidationMessages().length > 0) {
954
+ this.submitValidationMessages.set([]);
955
+ }
956
+ });
953
957
  // Auto-load when inputs are ready
954
958
  effect(() => {
955
959
  const autoLoad = this.autoLoad();
@@ -1000,7 +1004,8 @@ class ClientForm {
1000
1004
  return;
1001
1005
  this.loadSub?.unsubscribe();
1002
1006
  this.hasStartedLoad.set(true);
1003
- this.runtimeMessages.set([]);
1007
+ this.formRuntimeMessages.set([]);
1008
+ this.submitValidationMessages.set([]);
1004
1009
  this.state.loading.set(true);
1005
1010
  this.state.error.set(null);
1006
1011
  this.state.submitResponse.set(null);
@@ -1049,8 +1054,30 @@ class ClientForm {
1049
1054
  this.state.submitting.set(true);
1050
1055
  this.state.submitError.set(null);
1051
1056
  const request = this.buildSubmitRequest(loadResponse);
1052
- this.submitSub = this.api.submit(request).subscribe({
1053
- next: (response) => {
1057
+ this.submitValidationMessages.set([]);
1058
+ this.submitSub = this.api
1059
+ .validate(request)
1060
+ .pipe(switchMap((validationResponse) => {
1061
+ const validation = validationResponse.data;
1062
+ if (!validation) {
1063
+ const msg = validationResponse.message ?? 'Failed to validate form';
1064
+ this.state.submitting.set(false);
1065
+ this.state.setSubmitError(msg);
1066
+ this.errored.emit(msg);
1067
+ return EMPTY;
1068
+ }
1069
+ this.submitValidationMessages.set(this.mapSubmitValidationMessages(validation.results ?? []));
1070
+ if (validation.hasBlockingFailures) {
1071
+ this.state.submitting.set(false);
1072
+ return EMPTY;
1073
+ }
1074
+ return this.api.submit(request).pipe(map((response) => ({
1075
+ kind: 'submit',
1076
+ response,
1077
+ })));
1078
+ }))
1079
+ .subscribe({
1080
+ next: ({ response }) => {
1054
1081
  this.state.submitting.set(false);
1055
1082
  if (response.data) {
1056
1083
  this.state.setSubmitResponse(response.data);
@@ -1063,7 +1090,9 @@ class ClientForm {
1063
1090
  }
1064
1091
  },
1065
1092
  error: (err) => {
1066
- const msg = err?.error?.message ?? err?.message ?? 'Failed to submit form';
1093
+ const msg = err?.error?.message ??
1094
+ err?.message ??
1095
+ 'Failed to validate or submit form';
1067
1096
  this.state.setSubmitError(msg);
1068
1097
  this.errored.emit(msg);
1069
1098
  },
@@ -1094,7 +1123,7 @@ class ClientForm {
1094
1123
  * Check whether the current form state is valid.
1095
1124
  */
1096
1125
  isValid() {
1097
- return this.formControl.valid;
1126
+ return this.formControl.valid && this.runtimeErrors().length === 0;
1098
1127
  }
1099
1128
  /**
1100
1129
  * Reset the component to its initial state.
@@ -1103,11 +1132,12 @@ class ClientForm {
1103
1132
  this.loadSub?.unsubscribe();
1104
1133
  this.submitSub?.unsubscribe();
1105
1134
  this.formControl.reset({});
1106
- this.runtimeMessages.set([]);
1135
+ this.formRuntimeMessages.set([]);
1136
+ this.submitValidationMessages.set([]);
1107
1137
  this.state.reset();
1108
1138
  }
1109
1139
  onRuntimeMessagesChange(messages) {
1110
- this.runtimeMessages.set(messages ?? []);
1140
+ this.formRuntimeMessages.set(messages ?? []);
1111
1141
  }
1112
1142
  onStepChange(value) {
1113
1143
  const count = this.stepSections().length;
@@ -1155,6 +1185,7 @@ class ClientForm {
1155
1185
  ngOnDestroy() {
1156
1186
  this.loadSub?.unsubscribe();
1157
1187
  this.submitSub?.unsubscribe();
1188
+ this.formValueSub?.unsubscribe();
1158
1189
  }
1159
1190
  // ============================================================================
1160
1191
  // Private Helpers
@@ -1229,6 +1260,45 @@ class ClientForm {
1229
1260
  loadResponse,
1230
1261
  });
1231
1262
  }
1263
+ mapSubmitValidationMessages(results) {
1264
+ return results
1265
+ .filter((result) => !result.passed)
1266
+ .map((result) => ({
1267
+ code: result.isEvaluationError
1268
+ ? 'FORMULA_EVALUATION_ERROR'
1269
+ : 'FORMULA_FALSE',
1270
+ severity: result.severity,
1271
+ message: this.resolveSubmitValidationMessage(result),
1272
+ ruleId: result.ruleId,
1273
+ }));
1274
+ }
1275
+ resolveSubmitValidationMessage(result) {
1276
+ const translatedMessage = this.resolveTranslatableValue(result.message);
1277
+ if (translatedMessage) {
1278
+ return translatedMessage;
1279
+ }
1280
+ const errorMessage = result.errorMessage?.trim();
1281
+ if (errorMessage) {
1282
+ return errorMessage;
1283
+ }
1284
+ return 'Form validation failed';
1285
+ }
1286
+ resolveTranslatableValue(value) {
1287
+ if (typeof value === 'string') {
1288
+ const normalized = value.trim();
1289
+ return normalized.length > 0 ? normalized : null;
1290
+ }
1291
+ if (!value) {
1292
+ return null;
1293
+ }
1294
+ const lang = this.lang();
1295
+ const localized = value.display ?? value[lang] ?? value.en ?? value.ar ?? null;
1296
+ if (typeof localized !== 'string') {
1297
+ return null;
1298
+ }
1299
+ const normalized = localized.trim();
1300
+ return normalized.length > 0 ? normalized : null;
1301
+ }
1232
1302
  resolveStepTimelineState(stepValue, currentStep) {
1233
1303
  if (stepValue < currentStep) {
1234
1304
  return 'completed';