Skip to content
Merged
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
@@ -1,6 +1,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DateDisplayComponent } from './date.component';
import { format } from 'date-fns';
import { DateDisplayComponent } from './date.component';

describe('DateDisplayComponent', () => {
let component: DateDisplayComponent;
Expand Down Expand Up @@ -40,16 +40,51 @@ describe('DateDisplayComponent', () => {
expect(component.formattedDate).toBeUndefined();
});

it('should show relative date when formatDistanceWithinHours configured', () => {
const now = new Date();
const recentDate = new Date(now.getTime() - 1000 * 60 * 60); // 1 hour ago
it('should display "today" instead of relative time for same-day date', () => {
const recentDate = new Date(Date.now() - 1000 * 60); // 1 minute ago — same calendar day
fixture.componentRef.setInput('value', recentDate.toISOString());
Comment on lines +43 to 45
fixture.componentRef.setInput('widgetStructure', {
widget_params: { formatDistanceWithinHours: 48 },
});
component.ngOnInit();
fixture.detectChanges();

expect(component.formattedDate).toBe('today');
});
Comment on lines +43 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Two tests are flaky when the suite runs in the first minute after midnight

Both tests (lines 44 and 79) construct recentDate as Date.now() - 60_000 (one minute ago). If the test runner starts between 00:00:00 and 00:00:59, that timestamp falls on the previous calendar day, isToday(recentDate) returns false, and the assertion expect(component.formattedDate).toBe('today') fails.

🛠️ Proposed fix — anchor to a point safely within today
-		const recentDate = new Date(Date.now() - 1000 * 60); // 1 minute ago — same calendar day
+		// Use a time several hours into today to avoid midnight boundary flakiness
+		const recentDate = new Date();
+		recentDate.setHours(Math.max(2, recentDate.getHours()), 0, 0, 0); // at least 2 AM today

Apply the same change at line 79. Alternatively, inject a fake clock (e.g., jasmine.clock()) and pin the current time to a fixed midday value for the full test case.

Also applies to: 78-89

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@frontend/src/app/components/ui-components/table-display-fields/date/date.component.spec.ts`
around lines 43 - 53, The test is flaky because recentDate = Date.now() - 60_000
can roll back to the previous calendar day near midnight; update the test that
constructs recentDate (used with fixture.componentRef.setInput('value', ...) and
checked after component.ngOnInit() as component.formattedDate) to anchor the
"now" point safely within today (for example compute a baseDate = new Date();
baseDate.setHours(12,0,0,0) and then subtract one minute from that to create
recentDate) or alternatively use a fake clock (e.g., jasmine.clock()) to pin the
current time to a fixed midday value for the whole test so isToday(recentDate)
reliably returns true.


it('should show relative date for non-today date within formatDistanceWithinHours', () => {
const yesterdayNoon = new Date();
yesterdayNoon.setDate(yesterdayNoon.getDate() - 1);
yesterdayNoon.setHours(12, 0, 0, 0);
fixture.componentRef.setInput('value', yesterdayNoon.toISOString());
fixture.componentRef.setInput('widgetStructure', {
widget_params: { formatDistanceWithinHours: 48 },
});
component.ngOnInit();
fixture.detectChanges();

expect(component.formattedDate).toContain('ago');
});

it('should expose exact date as tooltip via fullDate', () => {
const dateStr = '2023-04-29';
fixture.componentRef.setInput('value', dateStr);
component.ngOnInit();
fixture.detectChanges();

expect(component.fullDate).toBe(format(new Date(dateStr), 'PPP'));
});

it('should set fullDate tooltip even when displaying "today"', () => {
const recentDate = new Date(Date.now() - 1000 * 60);
fixture.componentRef.setInput('value', recentDate.toISOString());
fixture.componentRef.setInput('widgetStructure', {
widget_params: { formatDistanceWithinHours: 48 },
});
Comment on lines +78 to +83
component.ngOnInit();
fixture.detectChanges();

expect(component.formattedDate).toBe('today');
expect(component.fullDate).toBe(format(recentDate, 'PPP'));
});
});
Original file line number Diff line number Diff line change
@@ -1,71 +1,73 @@
import { Component, OnInit } from '@angular/core';
import { format, formatDistanceToNow, differenceInHours } from 'date-fns';

import { BaseTableDisplayFieldComponent } from '../base-table-display-field/base-table-display-field.component';
import { ClipboardModule } from '@angular/cdk/clipboard';
import { Component, OnInit } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
import { differenceInHours, format, formatDistanceStrict, isToday, startOfToday } from 'date-fns';
import { BaseTableDisplayFieldComponent } from '../base-table-display-field/base-table-display-field.component';

@Component({
selector: 'app-date-display',
templateUrl: './date.component.html',
styleUrls: ['../base-table-display-field/base-table-display-field.component.css', './date.component.css'],
imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule]
selector: 'app-date-display',
templateUrl: './date.component.html',
styleUrls: ['../base-table-display-field/base-table-display-field.component.css', './date.component.css'],
imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule],
})
export class DateDisplayComponent extends BaseTableDisplayFieldComponent implements OnInit {
static type = 'date';
static type = 'date';

public formattedDate: string;
public formatDistanceWithinHours: number = 48;
public fullDate: string;

ngOnInit(): void {
this.parseWidgetParams();

public formattedDate: string;
public formatDistanceWithinHours: number = 48;
public fullDate: string;
if (this.value()) {
try {
const date = new Date(this.value());
if (!Number.isNaN(date.getTime())) {
// Always store the full date format for tooltip
this.fullDate = format(date, 'PPP'); // e.g., "April 29th, 2023"

ngOnInit(): void {
this.parseWidgetParams();

if (this.value()) {
try {
const date = new Date(this.value());
if (!Number.isNaN(date.getTime())) {
// Always store the full date format for tooltip
this.fullDate = format(date, "PPP"); // e.g., "April 29th, 2023"
// Check if formatDistanceWithinHours is enabled and date is within specified hours from now
if (this.formatDistanceWithinHours > 0 && this.isWithinHours(date, this.formatDistanceWithinHours)) {
this.formattedDate = isToday(date)
? 'today'
: formatDistanceStrict(date, startOfToday(), { addSuffix: true });
Comment on lines +34 to +36

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

startOfToday() as the formatDistanceStrict base gives misleading distances for dates near midnight

formatDistanceStrict(date, startOfToday(), { addSuffix: true }) measures elapsed time from midnight (00:00), not from the current moment. A date of "yesterday 11:59 PM" is only 1 minute before midnight, so at 3 PM today it would render as "1 minute ago" instead of the expected ~15 hours. The parallel DateTimeDisplayComponent uses formatDistanceToNow (current time as reference), which is the correct anchor for a relative-time label.

🐛 Proposed fix — use the current time as reference
-					this.formattedDate = isToday(date)
-						? 'today'
-						: formatDistanceStrict(date, startOfToday(), { addSuffix: true });
+					this.formattedDate = isToday(date)
+						? 'today'
+						: formatDistanceStrict(date, new Date(), { addSuffix: true });

startOfToday can then be removed from the import if it has no other uses.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@frontend/src/app/components/ui-components/table-display-fields/date/date.component.ts`
around lines 34 - 36, The code uses formatDistanceStrict(date, startOfToday(),
...) which measures distance from midnight and gives misleading results for
recent times; change to use the current instant by calling
formatDistanceStrict(date, new Date(), { addSuffix: true }) or switch to
formatDistanceToNow(date, { addSuffix: true }) to mirror
DateTimeDisplayComponent behavior, and remove the now-unused startOfToday
import; keep the isToday(date) check and assign formattedDate accordingly.

} else {
Comment on lines +33 to +37
this.formattedDate = format(date, 'P');
}
} else {
this.formattedDate = this.value();
this.fullDate = this.value();
}
} catch (_error) {
this.formattedDate = this.value();
this.fullDate = this.value();
}
}
}

