Skip to content

Commit 53d5873

Browse files
guguclaude
andauthored
Fix/select zero and date today (#1757)
* fix: render 0 in select widget and "today" in date widget - Select widget: gate em-dash on `value == null` instead of `!value` so `0` and `''` show as real values. - Date widget: render "today" for same-calendar-day dates within formatDistanceWithinHours, instead of "X hours ago". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: use formatDistanceStrict from start of day in date widget - Compute relative time as `formatDistanceStrict(value, startOfToday())` so output is anchored to the calendar boundary and uses exact units (e.g., "12 hours ago", "1 day ago") instead of `formatDistanceToNow`'s rounded "about X" output. - Add tests asserting the matTooltip's `fullDate` is populated, including when the cell text is "today". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5848980 commit 53d5873

4 files changed

Lines changed: 169 additions & 94 deletions

File tree

frontend/src/app/components/ui-components/table-display-fields/date/date.component.spec.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ComponentFixture, TestBed } from '@angular/core/testing';
2-
import { DateDisplayComponent } from './date.component';
32
import { format } from 'date-fns';
3+
import { DateDisplayComponent } from './date.component';
44

55
describe('DateDisplayComponent', () => {
66
let component: DateDisplayComponent;
@@ -40,16 +40,51 @@ describe('DateDisplayComponent', () => {
4040
expect(component.formattedDate).toBeUndefined();
4141
});
4242

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

52+
expect(component.formattedDate).toBe('today');
53+
});
54+
55+
it('should show relative date for non-today date within formatDistanceWithinHours', () => {
56+
const yesterdayNoon = new Date();
57+
yesterdayNoon.setDate(yesterdayNoon.getDate() - 1);
58+
yesterdayNoon.setHours(12, 0, 0, 0);
59+
fixture.componentRef.setInput('value', yesterdayNoon.toISOString());
60+
fixture.componentRef.setInput('widgetStructure', {
61+
widget_params: { formatDistanceWithinHours: 48 },
62+
});
63+
component.ngOnInit();
64+
fixture.detectChanges();
65+
5366
expect(component.formattedDate).toContain('ago');
5467
});
68+
69+
it('should expose exact date as tooltip via fullDate', () => {
70+
const dateStr = '2023-04-29';
71+
fixture.componentRef.setInput('value', dateStr);
72+
component.ngOnInit();
73+
fixture.detectChanges();
74+
75+
expect(component.fullDate).toBe(format(new Date(dateStr), 'PPP'));
76+
});
77+
78+
it('should set fullDate tooltip even when displaying "today"', () => {
79+
const recentDate = new Date(Date.now() - 1000 * 60);
80+
fixture.componentRef.setInput('value', recentDate.toISOString());
81+
fixture.componentRef.setInput('widgetStructure', {
82+
widget_params: { formatDistanceWithinHours: 48 },
83+
});
84+
component.ngOnInit();
85+
fixture.detectChanges();
86+
87+
expect(component.formattedDate).toBe('today');
88+
expect(component.fullDate).toBe(format(recentDate, 'PPP'));
89+
});
5590
});
Lines changed: 59 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,73 @@
1-
import { Component, OnInit } from '@angular/core';
2-
import { format, formatDistanceToNow, differenceInHours } from 'date-fns';
3-
4-
import { BaseTableDisplayFieldComponent } from '../base-table-display-field/base-table-display-field.component';
51
import { ClipboardModule } from '@angular/cdk/clipboard';
2+
import { Component, OnInit } from '@angular/core';
63
import { MatButtonModule } from '@angular/material/button';
74
import { MatIconModule } from '@angular/material/icon';
85
import { MatTooltipModule } from '@angular/material/tooltip';
6+
import { differenceInHours, format, formatDistanceStrict, isToday, startOfToday } from 'date-fns';
7+
import { BaseTableDisplayFieldComponent } from '../base-table-display-field/base-table-display-field.component';
98

