Skip to content

Commit 2c8bfac

Browse files
authored
feat(analytics): Allow to choose the same day as a range (dotCMS#33026)
- Introduced a new signal method `#handleChangeCustomDateRange` in `DotAnalyticsDashboardFiltersComponent` to manage custom date range changes and update URL parameters accordingly. - Refactored the existing logic to utilize the signals system for improved state management and reactivity. - Updated utility function `isValidCustomDateRange` to leverage `date-fns` for better date validation. - Removed outdated validation functions and tests related to date handling, streamlining the utility file. This enhancement improves the user experience by allowing dynamic updates to the date range filters while maintaining clean and modular code. ### Checklist - [x] Tests - [x] Translations - [x] Security Implications Contemplated (add notes if applicable)
1 parent 6d7a3cb commit 2c8bfac

File tree

7 files changed

+109
-273
lines changed

7 files changed

+109
-273
lines changed

core-web/libs/portlets/dot-analytics/data-access/src/lib/services/dot-analytics.service.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,7 @@ export class DotAnalyticsService {
9898
siteId: string | string[]
9999
): Observable<PageViewTimeLineEntity[]> {
100100
// Determine granularity based on specific timeRange values
101-
const granularity = Array.isArray(timeRange)
102-
? 'day' // For custom date ranges, default to day granularity
103-
: determineGranularityForTimeRange(timeRange);
101+
const granularity = determineGranularityForTimeRange(timeRange);
104102

105103
const queryBuilder = createCubeQuery()
106104
.measures(['totalRequest'])

core-web/libs/portlets/dot-analytics/data-access/src/lib/utils/data/analytics-data.utils.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,5 +722,22 @@ describe('Analytics Data Utils', () => {
722722
});
723723
});
724724
});
725+
726+
describe('custom date range', () => {
727+
it('should return day granularity for custom date range on the same month', () => {
728+
const result = determineGranularityForTimeRange(['2024-01-01', '2024-01-31']);
729+
expect(result).toBe('day');
730+
});
731+
732+
it('should return hour granularity for custom date range on the same day', () => {
733+
const result = determineGranularityForTimeRange(['2024-01-01', '2024-01-01']);
734+
expect(result).toBe('hour');
735+
});
736+
737+
it('should return month granularity for custom date range', () => {
738+
const result = determineGranularityForTimeRange(['2024-01-01', '2024-04-31']);
739+
expect(result).toBe('month');
740+
});
741+
});
725742
});
726743
});

core-web/libs/portlets/dot-analytics/data-access/src/lib/utils/data/analytics-data.utils.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { format, isSameDay } from 'date-fns';
1+
import { format, isSameDay, isSameMonth } from 'date-fns';
22

