@masterteam/forms 0.0.59 → 0.0.61

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
  /**
@@ -280,6 +286,7 @@ function mapPreviewSectionsToEntities(config, values, lang = 'en', mode = 'creat
280
286
  configuration: {
281
287
  ...property.configuration,
282
288
  size: WIDTH_TO_ENTITY_SIZE[field.width] ?? 24,
289
+ labelPosition: 'top',
283
290
  },
284
291
  };
285
292
  }),
@@ -454,19 +461,6 @@ function buildFormulaCondition(field) {
454
461
  mode: 'auto',
455
462
  };
456
463
  }
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
464
  /**
471
465
  * Resolve lookup items for Lookup / LookupMultiSelect viewTypes.
472
466
  *
@@ -717,8 +711,14 @@ class ClientForm {
717
711
  state = inject(ClientFormStateService);
718
712
  loadSub;
719
713
  submitSub;
714
+ formValueSub;
720
715
  hasStartedLoad = signal(false, ...(ngDevMode ? [{ debugName: "hasStartedLoad" }] : []));
721
- runtimeMessages = signal([], ...(ngDevMode ? [{ debugName: "runtimeMessages" }] : []));
716
+ formRuntimeMessages = signal([], ...(ngDevMode ? [{ debugName: "formRuntimeMessages" }] : []));
717
+ submitValidationMessages = signal([], ...(ngDevMode ? [{ debugName: "submitValidationMessages" }] : []));
718
+ runtimeMessages = computed(() => [
719
+ ...this.formRuntimeMessages(),
720
+ ...this.submitValidationMessages(),
721
+ ], ...(ngDevMode ? [{ debugName: "runtimeMessages" }] : []));
722
722
  translocoService = inject(TranslocoService);
723
723
  // ============================================================================
724
724
  // Public State Signals (for parent access via viewChild)
@@ -950,6 +950,11 @@ class ClientForm {
950
950
  // Effects
951
951
  // ============================================================================
952
952
  constructor() {
953
+ this.formValueSub = this.formControl.valueChanges.subscribe(() => {
954
+ if (this.submitValidationMessages().length > 0) {
955
+ this.submitValidationMessages.set([]);
956
+ }
957
+ });
953
958
  // Auto-load when inputs are ready
954
959
  effect(() => {
955
960
  const autoLoad = this.autoLoad();
@@ -1000,7 +1005,8 @@ class ClientForm {
1000
1005
  return;
1001
1006
  this.loadSub?.unsubscribe();
1002
1007
  this.hasStartedLoad.set(true);
1003
- this.runtimeMessages.set([]);
1008
+ this.formRuntimeMessages.set([]);
1009
+ this.submitValidationMessages.set([]);
1004
1010
  this.state.loading.set(true);
1005
1011
  this.state.error.set(null);
1006
1012
  this.state.submitResponse.set(null);
@@ -1049,8 +1055,30 @@ class ClientForm {
1049
1055
  this.state.submitting.set(true);
1050
1056
  this.state.submitError.set(null);
1051
1057
  const request = this.buildSubmitRequest(loadResponse);
1052
- this.submitSub = this.api.submit(request).subscribe({
1053
- next: (response) => {
1058
+ this.submitValidationMessages.set([]);
1059
+ this.submitSub = this.api
1060
+ .validate(request)
1061
+ .pipe(switchMap((validationResponse) => {
1062
+ const validation = validationResponse.data;
1063
+ if (!validation) {
1064
+ const msg = validationResponse.message ?? 'Failed to validate form';
1065
+ this.state.submitting.set(false);
1066
+ this.state.setSubmitError(msg);
1067
+ this.errored.emit(msg);
1068
+ return EMPTY;
1069
+ }
1070
+ this.submitValidationMessages.set(this.mapSubmitValidationMessages(validation.results ?? []));
1071
+ if (validation.hasBlockingFailures) {
1072
+ this.state.submitting.set(false);
1073
+ return EMPTY;
1074
+ }
1075
+ return this.api.submit(request).pipe(map((response) => ({
1076
+ kind: 'submit',
1077
+ response,
1078
+ })));
1079
+ }))
1080
+ .subscribe({
1081
+ next: ({ response }) => {
1054
1082
  this.state.submitting.set(false);
1055
1083
  if (response.data) {
1056
1084
  this.state.setSubmitResponse(response.data);
@@ -1063,7 +1091,9 @@ class ClientForm {
1063
1091
  }
1064
1092
  },
1065
1093
  error: (err) => {
1066
- const msg = err?.error?.message ?? err?.message ?? 'Failed to submit form';
1094
+ const msg = err?.error?.message ??
1095
+ err?.message ??
1096
+ 'Failed to validate or submit form';
1067
1097
  this.state.setSubmitError(msg);
1068
1098
  this.errored.emit(msg);
1069
1099
  },
@@ -1094,7 +1124,7 @@ class ClientForm {
1094
1124
  * Check whether the current form state is valid.
1095
1125
  */