109
@Component({
11-
selector: 'app-date-display',
12-
templateUrl: './date.component.html',
13-
styleUrls: ['../base-table-display-field/base-table-display-field.component.css', './date.component.css'],
14-
imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule]
10+
selector: 'app-date-display',
11+
templateUrl: './date.component.html',
12+
styleUrls: ['../base-table-display-field/base-table-display-field.component.css', './date.component.css'],
13+
imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule],
1514
})
1615
export class DateDisplayComponent extends BaseTableDisplayFieldComponent implements OnInit {
17-
static type = 'date';
16+
static type = 'date';
17+
18+
public formattedDate: string;
19+
public formatDistanceWithinHours: number = 48;
20+
public fullDate: string;
21+
22+
ngOnInit(): void {
23+
this.parseWidgetParams();
1824

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

23-
ngOnInit(): void {
24-
this.parseWidgetParams();
25-
26-
if (this.value()) {
27-
try {
28-
const date = new Date(this.value());
29-
if (!Number.isNaN(date.getTime())) {
30-
// Always store the full date format for tooltip
31-
this.fullDate = format(date, "PPP"); // e.g., "April 29th, 2023"
32+
// Check if formatDistanceWithinHours is enabled and date is within specified hours from now
33+
if (this.formatDistanceWithinHours > 0 && this.isWithinHours(date, this.formatDistanceWithinHours)) {
34+
this.formattedDate = isToday(date)
35+
? 'today'
36+
: formatDistanceStrict(date, startOfToday(), { addSuffix: true });
37+
} else {
38+
this.formattedDate = format(date, 'P');
39+
}
40+
} else {
41+
this.formattedDate = this.value();
42+
this.fullDate = this.value();
43+
}
44+
} catch (_error) {
45+
this.formattedDate = this.value();
46+
this.fullDate = this.value();
47+
}
48+
}
49+
}
3250

33-
// Check if formatDistanceWithinHours is enabled and date is within specified hours from now
34-
if (this.formatDistanceWithinHours > 0 && this.isWithinHours(date, this.formatDistanceWithinHours)) {
35-
this.formattedDate = formatDistanceToNow(date, { addSuffix: true });
36-
} else {
37-
this.formattedDate = format(date, "P");
38-
}
39-
} else {
40-
this.formattedDate = this.value();
41-
this.fullDate = this.value();
42-
}
43-
} catch (_error) {
44-
this.formattedDate = this.value();
45-
this.fullDate = this.value();
46-
}
47-
}
48-
}
51+
private parseWidgetParams(): void {
52+
if (this.widgetStructure()?.widget_params) {
53+
try {
54+
const params =
55+
typeof this.widgetStructure().widget_params === 'string'
56+
? JSON.parse(this.widgetStructure().widget_params as unknown as string)
57+
: this.widgetStructure().widget_params;
4958

50-
private parseWidgetParams(): void {
51-
if (this.widgetStructure()?.widget_params) {
52-
try {
53-
const params = typeof this.widgetStructure().widget_params === 'string'
54-
? JSON.parse(this.widgetStructure().widget_params as unknown as string)
55-
: this.widgetStructure().widget_params;
56-
57-
if (params.formatDistanceWithinHours !== undefined) {
58-
this.formatDistanceWithinHours = Number(params.formatDistanceWithinHours) || 48;
59-
}
60-
} catch (e) {
61-
console.error('Error parsing date widget params:', e);
62-
}
63-
}
64-
}
59+
if (params.formatDistanceWithinHours !== undefined) {
60+
this.formatDistanceWithinHours = Number(params.formatDistanceWithinHours) || 48;
61+
}
62+
} catch (e) {
63+
console.error('Error parsing date widget params:', e);
64+
}
65+
}
66+
}
6567

66-
private isWithinHours(date: Date, hours: number): boolean {
67-
const now = new Date();
68-
const hoursDifference = Math.abs(differenceInHours(date, now));
69-
return hoursDifference <= hours;
70-
}
68+
private isWithinHours(date: Date, hours: number): boolean {
69+
const now = new Date();
70+
const hoursDifference = Math.abs(differenceInHours(date, now));
71+
return hoursDifference <= hours;
72+
}
7173
}

