diff --git a/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.html b/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.html index b7204d8fa..03259b53e 100644 --- a/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.html +++ b/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.html @@ -182,7 +182,7 @@

*ngIf="(keyAttributesListFromStructure.length || hasKeyAttributesFromURL) && permissions.edit && pageMode !== 'view'" class="actions__continue" data-testid="record-save-and-continue-editing-button" - [disabled]="submitting || editRowForm.form.invalid" + [disabled]="submitting || !isFormValid" (click)="handleRowSubmitting(true)"> Save and continue editing @@ -190,20 +190,20 @@

diff --git a/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.ts b/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.ts index 513796953..1fc6c33ca 100644 --- a/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.ts +++ b/frontend/src/app/components/db-table-row-edit/db-table-row-edit.component.ts @@ -16,6 +16,7 @@ import { BreadcrumbsComponent } from '../ui-components/breadcrumbs/breadcrumbs.c import { CommonModule } from '@angular/common'; import { CompanyService } from 'src/app/services/company.service'; import { ConnectionsService } from 'src/app/services/connections.service'; +import { FormValidationService } from 'src/app/services/form-validation.service'; import { DBtype } from 'src/app/models/connection'; import { DbActionLinkDialogComponent } from '../dashboard/db-table-view/db-action-link-dialog/db-action-link-dialog.component'; import { DbTableRowViewComponent } from '../dashboard/db-table-view/db-table-row-view/db-table-row-view.component'; @@ -132,6 +133,7 @@ export class DbTableRowEditComponent implements OnInit { private _notifications: NotificationsService, private _tableState: TableStateService, private _company: CompanyService, + private _formValidation: FormValidationService, private route: ActivatedRoute, private ngZone: NgZone, public router: Router, @@ -148,6 +150,9 @@ export class DbTableRowEditComponent implements OnInit { } ngOnInit(): void { + // Clear any previous validation states when component initializes + this._formValidation.clearAll(); + this.loading = true; this.connectionID = this._connections.currentConnectionID; this.tableFiltersUrlString = JsonURL.stringify(this._tableState.getBackUrlFilters()); @@ -450,6 +455,10 @@ export class DbTableRowEditComponent implements OnInit { return recordEditTypes[this.connectionType] } + get isFormValid(): boolean { + return this._formValidation.isFormValid(); + } + get currentConnection() { return this._connections.currentConnection; } @@ -616,6 +625,18 @@ export class DbTableRowEditComponent implements OnInit { } handleRowSubmitting(continueEditing: boolean) { + // Double-check validation before submitting + if (!this._formValidation.isFormValid()) { + const invalidFields = this._formValidation.getInvalidFields(); + console.warn('Form has validation errors in fields:', invalidFields); + this._notifications.showAlert( + AlertType.Error, + 'Please fix validation errors before submitting', + [] + ); + return; + } + if (this.hasKeyAttributesFromURL && this.pageAction !== 'dub') { this.updateRow(continueEditing); } else { diff --git a/frontend/src/app/components/ui-components/record-edit-fields/number/number.component.html b/frontend/src/app/components/ui-components/record-edit-fields/number/number.component.html index 9511a12cc..4c6aa163d 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/number/number.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/number/number.component.html @@ -1,6 +1,7 @@ {{normalizedLabel}} diff --git a/frontend/src/app/components/ui-components/record-edit-fields/number/number.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/number/number.component.ts index adafd3c39..f3c3212a7 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/number/number.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/number/number.component.ts @@ -1,9 +1,10 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, ViewChild, AfterViewInit } from '@angular/core'; import { BaseEditFieldComponent } from '../base-row-field/base-row-field.component'; -import { FormsModule } from '@angular/forms'; +import { FormsModule, NgModel } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; +import { FormValidationService } from 'src/app/services/form-validation.service'; @Component({ selector: 'app-edit-number', @@ -11,8 +12,38 @@ import { MatInputModule } from '@angular/material/input'; styleUrls: ['./number.component.css'], imports: [MatFormFieldModule, MatInputModule, FormsModule] }) -export class NumberEditComponent extends BaseEditFieldComponent { +export class NumberEditComponent extends BaseEditFieldComponent implements AfterViewInit { @Input() value: number; + @ViewChild('numberField', { read: NgModel }) numberField: NgModel; static type = 'number'; + + constructor(private formValidationService: FormValidationService) { + super(); + } + + ngAfterViewInit(): void { + // Subscribe to value and status changes + if (this.numberField) { + // Initial validation state + this.updateFieldValidation(); + + // Listen for changes + this.numberField.valueChanges?.subscribe(() => { + this.updateFieldValidation(); + }); + + this.numberField.statusChanges?.subscribe(() => { + this.updateFieldValidation(); + }); + } + } + + private updateFieldValidation(): void { + if (this.numberField) { + const fieldIdentifier = `${this.label}-${this.key}`; + const isValid = this.numberField.valid || (!this.required && (this.value === null || this.value === undefined)); + this.formValidationService.updateFieldValidity(fieldIdentifier, isValid, this.numberField.errors); + } + } } diff --git a/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.html b/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.html index 49c432cb0..1a3d4fc5b 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.html +++ b/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.html @@ -7,10 +7,14 @@ textValidator [validateType]="validateType" [regexPattern]="regexPattern" + fieldValidation + [fieldKey]="key" + [fieldLabel]="label" + [fieldRequired]="required" attr.data-testid="record-{{label}}-text" [(ngModel)]="value" (ngModelChange)="onFieldChange.emit($event)">
{{value.length}} / {{maxLength}}
This field is required. Maximum length is {{maxLength}} characters. {{getValidationErrorMessage()}} -
\ No newline at end of file + diff --git a/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.ts b/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.ts index 3aa4f65bb..e49b15044 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.ts @@ -6,6 +6,7 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { CommonModule } from '@angular/common'; import { TextValidatorDirective } from 'src/app/directives/text-validator.directive'; +import { FieldValidationDirective } from 'src/app/directives/field-validation.directive'; @Injectable() @@ -13,7 +14,7 @@ import { TextValidatorDirective } from 'src/app/directives/text-validator.direct selector: 'app-edit-text', templateUrl: './text.component.html', styleUrls: ['./text.component.css'], - imports: [CommonModule, MatFormFieldModule, MatInputModule, FormsModule, TextValidatorDirective] + imports: [CommonModule, MatFormFieldModule, MatInputModule, FormsModule, TextValidatorDirective, FieldValidationDirective] }) export class TextEditComponent extends BaseEditFieldComponent implements OnInit { @Input() value: string; diff --git a/frontend/src/app/directives/field-validation.directive.ts b/frontend/src/app/directives/field-validation.directive.ts new file mode 100644 index 000000000..3e087ccb2 --- /dev/null +++ b/frontend/src/app/directives/field-validation.directive.ts @@ -0,0 +1,87 @@ +import { Directive, Input, OnDestroy, Optional } from '@angular/core'; +import { NgModel } from '@angular/forms'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { FormValidationService } from '../services/form-validation.service'; + +@Directive({ + selector: '[fieldValidation][ngModel]' +}) +export class FieldValidationDirective implements OnDestroy { + @Input() fieldKey: string; + @Input() fieldLabel: string; + @Input() fieldRequired: boolean = false; + + private destroy$ = new Subject(); + private fieldIdentifier: string; + + constructor( + @Optional() private ngModel: NgModel, + private formValidationService: FormValidationService + ) {} + + ngAfterViewInit(): void { + if (!this.ngModel) { + console.warn('FieldValidationDirective requires ngModel'); + return; + } + + // Create unique field identifier + this.fieldIdentifier = `${this.fieldLabel}-${this.fieldKey}`; + + // Initial validation state + this.updateValidation(); + + // Subscribe to value changes + if (this.ngModel.valueChanges) { + this.ngModel.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.updateValidation(); + }); + } + + // Subscribe to status changes + if (this.ngModel.statusChanges) { + this.ngModel.statusChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.updateValidation(); + }); + } + } + + private updateValidation(): void { + if (!this.ngModel || !this.fieldIdentifier) { + return; + } + + const value = this.ngModel.value; + const errors = this.ngModel.errors; + + // Determine if field is valid + // A field is valid if: + // 1. It has no errors + // 2. OR it's not required and is empty/null/undefined + const isValid = + !errors || + (!this.fieldRequired && (value === null || value === undefined || value === '')); + + this.formValidationService.updateFieldValidity( + this.fieldIdentifier, + isValid, + errors + ); + } + + ngOnDestroy(): void { + // Clean up subscription + this.destroy$.next(); + this.destroy$.complete(); + + // Remove field from validation service + if (this.fieldIdentifier) { + this.formValidationService.removeField(this.fieldIdentifier); + } + } +} diff --git a/frontend/src/app/services/form-validation.service.ts b/frontend/src/app/services/form-validation.service.ts new file mode 100644 index 000000000..221bb6f55 --- /dev/null +++ b/frontend/src/app/services/form-validation.service.ts @@ -0,0 +1,101 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; + +interface FieldValidationState { + isValid: boolean; + errors: any; + fieldIdentifier: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class FormValidationService { + private fieldStates = new Map(); + private formValidSubject = new BehaviorSubject(true); + + public formValid$: Observable = this.formValidSubject.asObservable(); + + constructor() { } + + /** + * Update the validity state of a field + */ + updateFieldValidity(fieldIdentifier: string, isValid: boolean, errors: any = null): void { + this.fieldStates.set(fieldIdentifier, { + fieldIdentifier, + isValid, + errors + }); + this.updateFormValidity(); + } + + /** + * Check if the entire form is valid + */ + isFormValid(): boolean { + if (this.fieldStates.size === 0) { + return true; + } + + for (const [_, state] of this.fieldStates) { + if (!state.isValid) { + return false; + } + } + + return true; + } + + /** + * Get list of invalid field identifiers + */ + getInvalidFields(): string[] { + const invalidFields: string[] = []; + + for (const [fieldIdentifier, state] of this.fieldStates) { + if (!state.isValid) { + invalidFields.push(fieldIdentifier); + } + } + + return invalidFields; + } + + /** + * Get validation errors for a specific field + */ + getFieldErrors(fieldIdentifier: string): any { + return this.fieldStates.get(fieldIdentifier)?.errors || null; + } + + /** + * Clear all validation states + */ + clearAll(): void { + this.fieldStates.clear(); + this.updateFormValidity(); + } + + /** + * Remove a specific field from validation tracking + */ + removeField(fieldIdentifier: string): void { + this.fieldStates.delete(fieldIdentifier); + this.updateFormValidity(); + } + + /** + * Update the form validity observable + */ + private updateFormValidity(): void { + this.formValidSubject.next(this.isFormValid()); + } + + /** + * Get all field states (useful for debugging) + */ + getAllFieldStates(): Map { + return new Map(this.fieldStates); + } +}