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, ],