diff --git a/CHANGELOG.md b/CHANGELOG.md
index c0f8da11..a7309395 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
+## [3.8.1] - 2022-12-06
+### Added
+* *Nothing*
+
+### Changed
+* *Nothing*
+
+### Deprecated
+* *Nothing*
+
+### Removed
+* *Nothing*
+
+### Fixed
+* [#756](https://github.com/shlinkio/shlink-web-client/issues/756) Fixed all visits interval not working unless switching to a different interval first.
+* [#757](https://github.com/shlinkio/shlink-web-client/issues/757) Fixed visits fallback interval not working until the visits view has been loaded at least twice.
+
+
## [3.8.0] - 2022-12-03
### Added
* [#708](https://github.com/shlinkio/shlink-web-client/issues/708) Added support for API v3.
diff --git a/src/utils/dates/DateRangeSelector.tsx b/src/utils/dates/DateRangeSelector.tsx
index 652d3c11..3aaeaf9e 100644
--- a/src/utils/dates/DateRangeSelector.tsx
+++ b/src/utils/dates/DateRangeSelector.tsx
@@ -9,6 +9,7 @@ import {
intervalToDateRange,
rangeIsInterval,
dateRangeIsEmpty,
+ ALL,
} from '../helpers/dateIntervals';
import { DateRangeRow } from './DateRangeRow';
import { DateIntervalDropdownItems } from './DateIntervalDropdownItems';
@@ -31,7 +32,7 @@ export const DateRangeSelector = (
const [activeDateRange, setActiveDateRange] = useState(initialIntervalIsRange ? undefined : initialDateRange);
const updateDateRange = (dateRange: DateRange) => {
- setActiveInterval(dateRangeIsEmpty(dateRange) ? 'all' : undefined);
+ setActiveInterval(dateRangeIsEmpty(dateRange) ? ALL : undefined);
setActiveDateRange(dateRange);
onDatesChange(dateRange);
};
diff --git a/src/utils/dates/Time.tsx b/src/utils/dates/Time.tsx
index a06381df..e2418659 100644
--- a/src/utils/dates/Time.tsx
+++ b/src/utils/dates/Time.tsx
@@ -1,5 +1,5 @@
import { parseISO, format as formatDate, getUnixTime, formatDistance } from 'date-fns';
-import { isDateObject, STANDARD_DATE_AND_TIME_FORMAT } from '../helpers/date';
+import { isDateObject, now, STANDARD_DATE_AND_TIME_FORMAT } from '../helpers/date';
export interface TimeProps {
date: Date | string;
@@ -12,7 +12,7 @@ export const Time = ({ date, format = STANDARD_DATE_AND_TIME_FORMAT, relative =
return (
);
};
diff --git a/src/utils/helpers/date.ts b/src/utils/helpers/date.ts
index e9489193..ee74e992 100644
--- a/src/utils/helpers/date.ts
+++ b/src/utils/helpers/date.ts
@@ -9,6 +9,8 @@ export type DateOrString = Date | string;
type NullableDate = DateOrString | null;
+export const now = () => new Date();
+
export const isDateObject = (date: DateOrString): date is Date => typeof date !== 'string';
const formatDateFromFormat = (date?: NullableDate, theFormat?: string): OptionalString => {
@@ -28,7 +30,7 @@ export const formatIsoDate = (date?: NullableDate) => formatDateFromFormat(date,
export const formatInternational = formatDate();
-export const parseDate = (date: string, theFormat: string) => parse(date, theFormat, new Date());
+export const parseDate = (date: string, theFormat: string) => parse(date, theFormat, now());
export const parseISO = (date: DateOrString): Date => (isDateObject(date) ? date : stdParseISO(date));
diff --git a/src/utils/helpers/dateIntervals.ts b/src/utils/helpers/dateIntervals.ts
index f81addb8..248fcb5a 100644
--- a/src/utils/helpers/dateIntervals.ts
+++ b/src/utils/helpers/dateIntervals.ts
@@ -1,13 +1,14 @@
import { subDays, startOfDay, endOfDay } from 'date-fns';
import { cond, filter, isEmpty, T } from 'ramda';
-import { dateOrNull, DateOrString, formatInternational, isBeforeOrEqual, parseISO } from './date';
+import { dateOrNull, DateOrString, formatInternational, isBeforeOrEqual, now, parseISO } from './date';
+import { equals } from '../utils';
export interface DateRange {
startDate?: Date | null;
endDate?: Date | null;
}
-const ALL = 'all';
+export const ALL = 'all';
const INTERVAL_TO_STRING_MAP = {
today: 'Today',
yesterday: 'Yesterday',
@@ -64,39 +65,25 @@ export const rangeOrIntervalToString = (range?: DateRange | DateInterval): strin
return INTERVAL_TO_STRING_MAP[range];
};
-const startOfDaysAgo = (daysAgo: number) => startOfDay(subDays(new Date(), daysAgo));
-const endingToday = (startDate: Date): DateRange => ({ startDate, endDate: endOfDay(new Date()) });
+const startOfDaysAgo = (daysAgo: number) => startOfDay(subDays(now(), daysAgo));
+const endingToday = (startDate: Date): DateRange => ({ startDate, endDate: endOfDay(now()) });
-export const intervalToDateRange = (dateInterval?: DateInterval): DateRange => {
- if (!dateInterval || dateInterval === ALL) {
- return {};
- }
-
- switch (dateInterval) {
- case 'today':
- return endingToday(startOfDay(new Date()));
- case 'yesterday':
- return { startDate: startOfDaysAgo(1), endDate: endOfDay(subDays(new Date(), 1)) };
- case 'last7Days':
- return endingToday(startOfDaysAgo(7));
- case 'last30Days':
- return endingToday(startOfDaysAgo(30));
- case 'last90Days':
- return endingToday(startOfDaysAgo(90));
- case 'last180Days':
- return endingToday(startOfDaysAgo(180));
- case 'last365Days':
- return endingToday(startOfDaysAgo(365));
- }
-
- return {};
-};
+export const intervalToDateRange = cond<[DateInterval | undefined], DateRange>([
+ [equals('today'), () => endingToday(startOfDay(now()))],
+ [equals('yesterday'), () => ({ startDate: startOfDaysAgo(1), endDate: endOfDay(subDays(now(), 1)) })],
+ [equals('last7Days'), () => endingToday(startOfDaysAgo(7))],
+ [equals('last30Days'), () => endingToday(startOfDaysAgo(30))],
+ [equals('last90Days'), () => endingToday(startOfDaysAgo(90))],
+ [equals('last180Days'), () => endingToday(startOfDaysAgo(180))],
+ [equals('last365Days'), () => endingToday(startOfDaysAgo(365))],
+ [T, () => ({})],
+]);
export const dateToMatchingInterval = (date: DateOrString): DateInterval => {
- const theDate: Date = parseISO(date);
+ const theDate = parseISO(date);
return cond([
- [() => isBeforeOrEqual(startOfDay(new Date()), theDate), () => 'today'],
+ [() => isBeforeOrEqual(startOfDay(now()), theDate), () => 'today'],
[() => isBeforeOrEqual(startOfDaysAgo(1), theDate), () => 'yesterday'],
[() => isBeforeOrEqual(startOfDaysAgo(7), theDate), () => 'last7Days'],
[() => isBeforeOrEqual(startOfDaysAgo(30), theDate), () => 'last30Days'],
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
index 44654994..8eda7256 100644
--- a/src/utils/utils.ts
+++ b/src/utils/utils.ts
@@ -24,3 +24,5 @@ export type OptionalString = Optional;
export const nonEmptyValueOrNull = (value: T): T | null => (isEmpty(value) ? null : value);
export const capitalize = (value: T): string => `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
+
+export const equals = (value: any) => (otherValue: any) => value === otherValue;
diff --git a/src/visits/VisitsStats.tsx b/src/visits/VisitsStats.tsx
index a6110bc4..ff3e74c2 100644
--- a/src/visits/VisitsStats.tsx
+++ b/src/visits/VisitsStats.tsx
@@ -122,6 +122,12 @@ export const VisitsStats: FC = ({
getVisits({ dateRange: resolvedDateRange, filter: visitsFilter }, isFirstLoad.current);
isFirstLoad.current = false;
}, [dateRange, visitsFilter]);
+ useEffect(() => {
+ // As soon as the fallback is loaded, if the initial interval used the settings one, we do fall back
+ if (fallbackInterval && initialInterval.current === (settings.visits?.defaultInterval ?? 'last30Days')) {
+ initialInterval.current = fallbackInterval;
+ }
+ }, [fallbackInterval]);
const renderVisitsContent = () => {
if (loadingLarge) {
diff --git a/src/visits/helpers/hooks.ts b/src/visits/helpers/hooks.ts
index af19d9c8..2c78b65f 100644
--- a/src/visits/helpers/hooks.ts
+++ b/src/visits/helpers/hooks.ts
@@ -37,7 +37,7 @@ export const useVisitsQuery = (): [VisitsFiltering, UpdateFiltering] => {
({ startDate, endDate, orphanVisitsType, excludeBots, domain }: VisitsQuery): VisitsFilteringAndDomain => ({
domain,
filtering: {
- dateRange: startDate || endDate ? datesToDateRange(startDate, endDate) : undefined,
+ dateRange: startDate != null || endDate != null ? datesToDateRange(startDate, endDate) : undefined,
visitsFilter: { orphanVisitsType, excludeBots: excludeBots === 'true' },
},
}),
@@ -47,8 +47,8 @@ export const useVisitsQuery = (): [VisitsFiltering, UpdateFiltering] => {
const updateFiltering = (extra: DeepPartial) => {
const { dateRange, visitsFilter } = mergeDeepRight(filtering, extra);
const query: VisitsQuery = {
- startDate: (dateRange?.startDate && formatIsoDate(dateRange.startDate)) || undefined,
- endDate: (dateRange?.endDate && formatIsoDate(dateRange.endDate)) || undefined,
+ startDate: (dateRange?.startDate && formatIsoDate(dateRange.startDate)) || '',
+ endDate: (dateRange?.endDate && formatIsoDate(dateRange.endDate)) || '',
excludeBots: visitsFilter.excludeBots ? 'true' : undefined,
orphanVisitsType: visitsFilter.orphanVisitsType,
domain: theDomain,
diff --git a/test/utils/helpers/dateIntervals.test.ts b/test/utils/helpers/dateIntervals.test.ts
index 37ebe6ff..76f98392 100644
--- a/test/utils/helpers/dateIntervals.test.ts
+++ b/test/utils/helpers/dateIntervals.test.ts
@@ -8,11 +8,10 @@ import {
rangeOrIntervalToString,
toDateRange,
} from '../../../src/utils/helpers/dateIntervals';
-import { parseDate } from '../../../src/utils/helpers/date';
+import { parseDate, now } from '../../../src/utils/helpers/date';
describe('date-types', () => {
- const now = () => new Date();
- const daysBack = (days: number) => subDays(new Date(), days);
+ const daysBack = (days: number) => subDays(now(), days);
describe('dateRangeIsEmpty', () => {
it.each([
@@ -26,9 +25,9 @@ describe('date-types', () => {
[{ startDate: undefined, endDate: undefined }, true],
[{ startDate: undefined, endDate: null }, true],
[{ startDate: null, endDate: undefined }, true],
- [{ startDate: new Date() }, false],
- [{ endDate: new Date() }, false],
- [{ startDate: new Date(), endDate: new Date() }, false],
+ [{ startDate: now() }, false],
+ [{ endDate: now() }, false],
+ [{ startDate: now(), endDate: now() }, false],
])('returns proper result', (dateRange, expectedResult) => {
expect(dateRangeIsEmpty(dateRange)).toEqual(expectedResult);
});
diff --git a/test/visits/reducers/domainVisits.test.ts b/test/visits/reducers/domainVisits.test.ts
index db52ba06..b7024e0d 100644
--- a/test/visits/reducers/domainVisits.test.ts
+++ b/test/visits/reducers/domainVisits.test.ts
@@ -197,12 +197,12 @@ describe('domainVisitsReducer', () => {
it.each([
[
- [Mock.of({ date: formatISO(subDays(new Date(), 20)) })],
+ [Mock.of({ date: formatISO(subDays(now, 20)) })],
{ type: fallbackToIntervalAction.toString(), payload: 'last30Days' },
3,
],
[
- [Mock.of({ date: formatISO(subDays(new Date(), 100)) })],
+ [Mock.of({ date: formatISO(subDays(now, 100)) })],
{ type: fallbackToIntervalAction.toString(), payload: 'last180Days' },
3,
],
diff --git a/test/visits/reducers/nonOrphanVisits.test.ts b/test/visits/reducers/nonOrphanVisits.test.ts
index e62a6d10..89dc2671 100644
--- a/test/visits/reducers/nonOrphanVisits.test.ts
+++ b/test/visits/reducers/nonOrphanVisits.test.ts
@@ -177,12 +177,12 @@ describe('nonOrphanVisitsReducer', () => {
it.each([
[
- [Mock.of({ date: formatISO(subDays(new Date(), 5)) })],
+ [Mock.of({ date: formatISO(subDays(now, 5)) })],
{ type: fallbackToIntervalAction.toString(), payload: 'last7Days' },
3,
],
[
- [Mock.of({ date: formatISO(subDays(new Date(), 200)) })],
+ [Mock.of({ date: formatISO(subDays(now, 200)) })],
{ type: fallbackToIntervalAction.toString(), payload: 'last365Days' },
3,
],
diff --git a/test/visits/reducers/orphanVisits.test.ts b/test/visits/reducers/orphanVisits.test.ts
index 0b728929..083b7fc4 100644
--- a/test/visits/reducers/orphanVisits.test.ts
+++ b/test/visits/reducers/orphanVisits.test.ts
@@ -175,12 +175,12 @@ describe('orphanVisitsReducer', () => {
it.each([
[
- [Mock.of({ date: formatISO(subDays(new Date(), 5)) })],
+ [Mock.of({ date: formatISO(subDays(now, 5)) })],
{ type: fallbackToIntervalAction.toString(), payload: 'last7Days' },
3,
],
[
- [Mock.of({ date: formatISO(subDays(new Date(), 200)) })],
+ [Mock.of({ date: formatISO(subDays(now, 200)) })],
{ type: fallbackToIntervalAction.toString(), payload: 'last365Days' },
3,
],
diff --git a/test/visits/reducers/shortUrlVisits.test.ts b/test/visits/reducers/shortUrlVisits.test.ts
index 4b65d351..6a2b7452 100644
--- a/test/visits/reducers/shortUrlVisits.test.ts
+++ b/test/visits/reducers/shortUrlVisits.test.ts
@@ -219,12 +219,12 @@ describe('shortUrlVisitsReducer', () => {
it.each([
[
- [Mock.of({ date: formatISO(subDays(new Date(), 5)) })],
+ [Mock.of({ date: formatISO(subDays(now, 5)) })],
{ type: fallbackToIntervalAction.toString(), payload: 'last7Days' },
3,
],
[
- [Mock.of({ date: formatISO(subDays(new Date(), 200)) })],
+ [Mock.of({ date: formatISO(subDays(now, 200)) })],
{ type: fallbackToIntervalAction.toString(), payload: 'last365Days' },
3,
],
diff --git a/test/visits/reducers/tagVisits.test.ts b/test/visits/reducers/tagVisits.test.ts
index fc94e980..c96eca44 100644
--- a/test/visits/reducers/tagVisits.test.ts
+++ b/test/visits/reducers/tagVisits.test.ts
@@ -193,12 +193,12 @@ describe('tagVisitsReducer', () => {
it.each([
[
- [Mock.of({ date: formatISO(subDays(new Date(), 20)) })],
+ [Mock.of({ date: formatISO(subDays(now, 20)) })],
{ type: fallbackToIntervalAction.toString(), payload: 'last30Days' },
3,
],
[
- [Mock.of({ date: formatISO(subDays(new Date(), 100)) })],
+ [Mock.of({ date: formatISO(subDays(now, 100)) })],
{ type: fallbackToIntervalAction.toString(), payload: 'last180Days' },
3,
],