33
import {
44
ChartData,
55
Granularity,
66
PageViewDeviceBrowsersEntity,
77
PageViewTimeLineEntity,
88
TablePageData,
9-
TimeRange,
9+
TimeRangeInput,
1010
TopPagePerformanceEntity,
1111
TopPerformaceTableEntity,
1212
TotalPageViewsEntity,
@@ -35,7 +35,19 @@ const TIME_FORMATS = {
3535
* @param timeRange - The time range for the analytics query
3636
* @returns The appropriate granularity level for the given time range
3737
*/
38-
export function determineGranularityForTimeRange(timeRange: TimeRange): Granularity {
38+
export function determineGranularityForTimeRange(timeRange: TimeRangeInput): Granularity {
39+
if (Array.isArray(timeRange)) {
40+
const [fromDate, toDate] = timeRange.map((date) => new Date(date));
41+
42+
if (isSameDay(fromDate, toDate)) {
43+
return 'hour';
44+
} else if (isSameMonth(fromDate, toDate)) {
45+
return 'day';
46+
} else {
47+
return 'month';
48+
}
49+
}
50+
3951
switch (timeRange) {
4052
case 'today':
4153

core-web/libs/portlets/dot-analytics/portlet/src/lib/dot-analytics-dashboard/components/dot-analytics-dashboard-filters/dot-analytics-dashboard-filters.component.spec.ts

Lines changed: 28 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe('DotAnalyticsDashboardFiltersComponent', () => {
1515

1616
const mockActivatedRoute = {
1717
snapshot: {
18-
queryParams: {}
18+
queryParamMap: new Map()
1919
}
2020
};
2121

@@ -224,21 +224,19 @@ describe('DotAnalyticsDashboardFiltersComponent', () => {
224224

225225
describe('URL Initialization', () => {
226226
it('should initialize from URL with predefined time range', () => {
227-
mockActivatedRoute.snapshot.queryParams = {
228-
time_range: 'last7days'
229-
};
227+
mockActivatedRoute.snapshot.queryParamMap = new Map([['time_range', 'last7days']]);
230228

231229
spectator = createComponent();
232230

233231
expect(spectator.component.$selectedTimeRange()).toBe('from 7 days ago to now');
234232
});
235233

236234
it('should initialize from URL with custom date range', () => {
237-
mockActivatedRoute.snapshot.queryParams = {
238-
time_range: 'custom',
239-
from: '2024-01-01',
240-
to: '2024-01-31'
241-
};
235+
mockActivatedRoute.snapshot.queryParamMap = new Map([
236+
['time_range', 'custom'],
237+
['from', '2024-01-01'],
238+
['to', '2024-01-31']
239+
]);
242240

243241
spectator = createComponent();
244242

@@ -250,9 +248,7 @@ describe('DotAnalyticsDashboardFiltersComponent', () => {
250248
});
251249

252250
it('should not initialize from invalid URL params', () => {
253-
mockActivatedRoute.snapshot.queryParams = {
254-
time_range: 'invalid-range'
255-
};
251+
mockActivatedRoute.snapshot.queryParamMap = new Map([['time_range', 'invalid-range']]);
256252

257253
spectator = createComponent();
258254

@@ -269,9 +265,7 @@ describe('DotAnalyticsDashboardFiltersComponent', () => {
269265
});
270266

271267
it('should not initialize custom range without from/to params', () => {
272-
mockActivatedRoute.snapshot.queryParams = {
273-
time_range: 'custom'
274-
};
268+
mockActivatedRoute.snapshot.queryParamMap = new Map([['time_range', 'custom']]);
275269

276270
spectator = createComponent();
277271

@@ -280,11 +274,11 @@ describe('DotAnalyticsDashboardFiltersComponent', () => {
280274
});
281275

282276
it('should fall back to default when custom dates are invalid', () => {
283-
mockActivatedRoute.snapshot.queryParams = {
284-
time_range: 'custom',
285-
from: 'invalid-date',
286-
to: '2024-01-31'
287-
};
277+
mockActivatedRoute.snapshot.queryParamMap = new Map([
278+
['time_range', 'custom'],
279+
['from', 'invalid-date'],
280+
['to', '2024-01-31']
281+
]);
288282

289283
spectator = createComponent();
290284

@@ -302,11 +296,11 @@ describe('DotAnalyticsDashboardFiltersComponent', () => {
302296
});
303297

304298
it('should fall back to default when from date is after to date', () => {
305-
mockActivatedRoute.snapshot.queryParams = {
306-
time_range: 'custom',
307-
from: '2024-01-31', // After to date
308-
to: '2024-01-01'
309-
};
299+
mockActivatedRoute.snapshot.queryParamMap = new Map([
300+
['time_range', 'custom'],
301+
['from', '2024-01-31'], // After to date
302+
['to', '2024-01-01']
303+
]);
310304

311305
spectator = createComponent();
312306

@@ -324,11 +318,11 @@ describe('DotAnalyticsDashboardFiltersComponent', () => {
324318
});
325319

326320
it('should fall back to default when both dates are invalid', () => {
327-
mockActivatedRoute.snapshot.queryParams = {
328-
time_range: 'custom',
329-
from: 'not-a-date',
330-
to: 'also-not-a-date'
331-
};
321+
mockActivatedRoute.snapshot.queryParamMap = new Map([
322+
['time_range', 'custom'],
323+
['from', 'not-a-date'],
324+
['to', 'also-not-a-date']
325+
]);
332326

333327
spectator = createComponent();
334328

@@ -337,9 +331,9 @@ describe('DotAnalyticsDashboardFiltersComponent', () => {
337331
});
338332

339333
it('should fall back to default when time_range is invalid predefined value', () => {
340-
mockActivatedRoute.snapshot.queryParams = {
341-
time_range: 'invalid-time-range'
342-
};
334+
mockActivatedRoute.snapshot.queryParamMap = new Map([
335+
['time_range', 'invalid-time-range']
336+
]);
343337

344338
spectator = createComponent();
345339

@@ -376,9 +370,7 @@ describe('DotAnalyticsDashboardFiltersComponent', () => {
376370

377371
it('should avoid infinite loops with URL synchronization', async () => {
378372
// Set up URL state that matches what we're about to set
379-
mockActivatedRoute.snapshot.queryParams = {
380-
time_range: 'last7days'
381-
};
373+
mockActivatedRoute.snapshot.queryParamMap = new Map([['time_range', 'last7days']]);
382374

383375
// This should not trigger URL update since it matches
384376
spectator.component.$selectedTimeRange.set('from 7 days ago to now');

core-web/libs/portlets/dot-analytics/portlet/src/lib/dot-analytics-dashboard/components/dot-analytics-dashboard-filters/dot-analytics-dashboard-filters.component.ts

Lines changed: 42 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { signalMethod } from '@ngrx/signals';
2+
13
import { CommonModule } from '@angular/common';
24
import {
35
ChangeDetectionStrategy,
@@ -62,6 +64,40 @@ export class DotAnalyticsDashboardFiltersComponent {
6264
/** Check if custom time range is selected */
6365
readonly $showCustomTimeRange = computed(() => this.$selectedTimeRange() === CUSTOM_TIME_RANGE);
6466

67+
readonly #handleChangeCustomDateRange = signalMethod<Date[] | null>((dateRange) => {
68+
if (!dateRange || dateRange.length !== 2 || !dateRange[0] || !dateRange[1]) {
69+
return;
70+
}
71+
const customRange: DateRange = [
72+
dateRange[0].toISOString().split('T')[0],
73+
dateRange[1].toISOString().split('T')[0]
74+
];
75+
76+
const queryParams = this.route.snapshot.queryParamMap;
77+
78+
// Read current URL params without creating dependency
79+
const currentUrlParams = {
80+
timeRange: queryParams.get('time_range'),
81+
from: queryParams.get('from'),
82+
to: queryParams.get('to')
83+
};
84+
85+
// Convert URL time_range to internal for comparison
86+
const currentInternalTimeRange = currentUrlParams.timeRange
87+
? fromUrlFriendly(currentUrlParams.timeRange)
88+
: null;
89+
90+
// Only update URL if different from current values
91+
if (
92+
currentInternalTimeRange !== CUSTOM_TIME_RANGE ||
93+
currentUrlParams.from !== customRange[0] ||
94+
currentUrlParams.to !== customRange[1]
95+
) {
96+
// Update URL with custom date range query params
97+
this.updateCustomDateRangeParams(customRange);
98+
}
99+
});
100+
65101
constructor() {
66102
// Initialize from URL params
67103
this.initFromUrl();
@@ -82,7 +118,7 @@ export class DotAnalyticsDashboardFiltersComponent {
82118

83119
// Read current URL param without creating dependency
84120
const currentUrlTimeRange = untracked(() => {
85-
return this.route.snapshot.queryParams['time_range'];
121+
return this.route.snapshot.queryParamMap.get('time_range');
86122
});
87123

88124
// Convert current URL value to internal for comparison
@@ -100,43 +136,7 @@ export class DotAnalyticsDashboardFiltersComponent {
100136
}
101137
});
102138

103-
// Handle custom date range changes and emit formatted dates
104-
effect(() => {
105-
const dateRange = this.$customDateRange();
106-
107-
if (dateRange && dateRange.length === 2 && dateRange[0] && dateRange[1]) {
108-
const customRange: DateRange = [
109-
dateRange[0].toISOString().split('T')[0],
110-
dateRange[1].toISOString().split('T')[0]
111-
];
112-
113-
// Read current URL params without creating dependency
114-
const currentUrlParams = untracked(() => {
115-
const params = this.route.snapshot.queryParams;
116-
117-
return {
118-
timeRange: params['time_range'],
119-
from: params['from'],
120-
to: params['to']
121-
};
122-
});
123-
124-
// Convert URL time_range to internal for comparison
125-
const currentInternalTimeRange = currentUrlParams.timeRange
126-
? fromUrlFriendly(currentUrlParams.timeRange)
127-
: null;
128-
129-
// Only update URL if different from current values
130-
if (
131-
currentInternalTimeRange !== CUSTOM_TIME_RANGE ||
132-
currentUrlParams.from !== customRange[0] ||
133-
currentUrlParams.to !== customRange[1]
134-
) {
135-
// Update URL with custom date range query params
136-
this.updateCustomDateRangeParams(customRange);
137-
}
138-
}
139-
});
139+
this.#handleChangeCustomDateRange(this.$customDateRange);
140140
}
141141

142142
/** Translated time period options for display */
@@ -161,10 +161,10 @@ export class DotAnalyticsDashboardFiltersComponent {
161161
* Initialize filter state from URL parameters
162162
*/
163163
private initFromUrl(): void {
164-
const params = this.route.snapshot.queryParams;
165-
const urlTimeRange = params['time_range'];
166-
const fromDate = params['from'];
167-
const toDate = params['to'];
164+
const params = this.route.snapshot.queryParamMap;
165+
const urlTimeRange = params.get('time_range');
166+
const fromDate = params.get('from');
167+
const toDate = params.get('to');
168168

169169
if (urlTimeRange) {
170170
// Convert URL-friendly value to internal value

0 commit comments

Comments
 (0)