Skip to content

Commit 486d162

Browse files
Date range preset addition and bugfix (#1116)
* added more to the preset list, updated start of the week to start from sunday * error handling for invalid inputs * updated change log * lint fix * this month date fix * updated utc * build fix * added max-width value * added max-width css
1 parent c6556d5 commit 486d162

File tree

6 files changed

+191
-25
lines changed

6 files changed

+191
-25
lines changed

projects/swimlane/ngx-ui/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## HEAD (unreleased)
44

5+
- Fix (`ngx-date-range-picker`): Error handling for invalid custom input and updated preset list.
6+
57
## 50.0.0-alpha.3 (2025-07-14)
68

79
- Feature: Added new CSS Variables for colors, spacing, and typography

projects/swimlane/ngx-ui/src/lib/components/date-range-calendar/date-range-picker.component.spec.ts

Lines changed: 96 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { IconModule } from '../icon/icon.module';
99
import { TooltipModule } from '../tooltip/tooltip.module';
1010
import { CommonModule } from '@angular/common';
1111
import { CalendarModule } from '../calendar/calendar.module';
12+
import { endOfMonth, startOfMonth } from 'date-fns';
1213

1314
describe('DateRangePickerComponent', () => {
1415
let component: DateRangePickerComponent;
@@ -32,12 +33,18 @@ describe('DateRangePickerComponent', () => {
3233

3334
fixture = TestBed.createComponent(DateRangePickerComponent);
3435
component = fixture.componentInstance;
35-
component.wrapperRef = {
36-
open: true
37-
} as any;
36+
component.wrapperRef = { open: true } as any;
3837
fixture.detectChanges();
3938
});
4039

40+
function expectValidRange(start: Date | null, end: Date | null) {
41+
expect(start).not.toBeNull();
42+
expect(end).not.toBeNull();
43+
if (start && end) {
44+
expect(start <= end).toBeTrue();
45+
}
46+
}
47+
4148
it('should create the component', () => {
4249
expect(component).toBeTruthy();
4350
});
@@ -76,6 +83,27 @@ describe('DateRangePickerComponent', () => {
7683
expect(component.validationError).toBe(`"From" can't be after "To"`);
7784
});
7885

86+
it('should set validation error when custom input contains invalid dates', () => {
87+
component.form.startRaw = 'invalid-start';
88+
component.form.endRaw = 'invalid-end';
89+
component.onCustomInputChange();
90+
expect(component.validationError).toBe(`Invalid date expression`);
91+
expect(component.form.startDate).toBeNull();
92+
expect(component.form.endDate).toBeNull();
93+
});
94+
95+
it('should handle invalid range selection where endDate < startDate', () => {
96+
const invalidStart = new Date('2025-07-20');
97+
const invalidEnd = new Date('2025-07-15'); // earlier than start
98+
99+
component.onRangeSelect({ startDate: invalidStart, endDate: invalidEnd });
100+
101+
expect(component.form.startDate).toEqual(invalidStart);
102+
expect(component.form.endDate).toBeNull(); // reset because invalid
103+
expect(component.rangeModel.startDate).toEqual(invalidStart);
104+
expect(component.rangeModel.endDate).toEqual(invalidStart);
105+
});
106+
79107
it('should update selected label based on preset match', () => {
80108
const preset = component.presets.find(p => p.label === 'Last 7 days');
81109
const [start, end] = preset.range();
@@ -86,8 +114,8 @@ describe('DateRangePickerComponent', () => {
86114
});
87115

88116
it('should set default label when no preset matched', () => {
89-
const start = new Date('2022-01-01T00:00:00');
90-
const end = new Date('2022-01-02T00:00:00');
117+
const start = new Date('2022-01-01');
118+
const end = new Date('2022-01-02');
91119
component.form.startDate = start;
92120
component.form.endDate = end;
93121
component.updateSelectedLabel();
@@ -118,25 +146,80 @@ describe('DateRangePickerComponent', () => {
118146
const first = new Date('2024-01-01');
119147
const second = new Date('2024-01-05');
120148
const third = new Date('2024-01-10');
121-
122149
component.form.startDate = first;
123150
component.form.endDate = second;
124-
125151
component.onRangeSelect({ startDate: third, endDate: third });
126-
127152
expect(component.form.startDate).toEqual(third);
128153
expect(component.form.endDate).toBeNull();
129154
});
130155

131156
it('should update label with custom range if no preset matches', () => {
132-
const start = new Date('2022-01-01T00:00:00Z');
133-
const end = new Date('2022-01-02T00:00:00Z');
134-
157+
const start = new Date('2022-01-01');
158+
const end = new Date('2022-01-02');
135159
component.form.startDate = start;
136160
component.form.endDate = end;
137-
138161
component.updateSelectedLabel();
139-
140162
expect(component.selectedLabel).toContain('2022');
141163
});
164+
165+
it('should select the entire month for "This month" preset', () => {
166+
const preset = component.presets.find(p => p.label === 'This month');
167+
expect(preset).toBeTruthy();
168+
const [start, end] = preset.range();
169+
expect(start).toEqual(startOfMonth(new Date()));
170+
expect(end).toEqual(endOfMonth(new Date()));
171+
});
172+
173+
it('should correctly set range for "This Week So Far"', () => {
174+
const preset = component.presets.find(p => p.label === 'This Week So Far');
175+
expect(preset).toBeTruthy();
176+
const [start, end] = preset.range();
177+
expect(start.getDay()).toBe(0); // Sunday
178+
expect(end <= new Date()).toBeTrue();
179+
expectValidRange(start, end);
180+
});
181+
182+
it('should correctly set range for "Last Week"', () => {
183+
const preset = component.presets.find(p => p.label === 'Last Week');
184+
expect(preset).toBeTruthy();
185+
const [start, end] = preset.range();
186+
expect(start.getDay()).toBe(0); // Sunday
187+
expect(end.getDay()).toBe(6); // Saturday
188+
expect(end < new Date()).toBeTrue();
189+
expectValidRange(start, end);
190+
});
191+
192+
it('should correctly set range for "This Quarter"', () => {
193+
const preset = component.presets.find(p => p.label === 'This Quarter');
194+
expect(preset).toBeTruthy();
195+
const [start, end] = preset.range();
196+
expect(start.getMonth() % 3).toBe(0);
197+
expectValidRange(start, end);
198+
});
199+
200+
it('should correctly set range for "Last Quarter"', () => {
201+
const preset = component.presets.find(p => p.label === 'Last Quarter');
202+
expect(preset).toBeTruthy();
203+
const [start, end] = preset.range();
204+
expect(start.getMonth() % 3).toBe(0);
205+
expectValidRange(start, end);
206+
});
207+
208+
it('should correctly set range for "This Year So Far"', () => {
209+
const preset = component.presets.find(p => p.label === 'This Year So Far');
210+
expect(preset).toBeTruthy();
211+
const [start, end] = preset.range();
212+
expect(start.getMonth()).toBe(0);
213+
expect(end <= new Date()).toBeTrue();
214+
expectValidRange(start, end);
215+
});
216+
217+
it('should correctly set range for "Today"', () => {
218+
const preset = component.presets.find(p => p.label === 'Today');
219+
expect(preset).toBeTruthy();
220+
const [start, end] = preset.range();
221+
expect(start.getDate()).toBe(new Date().getDate());
222+
expect(end.getDate()).toBe(new Date().getDate());
223+
expectValidRange(start, end);
224+
});
142225
});

projects/swimlane/ngx-ui/src/lib/components/date-range-calendar/date-range-picker.component.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import { ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewChild, ViewEncapsulation } from '@angular/core';
2020
import { DateRangeForm } from './models/date-range.model';
2121

22-
import { addMonths, endOfMonth, format, startOfMonth } from 'date-fns';
22+
import { addMonths, endOfMonth, format, isValid, startOfMonth } from 'date-fns';
2323
import { DropdownComponent } from '../dropdown/dropdown.component';
2424
import { DateUtils } from './services/date-utils.service';
2525

@@ -94,13 +94,19 @@ export class DateRangePickerComponent {
9494
this.form.endRaw = this.form.endDate ? format(this.form.endDate, this.dateFormat) : '';
9595

9696
this.updateSelectedPresetByValue();
97+
this.validationError = null;
9798
this.cdr.detectChanges();
9899
}
99100

100101
onCustomInputChange() {
101102
const start = this.parseFn(this.form.startRaw);
102103
const end = this.parseFn(this.form.endRaw);
103104

105+
if (!start || !end || !isValid(start) || !isValid(end)) {
106+
this.validationError = `Invalid date expression`;
107+
return;
108+
}
109+
104110
if (start && end && start <= end) {
105111
this.validationError = null;
106112
this.form.startDate = start;

projects/swimlane/ngx-ui/src/lib/components/date-range-calendar/services/date-utils.service.ts

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ import {
1818
addWeeks,
1919
addQuarters,
2020
subHours,
21-
addHours
21+
addHours,
22+
endOfDay,
23+
endOfMonth,
24+
endOfWeek,
25+
endOfQuarter,
26+
endOfYear
2227
} from 'date-fns';
2328

2429
export class DateUtils {
@@ -31,7 +36,7 @@ export class DateUtils {
3136
if (cleanExpr === 'now/d') return startOfDay(now);
3237
if (cleanExpr === 'now/M') return startOfMonth(now);
3338
if (cleanExpr === 'now/Y') return startOfYear(now);
34-
if (cleanExpr === 'now/w') return startOfWeek(now, { weekStartsOn: 1 });
39+
if (cleanExpr === 'now/w') return startOfWeek(now, { weekStartsOn: 0 });
3540
if (cleanExpr === 'now/Q') return startOfQuarter(now);
3641

3742
const match = cleanExpr.match(/^now([+-])(\d+)([mhdMywQ])(?:\/(\w))?$/);
@@ -75,7 +80,7 @@ export class DateUtils {
7580
case 'Y':
7681
return startOfYear(result);
7782
case 'w':
78-
return startOfWeek(result, { weekStartsOn: 1 });
83+
return startOfWeek(result, { weekStartsOn: 0 });
7984
case 'Q':
8085
return startOfQuarter(result);
8186
}
@@ -85,7 +90,10 @@ export class DateUtils {
8590
}
8691

8792
const fallback = new Date(cleanExpr);
88-
return isValid(fallback) ? fallback : now;
93+
if (!isValid(fallback)) {
94+
return null;
95+
}
96+
return fallback;
8997
}
9098

9199
static getDefaultPresets(_parseFn: (expr: string) => Date): {
@@ -109,40 +117,102 @@ export class DateUtils {
109117
expression: 'now-1h to now',
110118
range: () => [DateUtils.parseExpression('now-1h'), DateUtils.parseExpression('now')]
111119
},
120+
{
121+
label: 'Last 5 hours',
122+
expression: 'now-5h to now',
123+
range: () => [DateUtils.parseExpression('now-5h'), DateUtils.parseExpression('now')]
124+
},
125+
{
126+
label: 'Last 10 hours',
127+
expression: 'now-10h to now',
128+
range: () => [DateUtils.parseExpression('now-10h'), DateUtils.parseExpression('now')]
129+
},
112130
{
113131
label: 'Last 24 hours',
114132
expression: 'now-24h to now',
115133
range: () => [DateUtils.parseExpression('now-24h'), DateUtils.parseExpression('now')]
116134
},
135+
{
136+
label: 'Today',
137+
expression: 'now/d to now/d',
138+
range: () => [startOfDay(new Date()), endOfDay(new Date())]
139+
},
117140
{
118141
label: 'Today so far',
119142
expression: 'now/d to now',
120143
range: () => [DateUtils.parseExpression('now/d'), DateUtils.parseExpression('now')]
121144
},
122145
{
123146
label: 'Yesterday',
124-
expression: 'now-1d/d',
125-
range: () => [DateUtils.parseExpression('now-1d/d'), DateUtils.parseExpression('now-1d/d')]
147+
expression: 'now-1d/d to now-1d/d',
148+
range: () => [DateUtils.parseExpression('now-1d/d'), endOfDay(DateUtils.parseExpression('now-1d/d'))]
149+
},
150+
{
151+
label: 'Last 2 days',
152+
expression: 'now-2d to now',
153+
range: () => [DateUtils.parseExpression('now-2d'), DateUtils.parseExpression('now')]
154+
},
155+
{
156+
label: 'Last 3 days',
157+
expression: 'now-3d to now',
158+
range: () => [DateUtils.parseExpression('now-3d'), DateUtils.parseExpression('now')]
126159
},
127160
{
128161
label: 'Last 7 days',
129162
expression: 'now-7d to now',
130163
range: () => [DateUtils.parseExpression('now-7d'), DateUtils.parseExpression('now')]
131164
},
165+
{
166+
label: 'This Week',
167+
expression: 'now/w to now/w',
168+
range: () => [startOfWeek(new Date(), { weekStartsOn: 0 }), endOfWeek(new Date(), { weekStartsOn: 0 })]
169+
},
170+
{
171+
label: 'This Week So Far',
172+
expression: 'now/w to now',
173+
range: () => [startOfWeek(new Date(), { weekStartsOn: 0 }), new Date()]
174+
},
175+
{
176+
label: 'Last Week',
177+
expression: 'now-1w/w to now-1w/w',
178+
range: () => {
179+
const lastWeek = subWeeks(new Date(), 1);
180+
return [startOfWeek(lastWeek, { weekStartsOn: 0 }), endOfWeek(lastWeek, { weekStartsOn: 0 })];
181+
}
182+
},
132183
{
133184
label: 'This month',
134185
expression: 'now/M to now',
135-
range: () => [DateUtils.parseExpression('now/M'), DateUtils.parseExpression('now')]
186+
range: () => [DateUtils.parseExpression('now/M'), endOfMonth(new Date())]
136187
},
137188
{
138189
label: 'Last month',
139-
expression: 'now-1M/M',
140-
range: () => [DateUtils.parseExpression('now-1M/M'), DateUtils.parseExpression('now-1M/M')]
190+
expression: 'now-1M/M to now-1M/M',
191+
range: () => [DateUtils.parseExpression('now-1M/M'), endOfMonth(DateUtils.parseExpression('now-1M/M'))]
192+
},
193+
{
194+
label: 'This Quarter',
195+
expression: 'now/Q to now',
196+
range: () => [startOfQuarter(new Date()), endOfQuarter(new Date())]
197+
},
198+
{
199+
label: 'Last Quarter',
200+
expression: 'now-1Q/Q to end of last quarter',
201+
range: () => {
202+
const lastQuarterStart = startOfQuarter(subQuarters(new Date(), 1));
203+
const lastQuarterEnd = endOfQuarter(subQuarters(new Date(), 1));
204+
return [lastQuarterStart, lastQuarterEnd];
205+
}
141206
},
142207
{
143208
label: 'This year',
144209
expression: 'now/Y to now',
145-
range: () => [DateUtils.parseExpression('now/Y'), DateUtils.parseExpression('now')]
210+
range: () => [DateUtils.parseExpression('now/Y'), endOfYear(new Date())]
211+
},
212+
{
213+
label: 'This Year So Far',
214+
expression: 'now/Y to now',
215+
range: () => [startOfYear(new Date()), new Date()]
146216
},
147217
{ label: 'Custom range', range: () => [null, null] }
148218
];

projects/swimlane/ngx-ui/src/lib/components/filter/filter.component.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ $max-width: 300px;
8282
border-top-left-radius: 4px;
8383
border-bottom-left-radius: 4px;
8484
overflow: hidden;
85+
max-width: calc($max-width - 33px);
8586
text-overflow: ellipsis;
8687
white-space: nowrap;
8788
user-select: none;
@@ -421,5 +422,9 @@ $max-width: 300px;
421422
max-width: 100%;
422423
width: fit-content;
423424
}
425+
426+
.ngx-chip__contents {
427+
max-width: none;
428+
}
424429
}
425430
}

src/app/components/filters-page/filters-page.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ <h4>Basic</h4>
240240
<br />
241241
<br />
242242
<h4>Date Range Selection Calendar</h4>
243-
<ngx-filter #filterRef [label]="labelForRange"
243+
<ngx-filter #filterRef [label]="labelForRange" [autosize]="true"
244244
[ngClass]="labelForRange !== 'Select a range' ? 'active-selections' : ''" [type]="'customDropdown'"
245245
[customDropdownConfig]="customDropdownDateRangeConfig"
246246
[ngxIconClass]="labelForRange !== 'Select a range' ? 'ngx-x' : null"

0 commit comments

Comments
 (0)