Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -182,28 +182,28 @@ <h3>
*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
</button>

<button *ngIf="hasKeyAttributesFromURL && permissions?.edit && !pageAction && pageMode !== 'view'"
type="submit" mat-flat-button color="primary"
data-testid="record-edit-button"
[disabled]="submitting || editRowForm.form.invalid">
[disabled]="submitting || !isFormValid">
Save
</button>

<button *ngIf="hasKeyAttributesFromURL && permissions?.add && pageAction === 'dub'"
type="submit" mat-flat-button color="primary"
data-testid="record-duplicate-button"
[disabled]="submitting || editRowForm.form.invalid">
[disabled]="submitting || !isFormValid">
Duplicate
</button>

<button *ngIf="!hasKeyAttributesFromURL" type="submit" mat-flat-button color="primary"
data-testid="record-add-button"
[disabled]="submitting || editRowForm.form.invalid">
[disabled]="submitting || !isFormValid">
Add
</button>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -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());
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<mat-form-field class="number-form-field" appearance="outline">
<mat-label>{{normalizedLabel}}</mat-label>
<input type="number" matInput name="{{label}}-{{key}}"
#numberField="ngModel"
[required]="required" [disabled]="disabled" [readonly]="readonly"
attr.data-testid="record-{{label}}-number"
[(ngModel)]="value" (ngModelChange)="onFieldChange.emit($event)">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,49 @@
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',
templateUrl: './number.component.html',
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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)">
<div matSuffix *ngIf="maxLength && maxLength > 0 && value && (maxLength - value.length) < 100" class="counter">{{value.length}} / {{maxLength}}</div>
<mat-error *ngIf="textField.errors?.['required']">This field is required.</mat-error>
<mat-error *ngIf="textField.errors?.['maxlength']">Maximum length is {{maxLength}} characters.</mat-error>
<mat-error *ngIf="textField.errors?.['invalidPattern'] || textField.errors?.[('invalid' + validateType)]">{{getValidationErrorMessage()}}</mat-error>
</mat-form-field>
</mat-form-field>
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ 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()

@Component({
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;
Expand Down
87 changes: 87 additions & 0 deletions frontend/src/app/directives/field-validation.directive.ts
Original file line number Diff line number Diff line change
@@ -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<void>();
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);
}
}
}
101 changes: 101 additions & 0 deletions frontend/src/app/services/form-validation.service.ts
Original file line number Diff line number Diff line change
@@ -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<string, FieldValidationState>();
private formValidSubject = new BehaviorSubject<boolean>(true);

public formValid$: Observable<boolean> = 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<string, FieldValidationState> {
return new Map(this.fieldStates);
}
}
Loading