From fc516b4aa47624fc7216123aa86a5b49fb12a410 Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Wed, 22 Apr 2026 09:23:18 +0000 Subject: [PATCH 1/2] db table view: render null foreign-key values as a dash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guards the helpers that extract FK cell values against null/undefined and updates the two FK display components (table cell, row preview) to render a dash only when the value is genuinely absent. Using `== null` keeps `0`, empty string, and `false` as real FK values so rows whose FK id is 0 still render the link, not a dash. Previously `Object.keys(null)` threw on any row with a null FK column and `{{ value() || '—' }}` substituted a dash for legitimately-zero ids. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../db-table-row-view.component.spec.ts | 72 +++++++++++++++++++ .../db-table-row-view.component.ts | 34 ++++----- .../db-table-view.component.spec.ts | 27 +++++++ .../db-table-view/db-table-view.component.ts | 2 + .../foreign-key/foreign-key.component.html | 20 +++--- .../foreign-key/foreign-key.component.spec.ts | 45 ++++++++++++ .../foreign-key/foreign-key.component.html | 6 +- .../foreign-key/foreign-key.component.spec.ts | 52 +++++++++++--- .../foreign-key/foreign-key.component.ts | 37 +++++----- 9 files changed, 236 insertions(+), 59 deletions(-) diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.spec.ts index e7274fbf1..70f8737fc 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.spec.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.spec.ts @@ -22,4 +22,76 @@ describe('DbTableRowViewComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + describe('getForeignKeyValue', () => { + const baseRow = { + foreignKeys: { + user_id: { + column_name: 'user_id', + constraint_name: 'fk_user', + referenced_column_name: 'id', + referenced_table_name: 'users', + }, + }, + }; + + it('returns null when record field is null', () => { + component.selectedRow = { ...baseRow, record: { user_id: null } } as any; + expect(component.getForeignKeyValue('user_id')).toBeNull(); + }); + + it('returns null when record field is undefined', () => { + component.selectedRow = { ...baseRow, record: {} } as any; + expect(component.getForeignKeyValue('user_id')).toBeNull(); + }); + + it('returns identity column value when FK object has one', () => { + component.selectedRow = { + ...baseRow, + record: { user_id: { id: 42, name: 'alice' } }, + } as any; + expect(component.getForeignKeyValue('user_id')).toBe('alice'); + }); + + it('returns primitive FK value as-is (including 0)', () => { + component.selectedRow = { ...baseRow, record: { user_id: 0 } } as any; + expect(component.getForeignKeyValue('user_id')).toBe(0); + }); + + it('returns primitive FK value as-is (including empty string)', () => { + component.selectedRow = { ...baseRow, record: { user_id: '' } } as any; + expect(component.getForeignKeyValue('user_id')).toBe(''); + }); + }); + + describe('getForeignKeyQueryParams', () => { + const baseRow = { + foreignKeys: { + user_id: { + column_name: 'user_id', + constraint_name: 'fk_user', + referenced_column_name: 'id', + referenced_table_name: 'users', + }, + }, + }; + + it('returns {} when record field is null', () => { + component.selectedRow = { ...baseRow, record: { user_id: null } } as any; + expect(component.getForeignKeyQueryParams('user_id')).toEqual({}); + }); + + it('returns referenced column param when FK is an object', () => { + component.selectedRow = { + ...baseRow, + record: { user_id: { id: 42, name: 'alice' } }, + } as any; + expect(component.getForeignKeyQueryParams('user_id')).toEqual({ id: 42 }); + }); + + it('returns referenced column param when FK is a primitive', () => { + component.selectedRow = { ...baseRow, record: { user_id: 7 } } as any; + expect(component.getForeignKeyQueryParams('user_id')).toEqual({ id: 7 }); + }); + }); }); diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.ts index d89a69f08..50a96305c 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-row-view/db-table-row-view.component.ts @@ -257,33 +257,23 @@ export class DbTableRowViewComponent implements OnInit, OnDestroy { } getForeignKeyValue(field: string) { - if (this.selectedRow && typeof this.selectedRow.record[field] === 'object') { - const identityColumnName = Object.keys(this.selectedRow.record[field]).find( - (key) => key !== this.selectedRow.foreignKeys[field].referenced_column_name, - ); + const cell = this.selectedRow?.record?.[field]; + if (cell == null) return null; + if (typeof cell === 'object') { const referencedColumnName = this.selectedRow.foreignKeys[field].referenced_column_name; - if (identityColumnName) { - return this.selectedRow.record[field][identityColumnName]; - } - if (referencedColumnName) { - return this.selectedRow.record[field][referencedColumnName]; - } - return this.selectedRow.record[field] || ''; + const identityColumnName = Object.keys(cell).find((key) => key !== referencedColumnName); + if (identityColumnName) return cell[identityColumnName]; + if (referencedColumnName && cell[referencedColumnName] != null) return cell[referencedColumnName]; + return null; } - return this.selectedRow.record[field] || ''; + return cell; } getForeignKeyQueryParams(field: string) { - if (this.selectedRow) { - const referencedColumnName = this.selectedRow.foreignKeys[field]?.referenced_column_name; - - if (typeof this.selectedRow.record[field] === 'object') { - return { [referencedColumnName]: this.selectedRow.record[field][referencedColumnName] }; - } else { - return { [referencedColumnName]: this.selectedRow.record[field] }; - } - } - return {}; + const cell = this.selectedRow?.record?.[field]; + const referencedColumnName = this.selectedRow?.foreignKeys?.[field]?.referenced_column_name; + if (cell == null || !referencedColumnName) return {}; + return { [referencedColumnName]: typeof cell === 'object' ? cell[referencedColumnName] : cell }; } isWidget(columnName: string) { diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.spec.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.spec.ts index aadb22473..11fba7e69 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.spec.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.spec.ts @@ -241,4 +241,31 @@ describe('DbTableViewComponent', () => { const value = component.getCellValue(foreignKey, cell); expect(value).toEqual('John'); }); + + it('should return null (not throw) when foreign key cell is null', () => { + const foreignKey = { + autocomplete_columns: ['FirstName'], + column_name: 'CustomerId', + column_default: null, + constraint_name: 'Orders_ibfk_2', + referenced_column_name: 'Id', + referenced_table_name: 'Customers', + }; + + expect(component.getCellValue(foreignKey, null)).toBeNull(); + expect(component.getCellValue(foreignKey, undefined)).toBeNull(); + }); + + it('should not throw in isForeignKeySelected when record is null', () => { + const foreignKey = { + autocomplete_columns: ['FirstName'], + column_name: 'CustomerId', + column_default: null, + constraint_name: 'Orders_ibfk_2', + referenced_column_name: 'Id', + referenced_table_name: 'Customers', + }; + + expect(component.isForeignKeySelected(null, foreignKey)).toBe(false); + }); }); diff --git a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts index d16e7399c..bad975c84 100644 --- a/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts +++ b/frontend/src/app/components/dashboard/db-table-view/db-table-view.component.ts @@ -417,6 +417,7 @@ export class DbTableViewComponent implements OnInit, OnChanges { } getCellValue(foreignKey: TableForeignKey, cell) { + if (cell == null) return null; const identityColumnName = Object.keys(cell).find((key) => key !== foreignKey.referenced_column_name); if (identityColumnName) { return cell[identityColumnName]; @@ -680,6 +681,7 @@ export class DbTableViewComponent implements OnInit, OnChanges { } isForeignKeySelected(record, foreignKey: TableForeignKey) { + if (record == null) return false; const primaryKeyValue = record[foreignKey.referenced_column_name]; if (this.selectedRowType === 'foreignKey' && this.selectedRow && this.selectedRow.record !== null) { diff --git a/frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.html b/frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.html index a5b804a17..d9083c910 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.html +++ b/frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.html @@ -1,8 +1,12 @@ - - {{displayValue()}} - visibility - +@if (displayValue() == null) { + +} @else { + + {{displayValue()}} + visibility + +} diff --git a/frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.spec.ts b/frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.spec.ts index ede913808..518b9ade1 100644 --- a/frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.spec.ts +++ b/frontend/src/app/components/ui-components/record-view-fields/foreign-key/foreign-key.component.spec.ts @@ -27,4 +27,49 @@ describe('ForeignKeyRecordViewComponent', () => { component.ngOnInit(); expect(component.foreignKeyURLParams).toEqual({ id: 1, mode: 'view' }); }); + + it('should render a dash when displayValue is null', () => { + fixture.componentRef.setInput('link', '/foo'); + fixture.componentRef.setInput('primaryKeysParams', { id: 1 }); + fixture.componentRef.setInput('displayValue', null); + fixture.detectChanges(); + + const anchor = fixture.nativeElement.querySelector('a.foreign-key-link'); + const span = fixture.nativeElement.querySelector('span.field-view-value'); + expect(anchor).toBeFalsy(); + expect(span?.textContent?.trim()).toBe('—'); + }); + + it('should render a dash when displayValue is undefined', () => { + fixture.componentRef.setInput('link', '/foo'); + fixture.componentRef.setInput('primaryKeysParams', { id: 1 }); + fixture.componentRef.setInput('displayValue', undefined); + fixture.detectChanges(); + + const anchor = fixture.nativeElement.querySelector('a.foreign-key-link'); + const span = fixture.nativeElement.querySelector('span.field-view-value'); + expect(anchor).toBeFalsy(); + expect(span?.textContent?.trim()).toBe('—'); + }); + + it('should render the link (not a dash) when displayValue is 0', () => { + fixture.componentRef.setInput('link', '/foo'); + fixture.componentRef.setInput('primaryKeysParams', { id: 1 }); + fixture.componentRef.setInput('displayValue', 0 as unknown as string); + fixture.detectChanges(); + + const anchor = fixture.nativeElement.querySelector('a.foreign-key-link'); + expect(anchor).toBeTruthy(); + expect(anchor.querySelector('span').textContent.trim()).toBe('0'); + }); + + it('should render the link (not a dash) when displayValue is empty string', () => { + fixture.componentRef.setInput('link', '/foo'); + fixture.componentRef.setInput('primaryKeysParams', { id: 1 }); + fixture.componentRef.setInput('displayValue', ''); + fixture.detectChanges(); + + const anchor = fixture.nativeElement.querySelector('a.foreign-key-link'); + expect(anchor).toBeTruthy(); + }); }); diff --git a/frontend/src/app/components/ui-components/table-display-fields/foreign-key/foreign-key.component.html b/frontend/src/app/components/ui-components/table-display-fields/foreign-key/foreign-key.component.html index f44d3699a..66853cc22 100644 --- a/frontend/src/app/components/ui-components/table-display-fields/foreign-key/foreign-key.component.html +++ b/frontend/src/app/components/ui-components/table-display-fields/foreign-key/foreign-key.component.html @@ -1,6 +1,8 @@
- @if (relations() && value()) { + @if (value() == null) { + + } @else if (relations()) {