frontend/src/app/components/ui-components/table-display-fields/select/select.component.spec.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,54 @@ describe('SelectDisplayComponent', () => {
3838

3939
it('should display raw value when no options match', () => {
4040
fixture.componentRef.setInput('value', 'unknown');
41+
fixture.componentRef.setInput('widgetStructure', {
42+
widget_params: {
43+
options: [{ value: 'opt1', label: 'Option One' }],
44+
},
45+
});
46+
component.ngOnInit();
47+
fixture.detectChanges();
48+
expect(component.displayValue).toBe('unknown');
49+
});
50+
51+
it('should display option label when value is 0', () => {
52+
fixture.componentRef.setInput('value', 0);
4153
fixture.componentRef.setInput('widgetStructure', {
4254
widget_params: {
4355
options: [
44-
{ value: 'opt1', label: 'Option One' },
56+
{ value: 0, label: 'Zero' },
57+
{ value: 1, label: 'One' },
4558
],
4659
},
4760
});
4861
component.ngOnInit();
4962
fixture.detectChanges();
50-
expect(component.displayValue).toBe('unknown');
63+
expect(component.displayValue).toBe('Zero');
64+
});
65+
66+
it('should display em dash when value is null', () => {
67+
fixture.componentRef.setInput('value', null);
68+
component.ngOnInit();
69+
fixture.detectChanges();
70+
expect(component.displayValue).toBe('—');
71+
});
72+
73+
it('should display em dash when value is undefined', () => {
74+
fixture.componentRef.setInput('value', undefined);
75+
component.ngOnInit();
76+
fixture.detectChanges();
77+
expect(component.displayValue).toBe('—');
78+
});
79+
80+
it('should display empty string value as raw, not dash', () => {
81+
fixture.componentRef.setInput('value', '');
82+
fixture.componentRef.setInput('widgetStructure', {
83+
widget_params: {
84+
options: [{ value: 'opt1', label: 'Option One' }],
85+
},
86+
});
87+
component.ngOnInit();
88+
fixture.detectChanges();
89+
expect(component.displayValue).toBe('');
5190
});
5291
});
Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,42 @@
1-
import { Component, OnInit } from '@angular/core';
2-
3-
import { BaseTableDisplayFieldComponent } from '../base-table-display-field/base-table-display-field.component';
41
import { ClipboardModule } from '@angular/cdk/clipboard';
2+
import { Component, OnInit } from '@angular/core';
53
import { MatButtonModule } from '@angular/material/button';
64
import { MatIconModule } from '@angular/material/icon';
75
import { MatTooltipModule } from '@angular/material/tooltip';
6+
import { BaseTableDisplayFieldComponent } from '../base-table-display-field/base-table-display-field.component';
87

98
@Component({
10-
selector: 'app-select-display',
11-
templateUrl: './select.component.html',
12-
styleUrls: ['../base-table-display-field/base-table-display-field.component.css', './select.component.css'],
13-
imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule]
9+
selector: 'app-select-display',
10+
templateUrl: './select.component.html',
11+
styleUrls: ['../base-table-display-field/base-table-display-field.component.css', './select.component.css'],
12+
imports: [ClipboardModule, MatIconModule, MatButtonModule, MatTooltipModule],
1413
})
1514
export class SelectDisplayComponent extends BaseTableDisplayFieldComponent implements OnInit {
16-
public displayValue: string;
17-
public backgroundColor: string;
15+
public displayValue: string;
16+
public backgroundColor: string;
1817

19-
ngOnInit(): void {
20-
this.setDisplayValue();
21-
}
18+
ngOnInit(): void {
19+
this.setDisplayValue();
20+
}
2221

23-
private setDisplayValue(): void {
24-
if (!this.value()) {
25-
this.displayValue = '—';
26-
return;
27-
}
22+
private setDisplayValue(): void {
23+
if (this.value() == null) {
24+
this.displayValue = '—';
25+
return;
26+
}
2827

29-
if (this.widgetStructure()?.widget_params?.options) {
30-
// Find the matching option based on value and use its label
31-
const option = this.widgetStructure().widget_params.options.find(
32-
(opt: { value: any, label: string }) => opt.value === this.value()
33-
);
34-
this.displayValue = option ? option.label : this.value();
35-
this.backgroundColor = option?.background_color ? option.background_color : 'transparent';
36-
} else if (this.structure()?.data_type_params) {
37-
// If no widget structure but we have data_type_params, just use the value
38-
this.displayValue = this.value();
39-
} else {
40-
this.displayValue = this.value();
41-
}
42-
}
28+
if (this.widgetStructure()?.widget_params?.options) {
29+
// Find the matching option based on value and use its label
30+
const option = this.widgetStructure().widget_params.options.find(
31+
(opt: { value: any; label: string }) => opt.value === this.value(),
32+
);
33+
this.displayValue = option ? option.label : this.value();
34+
this.backgroundColor = option?.background_color ? option.background_color : 'transparent';
35+
} else if (this.structure()?.data_type_params) {
36+
// If no widget structure but we have data_type_params, just use the value
37+
this.displayValue = this.value();
38+
} else {
39+
this.displayValue = this.value();
40+
}
41+
}
4342
}

0 commit comments

Comments
 (0)