1096
1126
  isValid() {
1097
- return this.formControl.valid;
1127
+ return this.formControl.valid && this.runtimeErrors().length === 0;
1098
1128
  }
1099
1129
  /**
1100
1130
  * Reset the component to its initial state.
@@ -1103,11 +1133,12 @@ class ClientForm {
1103
1133
  this.loadSub?.unsubscribe();
1104
1134
  this.submitSub?.unsubscribe();
1105
1135
  this.formControl.reset({});
1106
- this.runtimeMessages.set([]);
1136
+ this.formRuntimeMessages.set([]);
1137
+ this.submitValidationMessages.set([]);
1107
1138
  this.state.reset();
1108
1139
  }
1109
1140
  onRuntimeMessagesChange(messages) {
1110
- this.runtimeMessages.set(messages ?? []);
1141
+ this.formRuntimeMessages.set(messages ?? []);
1111
1142
  }
1112
1143
  onStepChange(value) {
1113
1144
  const count = this.stepSections().length;
@@ -1155,6 +1186,7 @@ class ClientForm {
1155
1186
  ngOnDestroy() {
1156
1187
  this.loadSub?.unsubscribe();
1157
1188
  this.submitSub?.unsubscribe();
1189
+ this.formValueSub?.unsubscribe();
1158
1190
  }
1159
1191
  // ============================================================================
1160
1192
  // Private Helpers
@@ -1229,6 +1261,45 @@ class ClientForm {
1229
1261
  loadResponse,
1230
1262
  });
1231
1263
  }
1264
+ mapSubmitValidationMessages(results) {
1265
+ return results
1266
+ .filter((result) => !result.passed)
1267
+ .map((result) => ({
1268
+ code: result.isEvaluationError
1269
+ ? 'FORMULA_EVALUATION_ERROR'
1270
+ : 'FORMULA_FALSE',
1271
+ severity: result.severity,
1272
+ message: this.resolveSubmitValidationMessage(result),
1273
+ ruleId: result.ruleId,
1274
+ }));
1275
+ }
1276
+ resolveSubmitValidationMessage(result) {
1277
+ const translatedMessage = this.resolveTranslatableValue(result.message);
1278
+ if (translatedMessage) {
1279
+ return translatedMessage;
1280
+ }
1281
+ const errorMessage = result.errorMessage?.trim();
1282
+ if (errorMessage) {
1283
+ return errorMessage;
1284
+ }
1285
+ return 'Form validation failed';
1286
+ }
1287
+ resolveTranslatableValue(value) {
1288
+ if (typeof value === 'string') {
1289
+ const normalized = value.trim();
1290
+ return normalized.length > 0 ? normalized : null;
1291
+ }
1292
+ if (!value) {
1293
+ return null;
1294
+ }
1295
+ const lang = this.lang();
1296
+ const localized = value.display ?? value[lang] ?? value.en ?? value.ar ?? null;
1297
+ if (typeof localized !== 'string') {
1298
+ return null;
1299
+ }
1300
+ const normalized = localized.trim();
1301
+ return normalized.length > 0 ? normalized : null;
1302
+ }
1232
1303
  resolveStepTimelineState(stepValue, currentStep) {
1233
1304
  if (stepValue < currentStep) {
1234
1305
  return 'completed';