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)">
0 && value && (maxLength - value.length) < 100" class="counter">{{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);
+ }
+}