diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.ts index c662c8209..36eadbbe9 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-widgets/db-table-widgets.component.ts @@ -230,9 +230,12 @@ export class DbTableWidgetsComponent implements OnInit { // isCurrency, isBtcAddress, isISO8601, isISO31661Alpha2, isISO31661Alpha3, isISO4217, // isDataURI, isMagnetURI, isMimeType, isLatLong, isSlug, isStrongPassword, isTaxID, isVAT // OR use "regex" with a regex parameter for custom pattern matching +// force_send_empty_string: when true, always send "" to the backend on clear, +// even if the column is nullable. Default false (cleared input becomes NULL on nullable columns). { "validate": null, - "regex": null + "regex": null, + "force_send_empty_string": false }`, Textarea: `// provide number of strings to show. { 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 b46bc7023..89c0dc055 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 @@ -8,8 +8,8 @@ [validateType]="validateType" [regexPattern]="regexPattern" attr.data-testid="record-{{label()}}-text" - [(ngModel)]="value" (ngModelChange)="onFieldChange.emit($event)"> - @if (maxLength && maxLength > 0 && value() && (maxLength - value().length) < 100) { + [(ngModel)]="value" (ngModelChange)="handleValueChange($event)"> + @if (maxLength && maxLength > 0 && value() && 100 > (maxLength - value().length)) {
{{value().length}} / {{maxLength}}
} @if (textField.errors?.['required']) { diff --git a/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.spec.ts b/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.spec.ts index 5f3dd5d3d..89483fdc8 100644 --- a/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-edit-fields/text/text.component.spec.ts @@ -46,7 +46,9 @@ describe('TextEditComponent', () => { }); it('should parse regexPattern from widget params', () => { - fixture.componentRef.setInput('widgetStructure', { widget_params: { validate: 'regex', regex: '^[a-z]+$' } } as any); + fixture.componentRef.setInput('widgetStructure', { + widget_params: { validate: 'regex', regex: '^[a-z]+$' }, + } as any); component.ngOnInit(); expect(component.regexPattern).toBe('^[a-z]+$'); }); @@ -80,4 +82,49 @@ describe('TextEditComponent', () => { component.validateType = 'customValidator'; expect(component.getValidationErrorMessage()).toBe('Invalid customValidator'); }); + + it('should default forceSendEmptyString to false when widget_params omits the key', () => { + fixture.componentRef.setInput('widgetStructure', { widget_params: { validate: null, regex: null } } as any); + component.ngOnInit(); + expect(component.forceSendEmptyString).toBe(false); + }); + + it('should emit null on empty input when column is nullable and force_send_empty_string is not set', () => { + fixture.componentRef.setInput('structure', { allow_null: true } as any); + component.ngOnInit(); + const emitted: any[] = []; + component.onFieldChange.subscribe((v) => emitted.push(v)); + component.handleValueChange(''); + expect(emitted).toEqual([null]); + }); + + it('should emit empty string on empty input when column is not nullable', () => { + fixture.componentRef.setInput('structure', { allow_null: false } as any); + component.ngOnInit(); + const emitted: any[] = []; + component.onFieldChange.subscribe((v) => emitted.push(v)); + component.handleValueChange(''); + expect(emitted).toEqual(['']); + }); + + it('should emit empty string on empty input when force_send_empty_string is true even on nullable column', () => { + fixture.componentRef.setInput('structure', { allow_null: true } as any); + fixture.componentRef.setInput('widgetStructure', { + widget_params: { force_send_empty_string: true }, + } as any); + component.ngOnInit(); + const emitted: any[] = []; + component.onFieldChange.subscribe((v) => emitted.push(v)); + component.handleValueChange(''); + expect(emitted).toEqual(['']); + }); + + it('should emit non-empty value unchanged regardless of nullability', () => { + fixture.componentRef.setInput('structure', { allow_null: true } as any); + component.ngOnInit(); + const emitted: any[] = []; + component.onFieldChange.subscribe((v) => emitted.push(v)); + component.handleValueChange('hello'); + expect(emitted).toEqual(['hello']); + }); }); 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 052f867d6..6063f88d1 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 @@ -20,6 +20,7 @@ export class TextEditComponent extends BaseEditFieldComponent implements OnInit maxLength: number | null = null; validateType: string | null = null; regexPattern: string | null = null; + forceSendEmptyString: boolean = false; override ngOnInit(): void { super.ngOnInit(); @@ -35,9 +36,18 @@ export class TextEditComponent extends BaseEditFieldComponent implements OnInit this.validateType = params.validate || null; this.regexPattern = params.regex || null; + this.forceSendEmptyString = !!params.force_send_empty_string; } } + handleValueChange(v: string | null): void { + if (v === '' && !this.forceSendEmptyString && this.structure()?.allow_null === true) { + this.onFieldChange.emit(null); + return; + } + this.onFieldChange.emit(v); + } + getValidationErrorMessage(): string { if (!this.validateType) { return '';