@n8n/frontend-module-insights 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/LICENSE.md +88 -0
  2. package/README.md +64 -0
  3. package/biome.jsonc +4 -0
  4. package/eslint.config.mjs +33 -0
  5. package/package.json +72 -0
  6. package/src/__tests__/setup.ts +4 -0
  7. package/src/chartjs.utils.ts +208 -0
  8. package/src/components/InsightsDashboard.test.ts +891 -0
  9. package/src/components/InsightsDashboard.vue +426 -0
  10. package/src/components/InsightsDataRangePicker.vue +195 -0
  11. package/src/components/InsightsDateRangeAlert.test.ts +134 -0
  12. package/src/components/InsightsDateRangeAlert.vue +89 -0
  13. package/src/components/InsightsPaywall.vue +42 -0
  14. package/src/components/InsightsSummary.test.ts +206 -0
  15. package/src/components/InsightsSummary.vue +357 -0
  16. package/src/components/InsightsUpgradeModal.vue +74 -0
  17. package/src/components/__snapshots__/InsightsSummary.test.ts.snap +446 -0
  18. package/src/components/charts/InsightsChartAverageRuntime.vue +67 -0
  19. package/src/components/charts/InsightsChartFailed.vue +57 -0
  20. package/src/components/charts/InsightsChartFailureRate.vue +58 -0
  21. package/src/components/charts/InsightsChartTimeSaved.vue +74 -0
  22. package/src/components/charts/InsightsChartTotal.vue +58 -0
  23. package/src/components/charts/insightChartProps.ts +9 -0
  24. package/src/components/tables/InsightsTableWorkflows.test.ts +509 -0
  25. package/src/components/tables/InsightsTableWorkflows.vue +279 -0
  26. package/src/index.ts +4 -0
  27. package/src/insights.api.test.ts +435 -0
  28. package/src/insights.api.ts +71 -0
  29. package/src/insights.constants.ts +69 -0
  30. package/src/insights.module.test.ts +59 -0
  31. package/src/insights.module.ts +45 -0
  32. package/src/insights.store.test.ts +118 -0
  33. package/src/insights.store.ts +88 -0
  34. package/src/insights.types.ts +16 -0
  35. package/src/insights.utils.test.ts +339 -0
  36. package/src/insights.utils.ts +172 -0
  37. package/stylelint.config.mjs +9 -0
  38. package/vite.config.ts +29 -0