// Check if formatDistanceWithinHours is enabled and date is within specified hours from now
if (this.formatDistanceWithinHours > 0 && this.isWithinHours(date, this.formatDistanceWithinHours)) {
this.formattedDate = formatDistanceToNow(date, { addSuffix: true });
} else {
this.formattedDate = format(date, "P");
}
} else {
this.formattedDate = this.value();
this.fullDate = this.value();
}
} catch (_error) {
this.formattedDate = this.value();
this.fullDate = this.value();
}
}
}
private parseWidgetParams(): void {
if (this.widgetStructure()?.widget_params) {
try {
const params =
typeof this.widgetStructure().widget_params === 'string'
? JSON.parse(this.widgetStructure().widget_params as unknown as string)
: this.widgetStructure().widget_params;

private parseWidgetParams(): void {
if (this.widgetStructure()?.widget_params) {
try {
const params = typeof this.widgetStructure().widget_params === 'string'
? JSON.parse(this.widgetStructure().widget_params as unknown as string)
: this.widgetStructure().widget_params;

if (params.formatDistanceWithinHours !== undefined) {
this.formatDistanceWithinHours = Number(params.formatDistanceWithinHours) || 48;
}
} catch (e) {
console.error('Error parsing date widget params:', e);
}
}
}
if (params.formatDistanceWithinHours !== undefined) {
this.formatDistanceWithinHours = Number(params.formatDistanceWithinHours) || 48;
}
Comment on lines +59 to +61

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Number(...) || 48 silently ignores an explicit 0, preventing users from disabling relative formatting

Number(0) || 48 evaluates to 48 because 0 is falsy. A widget config of { formatDistanceWithinHours: 0 } is intended to disable relative formatting (the guard on line 33 is > 0), but the fallback overwrites it.

🐛 Proposed fix — use nullish coalescing
-				this.formatDistanceWithinHours = Number(params.formatDistanceWithinHours) || 48;
+				const parsed = Number(params.formatDistanceWithinHours);
+				this.formatDistanceWithinHours = !isNaN(parsed) ? parsed : 48;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (params.formatDistanceWithinHours !== undefined) {
this.formatDistanceWithinHours = Number(params.formatDistanceWithinHours) || 48;
}
if (params.formatDistanceWithinHours !== undefined) {
const parsed = Number(params.formatDistanceWithinHours);
this.formatDistanceWithinHours = !isNaN(parsed) ? parsed : 48;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@frontend/src/app/components/ui-components/table-display-fields/date/date.component.ts`
around lines 59 - 61, The current assignment uses
Number(params.formatDistanceWithinHours) || 48 which treats 0 as falsy and
forces the default 48; update the logic so an explicit 0 is preserved by using
nullish coalescing on params.formatDistanceWithinHours (e.g. compute a fallback
value with params.formatDistanceWithinHours ?? 48 and then Number(...) into
this.formatDistanceWithinHours) so that formatDistanceWithinHours = 0 remains
valid; change the assignment around the symbol this.formatDistanceWithinHours
and the params.formatDistanceWithinHours usage in date.component.ts accordingly.

} catch (e) {
console.error('Error parsing date widget params:', e);
}
}
}

private isWithinHours(date: Date, hours: number): boolean {
const now = new Date();
const hoursDifference = Math.abs(differenceInHours(date, now));
return hoursDifference <= hours;
}
private isWithinHours(date: Date, hours: number): boolean {
const now = new Date();
const hoursDifference = Math.abs(differenceInHours(date, now));
return hoursDifference <= hours;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,54 @@ describe('SelectDisplayComponent', () => {

it('should display raw value when no options match', () => {
fixture.componentRef.setInput('value', 'unknown');
fixture.componentRef.setInput('widgetStructure', {
widget_params: {
options: [{ value: 'opt1', label: 'Option One' }],
},
});
component.ngOnInit();
fixture.detectChanges();
expect(component.displayValue).toBe('unknown');
});

it('should display option label when value is 0', () => {
fixture.componentRef.setInput('value', 0);
fixture.componentRef.setInput('widgetStructure', {
widget_params: {
options: [
{ value: 'opt1', label: 'Option One' },
{ value: 0, label: 'Zero' },
{ value: 1, label: 'One' },
],
},
});
component.ngOnInit();
fixture.detectChanges();
expect(component.displayValue).toBe('unknown');
expect(component.displayValue).toBe('Zero');
});

it('should display em dash when value is null', () => {
fixture.componentRef.setInput('value', null);
component.ngOnInit();
fixture.detectChanges();
expect(component.displayValue).toBe('—');
});

it('should display em dash when value is undefined', () => {
fixture.componentRef.setInput('value', undefined);
component.ngOnInit();
fixture.detectChanges();
expect(component.displayValue).toBe('—');
});

it('should display empty string value as raw, not dash', () => {
fixture.componentRef.setInput('value', '');
fixture.componentRef.setInput('widgetStructure', {
widget_params: {
options: [{ value: 'opt1', label: 'Option One' }],
},
});
component.ngOnInit();
fixture.detectChanges();
expect(component.displayValue).toBe('');
});
});
Original file line number Diff line number Diff line change
@@ -1,43 +1,42 @@
import { Component, OnInit } from '@angular/core';

import { BaseTableDisplayFieldComponent } from '../base-table-display-field/base-table-display-field.component';
import { ClipboardModule } from '@angular/cdk/clipboard';
import { Component, OnInit } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
import { BaseTableDisplayFieldComponent } from '../base-table-display-field/base-table-display-field.component';

@Component({
selector: 'app-select-display',
templateUrl: './select.component.html',
styleUrls: ['../base-table-display-field/base-table-display-field.component.css', './select.component.css'],
imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule]
selector: 'app-select-display',
templateUrl: './select.component.html',
styleUrls: ['../base-table-display-field/base-table-display-field.component.css', './select.component.css'],
imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule],
})
export class SelectDisplayComponent extends BaseTableDisplayFieldComponent implements OnInit {
public displayValue: string;
public backgroundColor: string;
public displayValue: string;
public backgroundColor: string;

ngOnInit(): void {
this.setDisplayValue();
}
ngOnInit(): void {
this.setDisplayValue();
}

private setDisplayValue(): void {
if (!this.value()) {
this.displayValue = '—';
return;
}
private setDisplayValue(): void {
if (this.value() == null) {
this.displayValue = '—';
return;
}

if (this.widgetStructure()?.widget_params?.options) {
// Find the matching option based on value and use its label
const option = this.widgetStructure().widget_params.options.find(
(opt: { value: any, label: string }) => opt.value === this.value()
);
this.displayValue = option ? option.label : this.value();
this.backgroundColor = option?.background_color ? option.background_color : 'transparent';
} else if (this.structure()?.data_type_params) {
// If no widget structure but we have data_type_params, just use the value
this.displayValue = this.value();
} else {
this.displayValue = this.value();
}
}
if (this.widgetStructure()?.widget_params?.options) {
// Find the matching option based on value and use its label
const option = this.widgetStructure().widget_params.options.find(
(opt: { value: any; label: string }) => opt.value === this.value(),
);
this.displayValue = option ? option.label : this.value();
this.backgroundColor = option?.background_color ? option.background_color : 'transparent';
} else if (this.structure()?.data_type_params) {
// If no widget structure but we have data_type_params, just use the value
this.displayValue = this.value();
} else {
this.displayValue = this.value();
}
}
}
Loading