@@ -0,0 +1,339 @@
1
+ import { CalendarDate, getLocalTimeZone, today } from '@internationalized/date';
2
+ import type { InsightsSummary } from '@n8n/api-types';
3
+
4
+ import {
5
+ INSIGHTS_UNIT_MAPPING,
6
+ INSIGHTS_DEVIATION_UNIT_MAPPING,
7
+ INSIGHTS_SUMMARY_ORDER,
8
+ } from './insights.constants';
9
+ import {
10
+ transformInsightsTimeSaved,
11
+ transformInsightsAverageRunTime,
12
+ transformInsightsFailureRate,
13
+ transformInsightsValues,
14
+ transformInsightsDeviation,
15
+ transformInsightsSummary,
16
+ formatDateRange,
17
+ getMatchingPreset,
18
+ timeRangeMappings,
19
+ } from './insights.utils';
20
+
21
+ describe('Insights Transformers', () => {
22
+ describe('transformInsightsTimeSaved', () => {
23
+ it('should return minutes if absolute value is less than 60', () => {
24
+ expect(transformInsightsTimeSaved(30)).toBe(30);
25
+ expect(transformInsightsTimeSaved(59)).toBe(59);
26
+ expect(transformInsightsTimeSaved(-30)).toBe(-30);
27
+ expect(transformInsightsTimeSaved(-59)).toBe(-59);
28
+ });
29
+
30
+ it('should return hours if absolute value is 60 or more, rounding to nearest hour', () => {
31
+ expect(transformInsightsTimeSaved(60)).toBe(1);
32
+ expect(transformInsightsTimeSaved(120)).toBe(2);
33
+ expect(transformInsightsTimeSaved(89)).toBe(1);
34
+ expect(transformInsightsTimeSaved(90)).toBe(2);
35
+ expect(transformInsightsTimeSaved(-60)).toBe(-1);
36
+ expect(transformInsightsTimeSaved(-120)).toBe(-2);
37
+ expect(transformInsightsTimeSaved(-89)).toBe(-1);
38
+ expect(transformInsightsTimeSaved(-90)).toBe(-1);
39
+ });
40
+
41
+ it('should return 0 for 0 minutes', () => {
42
+ expect(transformInsightsTimeSaved(0)).toBe(0);
43
+ });
44
+ });
45
+
46
+ describe('transformInsightsAverageRunTime', () => {
47
+ it('should convert milliseconds to seconds', () => {
48
+ expect(transformInsightsAverageRunTime(1000)).toBe(1);
49
+ expect(transformInsightsAverageRunTime(500)).toBe(0.5);
50
+ expect(transformInsightsAverageRunTime(1234)).toBe(1.234);
51
+ expect(transformInsightsAverageRunTime(0)).toBe(0);
52
+ });
53
+ });
54
+
55
+ describe('transformInsightsFailureRate', () => {
56
+ it('should convert decimal to percentage', () => {
57
+ expect(transformInsightsFailureRate(0.5)).toBe(50);
58
+ expect(transformInsightsFailureRate(1)).toBe(100);
59
+ expect(transformInsightsFailureRate(0.253)).toBe(25.3);
60
+ expect(transformInsightsFailureRate(0)).toBe(0);
61
+ });
62
+ });
63
+
64
+ describe('transformInsightsValues', () => {
65
+ it('should correctly transform values based on type', () => {
66
+ expect(transformInsightsValues.total(100)).toBe(100);
67
+ expect(transformInsightsValues.failed(10)).toBe(10);
68
+ expect(transformInsightsValues.timeSaved(120)).toBe(2);
69
+ expect(transformInsightsValues.timeSaved(30)).toBe(30);
70
+ expect(transformInsightsValues.averageRunTime(2000)).toBe(2);
71
+ expect(transformInsightsValues.failureRate(0.1)).toBe(10);
72
+ });
73
+ });
74
+
75
+ describe('transformInsightsDeviation', () => {
76
+ describe('for total and failed types', () => {
77
+ it('should calculate percentage deviation', () => {
78
+ expect(transformInsightsDeviation.total(110, 10)).toBe(10);
79
+ expect(transformInsightsDeviation.failed(55, 5)).toBe(10);
80
+ });
81
+
82
+ it('should return 0 if value and deviation are 0', () => {
83
+ expect(transformInsightsDeviation.total(0, 0)).toBe(0);
84
+ expect(transformInsightsDeviation.failed(0, 0)).toBe(0);
85
+ });
86
+
87
+ it('should return Infinity if value equals deviation (and thus previous value is 0)', () => {
88
+ expect(transformInsightsDeviation.total(10, 10)).toBe(null);
89
+ expect(transformInsightsDeviation.failed(5, 5)).toBe(null);
90
+ });
91
+
92
+ it('should return 0 if deviation is 0 and value is not 0', () => {
93
+ expect(transformInsightsDeviation.total(100, 0)).toBe(0);
94
+ expect(transformInsightsDeviation.failed(50, 0)).toBe(0);
95
+ });
96
+ });
97
+
98
+ it('for timeSaved type, should transform deviation using transformInsightsTimeSaved', () => {
99
+ expect(transformInsightsDeviation.timeSaved(1000, 120)).toBe(2);
100
+ expect(transformInsightsDeviation.timeSaved(1000, 30)).toBe(30);
101
+ });
102
+
103
+ it('for averageRunTime type, should transform deviation using transformInsightsAverageRunTime', () => {
104
+ expect(transformInsightsDeviation.averageRunTime(5000, 1000)).toBe(1);
105
+ });
106
+
107
+ it('for failureRate type, should transform deviation using transformInsightsFailureRate', () => {
108
+ expect(transformInsightsDeviation.failureRate(0.5, 0.1)).toBe(10);
109
+ });
110
+ });
111
+
112
+ describe('transformInsightsSummary', () => {
113
+ beforeEach(() => {
114
+ vi.clearAllMocks();
115
+
116
+ INSIGHTS_SUMMARY_ORDER.forEach((key) => {
117
+ vi.spyOn(INSIGHTS_UNIT_MAPPING, key);
118
+ vi.spyOn(INSIGHTS_DEVIATION_UNIT_MAPPING, key);
119
+ });
120
+ });
121
+
122
+ it('should return an empty array if data is null', () => {
123
+ expect(transformInsightsSummary(null)).toEqual([]);
124
+ });
125
+
126
+ it('should correctly transform InsightsSummary data and respect INSIGHTS_SUMMARY_ORDER', () => {
127
+ const summaryData: InsightsSummary = {
128
+ timeSaved: { value: 1200, deviation: 120, unit: 'minute' },
129
+ total: { value: 110, deviation: 10, unit: 'count' },
130
+ failureRate: { value: 0.05, deviation: 0.01, unit: 'ratio' },
131
+ averageRunTime: { value: 5000, deviation: 1000, unit: 'millisecond' },
132
+ failed: { value: 5, deviation: 1, unit: 'count' },
133
+ };
134
+
135
+ const expectedOutput = [
136
+ {
137
+ id: 'total',
138
+ value: 110,
139
+ deviation: 10,
140
+ deviationUnit: '%',
141
+ unit: '',
142
+ },
143
+ {
144
+ id: 'failed',
145
+ value: 5,
146
+ deviation: 25,
147
+ deviationUnit: '%',
148
+ unit: '',
149
+ },
150
+ {
151
+ id: 'failureRate',
152
+ value: 5,
153
+ deviation: 1,
154
+ deviationUnit: 'pp',
155
+ unit: '%',
156
+ },
157
+ {
158
+ id: 'timeSaved',
159
+ value: 20,
160
+ deviation: 2,
161
+ deviationUnit: 'h',
162
+ unit: 'h',
163
+ },
164
+ {
165
+ id: 'averageRunTime',
166
+ value: 5,
167
+ deviation: 1,
168
+ deviationUnit: 's',
169
+ unit: 's',
170
+ },
171
+ ];
172
+
173
+ const result = transformInsightsSummary(summaryData);
174
+ expect(result).toEqual(expectedOutput);
175
+ expect(result.map((item) => item.id)).toEqual([
176
+ 'total',
177
+ 'failed',
178
+ 'failureRate',
179
+ 'timeSaved',
180
+ 'averageRunTime',
181
+ ]);
182
+
183
+ expect(INSIGHTS_UNIT_MAPPING.total).toHaveBeenCalledWith(110);
184
+ expect(INSIGHTS_DEVIATION_UNIT_MAPPING.total).toHaveBeenCalledWith(10);
185
+ expect(INSIGHTS_UNIT_MAPPING.failed).toHaveBeenCalledWith(5);
186
+ expect(INSIGHTS_DEVIATION_UNIT_MAPPING.failed).toHaveBeenCalledWith(1);
187
+ expect(INSIGHTS_UNIT_MAPPING.timeSaved).toHaveBeenCalledWith(1200);
188
+ expect(INSIGHTS_DEVIATION_UNIT_MAPPING.timeSaved).toHaveBeenCalledWith(120);
189
+ expect(INSIGHTS_UNIT_MAPPING.averageRunTime).toHaveBeenCalledWith(5000);
190
+ expect(INSIGHTS_DEVIATION_UNIT_MAPPING.averageRunTime).toHaveBeenCalledWith(1000);
191
+ expect(INSIGHTS_UNIT_MAPPING.failureRate).toHaveBeenCalledWith(0.05);
192
+ expect(INSIGHTS_DEVIATION_UNIT_MAPPING.failureRate).toHaveBeenCalledWith(0.01);
193
+ });
194
+
195
+ it('should handle null deviation correctly', () => {
196
+ const summaryData: InsightsSummary = {
197
+ total: { value: 100, deviation: null, unit: 'count' },
198
+ failed: { value: 5, deviation: 1, unit: 'count' },
199
+ timeSaved: { value: 30, deviation: null, unit: 'minute' },
200
+ averageRunTime: { value: 2000, deviation: 500, unit: 'millisecond' },
201
+ failureRate: { value: 0.1, deviation: null, unit: 'ratio' },
202
+ };
203
+
204
+ const expectedOutput = [
205
+ {
206
+ id: 'total',
207
+ value: 100,
208
+ deviation: null,
209
+ deviationUnit: '',
210
+ unit: '',
211
+ },
212
+ {
213
+ id: 'failed',
214
+ value: 5,
215
+ deviation: 25,
216
+ deviationUnit: '%',
217
+ unit: '',
218
+ },
219
+ {
220
+ id: 'failureRate',
221
+ value: 10,
222
+ deviation: null,
223
+ deviationUnit: '',
224
+ unit: '%',
225
+ },
226
+ {
227
+ id: 'timeSaved',
228
+ value: 30,
229
+ deviation: null,
230
+ deviationUnit: '',
231
+ unit: 'm',
232
+ },
233
+ {
234
+ id: 'averageRunTime',
235
+ value: 2,
236
+ deviation: 0.5,
237
+ deviationUnit: 's',
238
+ unit: 's',
239
+ },
240
+ ];
241
+ expect(transformInsightsSummary(summaryData)).toEqual(expectedOutput);
242
+
243
+ expect(INSIGHTS_UNIT_MAPPING.total).toHaveBeenCalledWith(100);
244
+ expect(INSIGHTS_DEVIATION_UNIT_MAPPING.total).not.toHaveBeenCalled(); // deviation is null
245
+
246
+ expect(INSIGHTS_UNIT_MAPPING.timeSaved).toHaveBeenCalledWith(30);
247
+ expect(INSIGHTS_DEVIATION_UNIT_MAPPING.timeSaved).not.toHaveBeenCalled(); // deviation is null
248
+ });
249
+ });
250
+
251
+ describe('formatDateRange', () => {
252
+ afterEach(() => {
253
+ vi.clearAllMocks();
254
+ });
255
+
256
+ it('should return empty string if start date is not provided', () => {
257
+ const result = formatDateRange({ end: new CalendarDate(2025, 6, 5) });
258
+
259
+ expect(result).toBe('');
260
+ });
261
+
262
+ it('should return formatted start date if the end date is not provided', () => {
263
+ const result = formatDateRange({ start: new CalendarDate(2025, 2, 28) });
264
+
265
+ expect(result).toBe('28 Feb, 2025');
266
+ });
267
+
268
+ it('should return formatted start date if the end date is the same as the start date', () => {
269
+ const result = formatDateRange({
270
+ start: new CalendarDate(2025, 10, 20),
271
+ end: new CalendarDate(2025, 10, 20),
272
+ });
273
+
274
+ expect(result).toBe('20 Oct, 2025');
275
+ });
276
+
277
+ it('should return formatted range for same year range', () => {
278
+ const result = formatDateRange({
279
+ start: new CalendarDate(2025, 10, 20),
280
+ end: new CalendarDate(2025, 10, 25),
281
+ });
282
+
283
+ expect(result).toBe('20 Oct - 25 Oct, 2025');
284
+ });
285
+
286
+ it('should return formatted range for different year range', () => {
287
+ const result = formatDateRange({
288
+ start: new CalendarDate(2024, 12, 20),
289
+ end: new CalendarDate(2025, 1, 5),
290
+ });
291
+
292
+ expect(result).toBe('20 Dec, 2024 - 5 Jan, 2025');
293
+ });
294
+ });
295
+
296
+ describe('getMatchingPreset', () => {
297
+ it('should return null if the end date is not provided', () => {
298
+ const result = getMatchingPreset({ start: new CalendarDate(2025, 10, 20) });
299
+
300
+ expect(result).toBeNull();
301
+ });
302
+
303
+ it('should return null if the start date is not provided', () => {
304
+ const result = getMatchingPreset({ end: new CalendarDate(2025, 10, 20) });
305
+
306
+ expect(result).toBeNull();
307
+ });
308
+
309
+ it('should return null if the end date is not today', () => {
310
+ const result = getMatchingPreset({
311
+ start: new CalendarDate(2025, 10, 20),
312
+ end: new CalendarDate(2025, 10, 21),
313
+ });
314
+
315
+ expect(result).toBeNull();
316
+ });
317
+
318
+ it('should return null for custom range', () => {
319
+ const result = getMatchingPreset({
320
+ start: new CalendarDate(2025, 10, 20),
321
+ end: today(getLocalTimeZone()),
322
+ });
323
+
324
+ expect(result).toBeNull();
325
+ });
326
+
327
+ test.each(Object.entries(timeRangeMappings))(
328
+ 'should return the %s" preset for for %d days',
329
+ (key, days) => {
330
+ const end = today(getLocalTimeZone());
331
+ const start = end.subtract({ days });
332
+
333
+ const result = getMatchingPreset({ start, end });
334
+
335
+ expect(result).toBe(key);
336
+ },
337
+ );
338
+ });
339
+ });
@@ -0,0 +1,172 @@
1
+ import type { DateValue } from '@internationalized/date';
2
+ import { getLocalTimeZone, isToday, now, toCalendarDateTime, today } from '@internationalized/date';
3
+ import type { InsightsDateRange, InsightsSummary, InsightsSummaryType } from '@n8n/api-types';
4
+ import type { DateRange } from '@n8n/design-system';
5
+ import { useI18n } from '@n8n/i18n';
6
+ import dateformat from 'dateformat';
7
+
8
+ import {
9
+ INSIGHTS_DEVIATION_UNIT_MAPPING,
10
+ INSIGHTS_SUMMARY_ORDER,
11
+ INSIGHTS_UNIT_MAPPING,
12
+ } from './insights.constants';
13
+ import type { InsightsSummaryDisplay } from './insights.types';
14
+
15
+ const DATE_FORMAT_DAY_MONTH_YEAR = 'd mmm, yyyy';
16
+ const DATE_FORMAT_DAY_MONTH = 'd mmm';
17
+
18
+ export const transformInsightsTimeSaved = (minutes: number): number =>
19
+ Math.round(minutes / (Math.abs(minutes) < 60 ? 1 : 60)); // we want to show saved time in minutes or hours
20
+ export const transformInsightsAverageRunTime = (ms: number): number => ms / 1000; // we want to show average run time in seconds
21
+ export const transformInsightsFailureRate = (value: number): number => value * 100; // we want to show failure rate in percentage
22
+
23
+ export const transformInsightsValues: Record<InsightsSummaryType, (value: number) => number> = {
24
+ total: (value: number) => value,
25
+ failed: (value: number) => value,
26
+ timeSaved: transformInsightsTimeSaved,
27
+ averageRunTime: transformInsightsAverageRunTime,
28
+ failureRate: transformInsightsFailureRate,
29
+ };
30
+
31
+ const getPreviousValue = (value: number, deviation: number): number => value - deviation;
32
+
33
+ const getDeviation = (value: number, deviation: number): number | null => {
34
+ if (value === 0 && deviation === 0) return 0;
35
+
36
+ const previousValue = getPreviousValue(value, deviation);
37
+ if (previousValue === 0) return null; // avoid division by zero
38
+
39
+ return (deviation / previousValue) * 100;
40
+ };
41
+
42
+ export const transformInsightsDeviation: Record<
43
+ InsightsSummaryType,
44
+ (value: number, deviation: number) => number | null
45
+ > = {
46
+ total: getDeviation,
47
+ failed: getDeviation,
48
+ timeSaved: (_: number, deviation: number) => transformInsightsTimeSaved(deviation),
49
+ averageRunTime: (_: number, deviation: number) => transformInsightsAverageRunTime(deviation),
50
+ failureRate: (_: number, deviation: number) => transformInsightsFailureRate(deviation),
51
+ };
52
+
53
+ export const transformInsightsSummary = (data: InsightsSummary | null): InsightsSummaryDisplay =>
54
+ data
55
+ ? INSIGHTS_SUMMARY_ORDER.map((key) => ({
56
+ id: key,
57
+ value: transformInsightsValues[key](data[key].value),
58
+ deviation:
59
+ data[key].deviation === null
60
+ ? null
61
+ : transformInsightsDeviation[key](data[key].value, data[key].deviation),
62
+ deviationUnit:
63
+ data[key].deviation === null
64
+ ? ''
65
+ : INSIGHTS_DEVIATION_UNIT_MAPPING[key](data[key].deviation),
66
+ unit: INSIGHTS_UNIT_MAPPING[key](data[key].value),
67
+ }))
68
+ : [];
69
+
70
+ export const timeRangeMappings: Record<InsightsDateRange['key'], number> = {
71
+ day: 1,
72
+ week: 7,
73
+ '2weeks': 14,
74
+ month: 30,
75
+ quarter: 90,
76
+ '6months': 180,
77
+ year: 365,
78
+ } as const;
79
+
80
+ export const getTimeRangeLabels = () => {
81
+ const i18n = useI18n();
82
+
83
+ return {
84
+ day: i18n.baseText('insights.lastNHours', { interpolate: { count: 24 } }),
85
+ week: i18n.baseText('insights.lastNDays', { interpolate: { count: 7 } }),
86
+ '2weeks': i18n.baseText('insights.lastNDays', { interpolate: { count: 14 } }),
87
+ month: i18n.baseText('insights.lastNDays', { interpolate: { count: 30 } }),
88
+ quarter: i18n.baseText('insights.lastNDays', { interpolate: { count: 90 } }),
89
+ '6months': i18n.baseText('insights.months', { interpolate: { count: 6 } }),
90
+ year: i18n.baseText('insights.oneYear'),
91
+ };
92
+ };
93
+
94
+ /**
95
+ * @returns A human readable string representing the date range e.g '01 Jan - 05 Jan 2025'
96
+ */
97
+ export const formatDateRange = (range: { start?: DateValue; end?: DateValue }): string => {
98
+ const { start, end } = range;
99
+ if (!start) return '';
100
+
101
+ const startDate = start.toDate(getLocalTimeZone());
102
+ const endDate = end?.toDate(getLocalTimeZone());
103
+
104
+ if (!end || start.compare(end) === 0) {
105
+ return dateformat(startDate, DATE_FORMAT_DAY_MONTH_YEAR);
106
+ }
107
+
108
+ if (start.year === end.year) {
109
+ return `${dateformat(startDate, DATE_FORMAT_DAY_MONTH)} - ${dateformat(endDate, DATE_FORMAT_DAY_MONTH_YEAR)}`;
110
+ }
111
+
112
+ return `${dateformat(startDate, DATE_FORMAT_DAY_MONTH_YEAR)} - ${dateformat(endDate, DATE_FORMAT_DAY_MONTH_YEAR)}`;
113
+ };
114
+
115
+ /**
116
+ * @returns The matching preset key if the range matches a preset, null for custom ranges
117
+ */
118
+ export const getMatchingPreset = (range: { start?: DateValue; end?: DateValue }):
119
+ | InsightsDateRange['key']
120
+ | null => {
121
+ const { start, end } = range;
122
+ if (!start || !end || !isToday(end, getLocalTimeZone())) return null;
123
+
124
+ const daysDiff = end.compare(start);
125
+
126
+ for (const [key, days] of Object.entries(timeRangeMappings)) {
127
+ if (daysDiff === days) return key as InsightsDateRange['key'];
128
+ }
129
+
130
+ return null;
131
+ };
132
+
133
+ /**
134
+ * Converts DateValue range to adjusted Date objects for API calls
135
+ * - For single-day ranges ending today: injects current time into both dates (exact 24 hours)
136
+ * - For multi-day ranges ending today: injects current time into end date only
137
+ * - For past dates: uses end-of-day for end date
138
+ *
139
+ * @param dateRange - The date range to adjust
140
+ */
141
+ export const getAdjustedDateRange = (dateRange: DateRange): { startDate: Date; endDate: Date } => {
142
+ if (!dateRange.start || !dateRange.end) return { startDate: new Date(), endDate: new Date() };
143
+
144
+ const timezone = getLocalTimeZone();
145
+
146
+ const todayInTimezone = today(timezone);
147
+ const isEndDateToday = dateRange.end && dateRange.end.compare(todayInTimezone) === 0;
148
+ const daysDiff = dateRange.end && dateRange.start ? dateRange.end.compare(dateRange.start) : 0;
149
+
150
+ if (isEndDateToday) {
151
+ const nowInTimezone = now(timezone);
152
+
153
+ if (daysDiff === 1) {
154
+ // For single-day ranges (e.g., "Last 24 hours"), inject current time into both dates
155
+ // This gives exact 24-hour ranges
156
+ const startDate = toCalendarDateTime(dateRange.start, nowInTimezone).toDate(timezone);
157
+ const endDate = toCalendarDateTime(dateRange.end, nowInTimezone).toDate(timezone);
158
+ return { startDate, endDate };
159
+ } else {
160
+ // For multi-day ranges, use start-of-day for start but current time for end
161
+ const startDate = dateRange.start?.toDate(timezone) ?? new Date();
162
+ const endDate = toCalendarDateTime(dateRange.end, nowInTimezone).toDate(timezone);
163
+ return { startDate, endDate };
164
+ }
165
+ } else {
166
+ // For past dates, use beginning of day for start and end of day for end to ensure full day coverage
167
+ const startDate = dateRange.start?.toDate(timezone) ?? new Date();
168
+ const endDate = dateRange.end?.toDate(timezone) ?? new Date();
169
+ endDate.setHours(23, 59, 59, 999);
170
+ return { startDate, endDate };
171
+ }
172
+ };
@@ -0,0 +1,9 @@
1
+ import { baseConfig } from '@n8n/stylelint-config/base';
2
+
3
+ export default {
4
+ ...baseConfig,
5
+ rules: {
6
+ ...baseConfig.rules,
7
+ '@n8n/css-var-naming': [true, { severity: 'error' }],
8
+ },
9
+ };
package/vite.config.ts ADDED
@@ -0,0 +1,29 @@
1
+ import vue from '@vitejs/plugin-vue';
2
+ import icons from 'unplugin-icons/vite';
3
+ import svgLoader from 'vite-svg-loader';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { resolve } from 'node:path';
6
+ import { defineConfig, mergeConfig } from 'vite';
7
+ import { frontendAliases } from '@n8n/frontend-vite-config';
8
+ import { vitestConfig } from '@n8n/vitest-config/frontend';
9
+
10
+ const packageDir = fileURLToPath(new URL('.', import.meta.url));
11
+ const packagesDir = resolve(packageDir, '..', '..', '..');
12
+
13
+ export default mergeConfig(
14
+ defineConfig({
15
+ // What lets a test render a design-system component. Its icon set imports
16
+ // `~icons/lucide/*` (unplugin-icons) and `./custom/*.svg` (svgLoader); without
17
+ // either, an icon resolves to a URL string and rendering throws. The shared
18
+ // vitest config stubs only the `nodes/*.svg` subset.
19
+ plugins: [vue(), icons({ compiler: 'vue3' }), svgLoader()],
20
+ resolve: {
21
+ // The same platform mapping the editor-ui dev server uses, so a test resolves
22
+ // `@n8n/stores/...` from source rather than from a stale `dist` — the two disagreeing is
23
+ // what put 1,111 specifiers on the wrong side of the src/dist line. Sibling modules are
24
+ // deliberately absent: nothing here should make a cross-module import resolve.
25
+ alias: frontendAliases(packagesDir),
26
+ },
27
+ }),
28
+ vitestConfig,
29
+ );