From d3f9650e82a607b04d017153fa0890deed8b706e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 6 Mar 2021 10:56:49 +0100 Subject: [PATCH 1/6] Added new visits settings --- src/settings/reducers/settings.ts | 9 +++++++++ src/visits/OrphanVisits.tsx | 4 ++++ src/visits/ShortUrlVisits.tsx | 4 ++++ src/visits/TagVisits.tsx | 11 ++++++++++- src/visits/VisitsStats.tsx | 8 ++++++-- src/visits/services/provideServices.ts | 6 +++--- test/settings/reducers/settings.test.ts | 3 ++- test/visits/VisitsStats.test.tsx | 2 ++ 8 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/settings/reducers/settings.ts b/src/settings/reducers/settings.ts index e5c0d1f9..6e3e1e8c 100644 --- a/src/settings/reducers/settings.ts +++ b/src/settings/reducers/settings.ts @@ -3,6 +3,7 @@ import { dissoc, mergeDeepRight } from 'ramda'; import { buildReducer } from '../../utils/helpers/redux'; import { RecursivePartial } from '../../utils/utils'; import { Theme } from '../../utils/theme'; +import { DateInterval } from '../../utils/dates/types'; export const SET_SETTINGS = 'shlink/realTimeUpdates/SET_SETTINGS'; @@ -24,10 +25,15 @@ export interface UiSettings { theme: Theme; } +export interface VisitsSettings { + defaultInterval: DateInterval; +} + export interface Settings { realTimeUpdates: RealTimeUpdatesSettings; shortUrlCreation?: ShortUrlCreationSettings; ui?: UiSettings; + visits?: VisitsSettings; } const initialState: Settings = { @@ -40,6 +46,9 @@ const initialState: Settings = { ui: { theme: 'light', }, + visits: { + defaultInterval: 'last30Days', + }, }; type SettingsAction = Action & Settings; diff --git a/src/visits/OrphanVisits.tsx b/src/visits/OrphanVisits.tsx index cb959869..04d421eb 100644 --- a/src/visits/OrphanVisits.tsx +++ b/src/visits/OrphanVisits.tsx @@ -2,6 +2,7 @@ import { RouteComponentProps } from 'react-router'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { ShlinkVisitsParams } from '../api/types'; import { Topics } from '../mercure/helpers/Topics'; +import { Settings } from '../settings/reducers/settings'; import VisitsStats from './VisitsStats'; import { OrphanVisitsHeader } from './OrphanVisitsHeader'; import { VisitsInfo } from './types'; @@ -10,6 +11,7 @@ export interface OrphanVisitsProps extends RouteComponentProps { getOrphanVisits: (params: ShlinkVisitsParams) => void; orphanVisits: VisitsInfo; cancelGetOrphanVisits: () => void; + settings: Settings; } export const OrphanVisits = boundToMercureHub(({ @@ -18,12 +20,14 @@ export const OrphanVisits = boundToMercureHub(({ getOrphanVisits, orphanVisits, cancelGetOrphanVisits, + settings, }: OrphanVisitsProps) => ( diff --git a/src/visits/ShortUrlVisits.tsx b/src/visits/ShortUrlVisits.tsx index 509e34ae..d4894b0a 100644 --- a/src/visits/ShortUrlVisits.tsx +++ b/src/visits/ShortUrlVisits.tsx @@ -5,6 +5,7 @@ import { ShlinkVisitsParams } from '../api/types'; import { parseQuery } from '../utils/helpers/query'; import { Topics } from '../mercure/helpers/Topics'; import { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail'; +import { Settings } from '../settings/reducers/settings'; import { ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits'; import ShortUrlVisitsHeader from './ShortUrlVisitsHeader'; import VisitsStats from './VisitsStats'; @@ -15,6 +16,7 @@ export interface ShortUrlVisitsProps extends RouteComponentProps<{ shortCode: st getShortUrlDetail: Function; shortUrlDetail: ShortUrlDetail; cancelGetShortUrlVisits: () => void; + settings: Settings; } const ShortUrlVisits = boundToMercureHub(({ @@ -26,6 +28,7 @@ const ShortUrlVisits = boundToMercureHub(({ getShortUrlVisits, getShortUrlDetail, cancelGetShortUrlVisits, + settings, }: ShortUrlVisitsProps) => { const { shortCode } = params; const { domain } = parseQuery<{ domain?: string }>(search); @@ -42,6 +45,7 @@ const ShortUrlVisits = boundToMercureHub(({ visitsInfo={shortUrlVisits} baseUrl={url} domain={domain} + settings={settings} > diff --git a/src/visits/TagVisits.tsx b/src/visits/TagVisits.tsx index d6772682..0f687382 100644 --- a/src/visits/TagVisits.tsx +++ b/src/visits/TagVisits.tsx @@ -3,6 +3,7 @@ import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import ColorGenerator from '../utils/services/ColorGenerator'; import { ShlinkVisitsParams } from '../api/types'; import { Topics } from '../mercure/helpers/Topics'; +import { Settings } from '../settings/reducers/settings'; import { TagVisits as TagVisitsState } from './reducers/tagVisits'; import TagVisitsHeader from './TagVisitsHeader'; import VisitsStats from './VisitsStats'; @@ -11,6 +12,7 @@ export interface TagVisitsProps extends RouteComponentProps<{ tag: string }> { getTagVisits: (tag: string, query: any) => void; tagVisits: TagVisitsState; cancelGetTagVisits: () => void; + settings: Settings; } const TagVisits = (colorGenerator: ColorGenerator) => boundToMercureHub(({ @@ -19,12 +21,19 @@ const TagVisits = (colorGenerator: ColorGenerator) => boundToMercureHub(({ getTagVisits, tagVisits, cancelGetTagVisits, + settings, }: TagVisitsProps) => { const { tag } = params; const loadVisits = (params: ShlinkVisitsParams) => getTagVisits(tag, params); return ( - + ); diff --git a/src/visits/VisitsStats.tsx b/src/visits/VisitsStats.tsx index 0d391717..c8d80918 100644 --- a/src/visits/VisitsStats.tsx +++ b/src/visits/VisitsStats.tsx @@ -13,6 +13,7 @@ import { ShlinkVisitsParams } from '../api/types'; import { DateInterval, DateRange, intervalToDateRange } from '../utils/dates/types'; import { Result } from '../utils/Result'; import { ShlinkApiError } from '../api/ShlinkApiError'; +import { Settings } from '../settings/reducers/settings'; import SortableBarGraph from './helpers/SortableBarGraph'; import GraphCard from './helpers/GraphCard'; import LineChartCard from './helpers/LineChartCard'; @@ -25,6 +26,7 @@ import './VisitsStats.scss'; export interface VisitsStatsProps { getVisits: (params: Partial) => void; visitsInfo: VisitsInfo; + settings: Settings; cancelGetVisits: () => void; baseUrl: string; domain?: string; @@ -59,7 +61,6 @@ const highlightedVisitsToStats = ( return acc; }, {}); let selectedBar: string | undefined; -const initialInterval: DateInterval = 'last30Days'; const VisitsNavLink: FC = ({ subPath, title, icon, to }) => ( = ({ subPath, title ); -const VisitsStats: FC = ({ children, visitsInfo, getVisits, cancelGetVisits, baseUrl, domain }) => { +const VisitsStats: FC = ( + { children, visitsInfo, getVisits, cancelGetVisits, baseUrl, domain, settings }, +) => { + const initialInterval: DateInterval = settings.visits?.defaultInterval ?? 'last30Days'; const [ dateRange, setDateRange ] = useState(intervalToDateRange(initialInterval)); const [ highlightedVisits, setHighlightedVisits ] = useState([]); const [ highlightedLabel, setHighlightedLabel ] = useState(); diff --git a/src/visits/services/provideServices.ts b/src/visits/services/provideServices.ts index 5e7775f5..3ebb8c08 100644 --- a/src/visits/services/provideServices.ts +++ b/src/visits/services/provideServices.ts @@ -18,19 +18,19 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('ShortUrlVisits', () => ShortUrlVisits); bottle.decorator('ShortUrlVisits', connect( - [ 'shortUrlVisits', 'shortUrlDetail', 'mercureInfo' ], + [ 'shortUrlVisits', 'shortUrlDetail', 'mercureInfo', 'settings' ], [ 'getShortUrlVisits', 'getShortUrlDetail', 'cancelGetShortUrlVisits', 'createNewVisits', 'loadMercureInfo' ], )); bottle.serviceFactory('TagVisits', TagVisits, 'ColorGenerator'); bottle.decorator('TagVisits', connect( - [ 'tagVisits', 'mercureInfo' ], + [ 'tagVisits', 'mercureInfo', 'settings' ], [ 'getTagVisits', 'cancelGetTagVisits', 'createNewVisits', 'loadMercureInfo' ], )); bottle.serviceFactory('OrphanVisits', () => OrphanVisits); bottle.decorator('OrphanVisits', connect( - [ 'orphanVisits', 'mercureInfo' ], + [ 'orphanVisits', 'mercureInfo', 'settings' ], [ 'getOrphanVisits', 'cancelGetOrphanVisits', 'createNewVisits', 'loadMercureInfo' ], )); diff --git a/test/settings/reducers/settings.test.ts b/test/settings/reducers/settings.test.ts index 57d22066..8dd423ae 100644 --- a/test/settings/reducers/settings.test.ts +++ b/test/settings/reducers/settings.test.ts @@ -10,7 +10,8 @@ describe('settingsReducer', () => { const realTimeUpdates = { enabled: true }; const shortUrlCreation = { validateUrls: false }; const ui = { theme: 'light' }; - const settings = { realTimeUpdates, shortUrlCreation, ui }; + const visits = { defaultInterval: 'last30Days' }; + const settings = { realTimeUpdates, shortUrlCreation, ui, visits }; describe('reducer', () => { it('returns realTimeUpdates when action is SET_SETTINGS', () => { diff --git a/test/visits/VisitsStats.test.tsx b/test/visits/VisitsStats.test.tsx index d878d2e8..d66255cb 100644 --- a/test/visits/VisitsStats.test.tsx +++ b/test/visits/VisitsStats.test.tsx @@ -9,6 +9,7 @@ import { Visit, VisitsInfo } from '../../src/visits/types'; import LineChartCard from '../../src/visits/helpers/LineChartCard'; import VisitsTable from '../../src/visits/VisitsTable'; import { Result } from '../../src/utils/Result'; +import { Settings } from '../../src/settings/reducers/settings'; describe('', () => { const visits = [ Mock.all(), Mock.all(), Mock.all() ]; @@ -23,6 +24,7 @@ describe('', () => { visitsInfo={Mock.of(visitsInfo)} cancelGetVisits={() => {}} baseUrl={''} + settings={Mock.all()} />, ); From fee62484b591bdbf126ee969fd8629b4157cec50 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 6 Mar 2021 16:54:43 +0100 Subject: [PATCH 2/6] Created section to set default date interval for visits --- src/settings/Settings.tsx | 37 ++++++++------ src/settings/Visits.tsx | 22 +++++++++ src/settings/reducers/settings.ts | 5 ++ src/settings/services/provideServices.ts | 8 ++- src/utils/dates/DateIntervalDropdownItems.tsx | 20 ++++++++ src/utils/dates/DateIntervalSelector.tsx | 10 ++++ src/utils/dates/DateRangeSelector.tsx | 9 +--- src/utils/dates/types/index.ts | 2 + test/settings/reducers/settings.test.ts | 9 ++++ .../dates/DateIntervalDropdownItems.test.tsx | 39 +++++++++++++++ test/utils/dates/DateRangeSelector.test.tsx | 49 ++++++++++++------- test/visits/OrphanVisits.test.tsx | 2 + 12 files changed, 170 insertions(+), 42 deletions(-) create mode 100644 src/settings/Visits.tsx create mode 100644 src/utils/dates/DateIntervalDropdownItems.tsx create mode 100644 src/utils/dates/DateIntervalSelector.tsx create mode 100644 test/utils/dates/DateIntervalDropdownItems.test.tsx diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 8e6731f5..8305442e 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -1,22 +1,29 @@ -import { FC } from 'react'; +import { FC, ReactNode } from 'react'; import { Row } from 'reactstrap'; import NoMenuLayout from '../common/NoMenuLayout'; -const Settings = (RealTimeUpdates: FC, ShortUrlCreation: FC, UserInterface: FC) => () => ( +const SettingsSections: FC<{ items: ReactNode[][] }> = ({ items }) => ( + <> + {items.map((child, index) => ( + + {child.map((subChild, subIndex) => ( +
+
{subChild}
+
+ ))} +
+ ))} + +); + +const Settings = (RealTimeUpdates: FC, ShortUrlCreation: FC, UserInterface: FC, Visits: FC) => () => ( - -
-
- -
-
- -
-
-
- -
-
+ , ], // eslint-disable-line react/jsx-key + [ , ], // eslint-disable-line react/jsx-key + ]} + />
); diff --git a/src/settings/Visits.tsx b/src/settings/Visits.tsx new file mode 100644 index 00000000..baca1fd0 --- /dev/null +++ b/src/settings/Visits.tsx @@ -0,0 +1,22 @@ +import { FormGroup } from 'reactstrap'; +import { FC } from 'react'; +import { SimpleCard } from '../utils/SimpleCard'; +import { DateIntervalSelector } from '../utils/dates/DateIntervalSelector'; +import { Settings, VisitsSettings } from './reducers/settings'; + +interface VisitsProps { + settings: Settings; + setVisitsSettings: (settings: VisitsSettings) => void; +} + +export const Visits: FC = ({ settings, setVisitsSettings }) => ( + + + + setVisitsSettings({ defaultInterval })} + /> + + +); diff --git a/src/settings/reducers/settings.ts b/src/settings/reducers/settings.ts index 6e3e1e8c..0ba6f106 100644 --- a/src/settings/reducers/settings.ts +++ b/src/settings/reducers/settings.ts @@ -78,3 +78,8 @@ export const setUiSettings = (settings: UiSettings): PartialSettingsAction => ({ type: SET_SETTINGS, ui: settings, }); + +export const setVisitsSettings = (settings: VisitsSettings): PartialSettingsAction => ({ + type: SET_SETTINGS, + visits: settings, +}); diff --git a/src/settings/services/provideServices.ts b/src/settings/services/provideServices.ts index cd01599b..52652154 100644 --- a/src/settings/services/provideServices.ts +++ b/src/settings/services/provideServices.ts @@ -5,16 +5,18 @@ import { setRealTimeUpdatesInterval, setShortUrlCreationSettings, setUiSettings, + setVisitsSettings, toggleRealTimeUpdates, } from '../reducers/settings'; import { ConnectDecorator } from '../../container/types'; import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServer'; import { ShortUrlCreation } from '../ShortUrlCreation'; import { UserInterface } from '../UserInterface'; +import { Visits } from '../Visits'; const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { // Components - bottle.serviceFactory('Settings', Settings, 'RealTimeUpdates', 'ShortUrlCreation', 'UserInterface'); + bottle.serviceFactory('Settings', Settings, 'RealTimeUpdates', 'ShortUrlCreation', 'UserInterface', 'Visits'); bottle.decorator('Settings', withoutSelectedServer); bottle.decorator('Settings', connect(null, [ 'resetSelectedServer' ])); @@ -30,11 +32,15 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('UserInterface', () => UserInterface); bottle.decorator('UserInterface', connect([ 'settings' ], [ 'setUiSettings' ])); + bottle.serviceFactory('Visits', () => Visits); + bottle.decorator('Visits', connect([ 'settings' ], [ 'setVisitsSettings' ])); + // Actions bottle.serviceFactory('toggleRealTimeUpdates', () => toggleRealTimeUpdates); bottle.serviceFactory('setRealTimeUpdatesInterval', () => setRealTimeUpdatesInterval); bottle.serviceFactory('setShortUrlCreationSettings', () => setShortUrlCreationSettings); bottle.serviceFactory('setUiSettings', () => setUiSettings); + bottle.serviceFactory('setVisitsSettings', () => setVisitsSettings); }; export default provideServices; diff --git a/src/utils/dates/DateIntervalDropdownItems.tsx b/src/utils/dates/DateIntervalDropdownItems.tsx new file mode 100644 index 00000000..b1ea2208 --- /dev/null +++ b/src/utils/dates/DateIntervalDropdownItems.tsx @@ -0,0 +1,20 @@ +import { DropdownItem } from 'reactstrap'; +import { FC } from 'react'; +import { DATE_INTERVALS, DateInterval, rangeOrIntervalToString } from './types'; + +export interface DateIntervalDropdownProps { + active?: DateInterval; + onChange: (interval: DateInterval) => void; +} + +export const DateIntervalDropdownItems: FC = ({ active, onChange }) => ( + <> + {DATE_INTERVALS.map( + (interval) => ( + onChange(interval)}> + {rangeOrIntervalToString(interval)} + + ), + )} + +); diff --git a/src/utils/dates/DateIntervalSelector.tsx b/src/utils/dates/DateIntervalSelector.tsx new file mode 100644 index 00000000..59d741db --- /dev/null +++ b/src/utils/dates/DateIntervalSelector.tsx @@ -0,0 +1,10 @@ +import { FC } from 'react'; +import { DropdownBtn } from '../DropdownBtn'; +import { rangeOrIntervalToString } from './types'; +import { DateIntervalDropdownItems, DateIntervalDropdownProps } from './DateIntervalDropdownItems'; + +export const DateIntervalSelector: FC = ({ onChange, active }) => ( + + + +); diff --git a/src/utils/dates/DateRangeSelector.tsx b/src/utils/dates/DateRangeSelector.tsx index 1b680f90..4b6212b8 100644 --- a/src/utils/dates/DateRangeSelector.tsx +++ b/src/utils/dates/DateRangeSelector.tsx @@ -10,6 +10,7 @@ import { rangeIsInterval, } from './types'; import DateRangeRow from './DateRangeRow'; +import { DateIntervalDropdownItems } from './DateIntervalDropdownItems'; export interface DateRangeSelectorProps { initialDateRange?: DateInterval | DateRange; @@ -47,13 +48,7 @@ export const DateRangeSelector = ( {defaultText} - {([ 'today', 'yesterday', 'last7Days', 'last30Days', 'last90Days', 'last180days', 'last365Days' ] as DateInterval[]).map( - (interval) => ( - - {rangeOrIntervalToString(interval)} - - ), - )} + updateInterval(interval)()} /> Custom: diff --git a/src/utils/dates/types/index.ts b/src/utils/dates/types/index.ts index aeb3c93a..07e5e6db 100644 --- a/src/utils/dates/types/index.ts +++ b/src/utils/dates/types/index.ts @@ -24,6 +24,8 @@ const INTERVAL_TO_STRING_MAP: Record = { last365Days: 'Last 365 days', }; +export const DATE_INTERVALS: DateInterval[] = Object.keys(INTERVAL_TO_STRING_MAP) as DateInterval[]; + const dateRangeToString = (range?: DateRange): string | undefined => { if (!range || dateRangeIsEmpty(range)) { return undefined; diff --git a/test/settings/reducers/settings.test.ts b/test/settings/reducers/settings.test.ts index 8dd423ae..9699f57a 100644 --- a/test/settings/reducers/settings.test.ts +++ b/test/settings/reducers/settings.test.ts @@ -4,6 +4,7 @@ import reducer, { setRealTimeUpdatesInterval, setShortUrlCreationSettings, setUiSettings, + setVisitsSettings, } from '../../../src/settings/reducers/settings'; describe('settingsReducer', () => { @@ -50,4 +51,12 @@ describe('settingsReducer', () => { expect(result).toEqual({ type: SET_SETTINGS, ui: { theme: 'dark' } }); }); }); + + describe('setVisitsSettings', () => { + it('creates action to set visits settings', () => { + const result = setVisitsSettings({ defaultInterval: 'last180days' }); + + expect(result).toEqual({ type: SET_SETTINGS, visits: { defaultInterval: 'last180days' } }); + }); + }); }); diff --git a/test/utils/dates/DateIntervalDropdownItems.test.tsx b/test/utils/dates/DateIntervalDropdownItems.test.tsx new file mode 100644 index 00000000..f9d924ba --- /dev/null +++ b/test/utils/dates/DateIntervalDropdownItems.test.tsx @@ -0,0 +1,39 @@ +import { DropdownItem } from 'reactstrap'; +import { shallow, ShallowWrapper } from 'enzyme'; +import { DateIntervalDropdownItems } from '../../../src/utils/dates/DateIntervalDropdownItems'; +import { DATE_INTERVALS } from '../../../src/utils/dates/types'; + +describe('', () => { + let wrapper: ShallowWrapper; + const onChange = jest.fn(); + + beforeEach(() => { + wrapper = shallow(); + }); + + afterEach(jest.clearAllMocks); + afterEach(() => wrapper?.unmount()); + + test('expected amount of items is rendered', () => { + const items = wrapper.find(DropdownItem); + + expect(items).toHaveLength(DATE_INTERVALS.length); + }); + + test('expected item is active', () => { + const items = wrapper.find(DropdownItem); + const EXPECTED_ACTIVE_INDEX = 5; + + expect.assertions(DATE_INTERVALS.length); + items.forEach((item, index) => expect(item.prop('active')).toEqual(index === EXPECTED_ACTIVE_INDEX)); + }); + + test('selecting an element triggers onChange callback', () => { + const items = wrapper.find(DropdownItem); + + items.at(2).simulate('click'); + items.at(4).simulate('click'); + items.at(1).simulate('click'); + expect(onChange).toHaveBeenCalledTimes(3); + }); +}); diff --git a/test/utils/dates/DateRangeSelector.test.tsx b/test/utils/dates/DateRangeSelector.test.tsx index 78a375c7..35e8ab1a 100644 --- a/test/utils/dates/DateRangeSelector.test.tsx +++ b/test/utils/dates/DateRangeSelector.test.tsx @@ -4,6 +4,7 @@ import moment from 'moment'; import { Mock } from 'ts-mockery'; import { DateRangeSelector, DateRangeSelectorProps } from '../../../src/utils/dates/DateRangeSelector'; import { DateInterval } from '../../../src/utils/dates/types'; +import { DateIntervalDropdownItems } from '../../../src/utils/dates/DateIntervalDropdownItems'; describe('', () => { let wrapper: ShallowWrapper; @@ -20,39 +21,49 @@ describe('', () => { test('proper amount of items is rendered', () => { const wrapper = createWrapper(); const items = wrapper.find(DropdownItem); + const dateIntervalItems = wrapper.find(DateIntervalDropdownItems); - expect(items).toHaveLength(12); + expect(items).toHaveLength(5); + expect(dateIntervalItems).toHaveLength(1); expect(items.filter('[divider]')).toHaveLength(2); expect(items.filter('[header]')).toHaveLength(1); expect(items.filter('[text]')).toHaveLength(1); - expect(items.filter('[active]')).toHaveLength(8); + expect(items.filter('[active]')).toHaveLength(1); }); test.each([ - [ undefined, 0 ], - [ 'today' as DateInterval, 1 ], - [ 'yesterday' as DateInterval, 2 ], - [ 'last7Days' as DateInterval, 3 ], - [ 'last30Days' as DateInterval, 4 ], - [ 'last90Days' as DateInterval, 5 ], - [ 'last180days' as DateInterval, 6 ], - [ 'last365Days' as DateInterval, 7 ], - [{ startDate: moment() }, 8 ], - ])('proper element is active based on provided date range', (initialDateRange, expectedActiveIndex) => { + [ undefined, 1, 0 ], + [ 'today' as DateInterval, 0, 1 ], + [ 'yesterday' as DateInterval, 0, 1 ], + [ 'last7Days' as DateInterval, 0, 1 ], + [ 'last30Days' as DateInterval, 0, 1 ], + [ 'last90Days' as DateInterval, 0, 1 ], + [ 'last180days' as DateInterval, 0, 1 ], + [ 'last365Days' as DateInterval, 0, 1 ], + [{ startDate: moment() }, 0, 0 ], + ])('proper element is active based on provided date range', ( + initialDateRange, + expectedActiveItems, + expectedActiveIntervalItems, + ) => { const wrapper = createWrapper({ initialDateRange }); - const items = wrapper.find(DropdownItem).filter('[active]'); + const items = wrapper.find(DropdownItem).filterWhere((item) => item.prop('active') === true); + const dateIntervalItems = wrapper.find(DateIntervalDropdownItems).filterWhere( + (item) => item.prop('active') !== undefined, + ); - expect.assertions(8); - items.forEach((item, index) => expect(item.prop('active')).toEqual(index === expectedActiveIndex)); + expect(items).toHaveLength(expectedActiveItems); + expect(dateIntervalItems).toHaveLength(expectedActiveIntervalItems); }); test('selecting an element triggers onDatesChange callback', () => { const wrapper = createWrapper(); - const items = wrapper.find(DropdownItem).filter('[active]'); + const item = wrapper.find(DropdownItem).at(0); + const dateIntervalItems = wrapper.find(DateIntervalDropdownItems); - items.at(2).simulate('click'); - items.at(4).simulate('click'); - items.at(1).simulate('click'); + item.simulate('click'); + item.simulate('click'); + dateIntervalItems.simulate('change'); expect(onDatesChange).toHaveBeenCalledTimes(3); }); }); diff --git a/test/visits/OrphanVisits.test.tsx b/test/visits/OrphanVisits.test.tsx index be362905..ad953bbc 100644 --- a/test/visits/OrphanVisits.test.tsx +++ b/test/visits/OrphanVisits.test.tsx @@ -7,6 +7,7 @@ import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub'; import { VisitsInfo } from '../../src/visits/types'; import VisitsStats from '../../src/visits/VisitsStats'; import { OrphanVisitsHeader } from '../../src/visits/OrphanVisitsHeader'; +import { Settings } from '../../src/settings/reducers/settings'; describe('', () => { it('wraps visits stats and header', () => { @@ -24,6 +25,7 @@ describe('', () => { history={Mock.of({ goBack })} location={Mock.all()} match={Mock.of({ url: 'the_base_url' })} + settings={Mock.all()} />, ).dive(); const stats = wrapper.find(VisitsStats); From 426d000a59fae461b52471096032067e18cf11b6 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 6 Mar 2021 17:21:23 +0100 Subject: [PATCH 3/6] Added tests for new visits settings --- test/settings/Visits.test.tsx | 66 +++++++++++++++++++ .../utils/dates/DateIntervalSelector.test.tsx | 26 ++++++++ 2 files changed, 92 insertions(+) create mode 100644 test/settings/Visits.test.tsx create mode 100644 test/utils/dates/DateIntervalSelector.test.tsx diff --git a/test/settings/Visits.test.tsx b/test/settings/Visits.test.tsx new file mode 100644 index 00000000..cfbaf833 --- /dev/null +++ b/test/settings/Visits.test.tsx @@ -0,0 +1,66 @@ +import { shallow, ShallowWrapper } from 'enzyme'; +import { Mock } from 'ts-mockery'; +import { Settings } from '../../src/settings/reducers/settings'; +import { Visits } from '../../src/settings/Visits'; +import { SimpleCard } from '../../src/utils/SimpleCard'; +import { DateIntervalSelector } from '../../src/utils/dates/DateIntervalSelector'; + +describe('', () => { + let wrapper: ShallowWrapper; + const setVisitsSettings = jest.fn(); + const createWrapper = (settings: Partial = {}) => { + wrapper = shallow((settings)} setVisitsSettings={setVisitsSettings} />); + + return wrapper; + }; + + afterEach(jest.clearAllMocks); + afterEach(() => wrapper?.unmount()); + + it('renders expected components', () => { + const wrapper = createWrapper(); + + expect(wrapper.find(SimpleCard).prop('title')).toEqual('Visits'); + expect(wrapper.find('label').prop('children')).toEqual('Default interval to load on visits sections:'); + expect(wrapper.find(DateIntervalSelector)).toHaveLength(1); + }); + + it.each([ + [ Mock.all(), 'last30Days' ], + [ Mock.of({ visits: {} }), 'last30Days' ], + [ + Mock.of({ + visits: { + defaultInterval: 'last7Days', + }, + }), + 'last7Days', + ], + [ + Mock.of({ + visits: { + defaultInterval: 'today', + }, + }), + 'today', + ], + ])('sets expected interval as active', (settings, expectedInterval) => { + const wrapper = createWrapper(settings); + + expect(wrapper.find(DateIntervalSelector).prop('active')).toEqual(expectedInterval); + }); + + it('invokes setVisitsSettings when interval changes', () => { + const wrapper = createWrapper(); + const selector = wrapper.find(DateIntervalSelector); + + selector.simulate('change', 'last7Days'); + selector.simulate('change', 'last180days'); + selector.simulate('change', 'yesterday'); + + expect(setVisitsSettings).toHaveBeenCalledTimes(3); + expect(setVisitsSettings).toHaveBeenNthCalledWith(1, { defaultInterval: 'last7Days' }); + expect(setVisitsSettings).toHaveBeenNthCalledWith(2, { defaultInterval: 'last180days' }); + expect(setVisitsSettings).toHaveBeenNthCalledWith(3, { defaultInterval: 'yesterday' }); + }); +}); diff --git a/test/utils/dates/DateIntervalSelector.test.tsx b/test/utils/dates/DateIntervalSelector.test.tsx new file mode 100644 index 00000000..51a24f88 --- /dev/null +++ b/test/utils/dates/DateIntervalSelector.test.tsx @@ -0,0 +1,26 @@ +import { shallow, ShallowWrapper } from 'enzyme'; +import { DateInterval, rangeOrIntervalToString } from '../../../src/utils/dates/types'; +import { DateIntervalSelector } from '../../../src/utils/dates/DateIntervalSelector'; +import { DateIntervalDropdownItems } from '../../../src/utils/dates/DateIntervalDropdownItems'; +import { DropdownBtn } from '../../../src/utils/DropdownBtn'; + +describe('', () => { + let wrapper: ShallowWrapper; + const activeInterval: DateInterval = 'last7Days'; + const onChange = jest.fn(); + + beforeEach(() => { + wrapper = shallow(); + }); + afterEach(() => wrapper?.unmount()); + + test('props are passed down to nested DateIntervalDropdownItems', () => { + const items = wrapper.find(DateIntervalDropdownItems); + const dropdown = wrapper.find(DropdownBtn); + + expect(dropdown.prop('text')).toEqual(rangeOrIntervalToString(activeInterval)); + expect(items).toHaveLength(1); + expect(items.prop('onChange')).toEqual(onChange); + expect(items.prop('active')).toEqual(activeInterval); + }); +}); From e380ddb40f21f6e774e7a51d970184c525e754b2 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 6 Mar 2021 17:25:09 +0100 Subject: [PATCH 4/6] Replaced test by it in tests --- test/servers/Overview.test.tsx | 10 +++++----- test/utils/CopyToClipboardIcon.test.tsx | 2 +- test/utils/dates/DateIntervalDropdownItems.test.tsx | 6 +++--- test/utils/dates/DateIntervalSelector.test.tsx | 2 +- test/utils/dates/DateRangeSelector.test.tsx | 8 ++++---- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/test/servers/Overview.test.tsx b/test/servers/Overview.test.tsx index 7827b637..3b29b3e4 100644 --- a/test/servers/Overview.test.tsx +++ b/test/servers/Overview.test.tsx @@ -45,7 +45,7 @@ describe('', () => { afterEach(() => wrapper?.unmount()); - test('cards display loading messages when still loading', () => { + it('displays loading messages when still loading', () => { const wrapper = createWrapper(true); const cards = wrapper.find(CardText); @@ -53,7 +53,7 @@ describe('', () => { cards.forEach((card) => expect(card.html()).toContain('Loading...')); }); - test('amounts are displayed in cards after finishing loading', () => { + it('displays amounts in cards after finishing loading', () => { const wrapper = createWrapper(); const cards = wrapper.find(CardText); @@ -64,21 +64,21 @@ describe('', () => { expect(cards.at(3).html()).toContain(prettify(3)); }); - test('first card displays warning for old shlink versions', () => { + it('displays warning in first card for old shlink versions', () => { const wrapper = createWrapper(); const firstCard = wrapper.find(CardText).first(); expect(firstCard.html()).toContain('Shlink 2.2 is needed'); }); - test('nests complex components', () => { + it('nests complex components', () => { const wrapper = createWrapper(); expect(wrapper.find(CreateShortUrl)).toHaveLength(1); expect(wrapper.find(ShortUrlsTable)).toHaveLength(1); }); - test('links to other sections are displayed', () => { + it('displays links to other sections', () => { const wrapper = createWrapper(); const links = wrapper.find(Link); diff --git a/test/utils/CopyToClipboardIcon.test.tsx b/test/utils/CopyToClipboardIcon.test.tsx index 9f0d709b..86d7a924 100644 --- a/test/utils/CopyToClipboardIcon.test.tsx +++ b/test/utils/CopyToClipboardIcon.test.tsx @@ -13,7 +13,7 @@ describe('', () => { }); afterEach(() => wrapper?.unmount()); - test('expected components are wrapped', () => { + it('wraps expected components', () => { const copyToClipboard = wrapper.find(CopyToClipboard); const icon = wrapper.find(FontAwesomeIcon); diff --git a/test/utils/dates/DateIntervalDropdownItems.test.tsx b/test/utils/dates/DateIntervalDropdownItems.test.tsx index f9d924ba..2d15ebd8 100644 --- a/test/utils/dates/DateIntervalDropdownItems.test.tsx +++ b/test/utils/dates/DateIntervalDropdownItems.test.tsx @@ -14,13 +14,13 @@ describe('', () => { afterEach(jest.clearAllMocks); afterEach(() => wrapper?.unmount()); - test('expected amount of items is rendered', () => { + it('renders expected amount of items', () => { const items = wrapper.find(DropdownItem); expect(items).toHaveLength(DATE_INTERVALS.length); }); - test('expected item is active', () => { + it('sets expected item as active', () => { const items = wrapper.find(DropdownItem); const EXPECTED_ACTIVE_INDEX = 5; @@ -28,7 +28,7 @@ describe('', () => { items.forEach((item, index) => expect(item.prop('active')).toEqual(index === EXPECTED_ACTIVE_INDEX)); }); - test('selecting an element triggers onChange callback', () => { + it('triggers onChange callback when selecting an element', () => { const items = wrapper.find(DropdownItem); items.at(2).simulate('click'); diff --git a/test/utils/dates/DateIntervalSelector.test.tsx b/test/utils/dates/DateIntervalSelector.test.tsx index 51a24f88..94c068d7 100644 --- a/test/utils/dates/DateIntervalSelector.test.tsx +++ b/test/utils/dates/DateIntervalSelector.test.tsx @@ -14,7 +14,7 @@ describe('', () => { }); afterEach(() => wrapper?.unmount()); - test('props are passed down to nested DateIntervalDropdownItems', () => { + it('passes props down to nested DateIntervalDropdownItems', () => { const items = wrapper.find(DateIntervalDropdownItems); const dropdown = wrapper.find(DropdownBtn); diff --git a/test/utils/dates/DateRangeSelector.test.tsx b/test/utils/dates/DateRangeSelector.test.tsx index 35e8ab1a..f26ef990 100644 --- a/test/utils/dates/DateRangeSelector.test.tsx +++ b/test/utils/dates/DateRangeSelector.test.tsx @@ -18,7 +18,7 @@ describe('', () => { afterEach(jest.clearAllMocks); afterEach(() => wrapper?.unmount()); - test('proper amount of items is rendered', () => { + it('renders proper amount of items', () => { const wrapper = createWrapper(); const items = wrapper.find(DropdownItem); const dateIntervalItems = wrapper.find(DateIntervalDropdownItems); @@ -31,7 +31,7 @@ describe('', () => { expect(items.filter('[active]')).toHaveLength(1); }); - test.each([ + it.each([ [ undefined, 1, 0 ], [ 'today' as DateInterval, 0, 1 ], [ 'yesterday' as DateInterval, 0, 1 ], @@ -41,7 +41,7 @@ describe('', () => { [ 'last180days' as DateInterval, 0, 1 ], [ 'last365Days' as DateInterval, 0, 1 ], [{ startDate: moment() }, 0, 0 ], - ])('proper element is active based on provided date range', ( + ])('sets proper element as active based on provided date range', ( initialDateRange, expectedActiveItems, expectedActiveIntervalItems, @@ -56,7 +56,7 @@ describe('', () => { expect(dateIntervalItems).toHaveLength(expectedActiveIntervalItems); }); - test('selecting an element triggers onDatesChange callback', () => { + it('triggers onDatesChange callback when selecting an element', () => { const wrapper = createWrapper(); const item = wrapper.find(DropdownItem).at(0); const dateIntervalItems = wrapper.find(DateIntervalDropdownItems); From e76b22b2ae40b2fb286d9b57e1d7eb4f47b7dd46 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 6 Mar 2021 17:30:21 +0100 Subject: [PATCH 5/6] Ensured consistent heights in settings cards --- src/settings/RealTimeUpdates.tsx | 2 +- src/settings/Settings.tsx | 4 ++-- src/settings/ShortUrlCreation.tsx | 2 +- src/settings/UserInterface.tsx | 2 +- src/settings/Visits.tsx | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/settings/RealTimeUpdates.tsx b/src/settings/RealTimeUpdates.tsx index d737f6e9..46ac074e 100644 --- a/src/settings/RealTimeUpdates.tsx +++ b/src/settings/RealTimeUpdates.tsx @@ -15,7 +15,7 @@ const intervalValue = (interval?: number) => !interval ? '' : `${interval}`; const RealTimeUpdates = ( { settings: { realTimeUpdates }, toggleRealTimeUpdates, setRealTimeUpdatesInterval }: RealTimeUpdatesProps, ) => ( - + Enable or disable real-time updates, when using Shlink v2.2.0 or newer. diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 8305442e..1c9a1b36 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -7,8 +7,8 @@ const SettingsSections: FC<{ items: ReactNode[][] }> = ({ items }) => ( {items.map((child, index) => ( {child.map((subChild, subIndex) => ( -
-
{subChild}
+
+ {subChild}
))} diff --git a/src/settings/ShortUrlCreation.tsx b/src/settings/ShortUrlCreation.tsx index afd88c12..b91ed500 100644 --- a/src/settings/ShortUrlCreation.tsx +++ b/src/settings/ShortUrlCreation.tsx @@ -12,7 +12,7 @@ interface ShortUrlCreationProps { export const ShortUrlCreation: FC = ( { settings: { shortUrlCreation }, setShortUrlCreationSettings }, ) => ( - + = ({ settings: { ui }, setUiSettings }) => ( - + = ({ settings, setVisitsSettings }) => ( - + Date: Sat, 6 Mar 2021 17:33:34 +0100 Subject: [PATCH 6/6] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ca5ced5..45b03681 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), * [#177](https://github.com/shlinkio/shlink-web-client/issues/177) Added dark theme. * [#387](https://github.com/shlinkio/shlink-web-client/issues/387) Added a section to see orphan visits stats, when consuming Shlink >=2.6.0. * [#383](https://github.com/shlinkio/shlink-web-client/issues/383) Added title to short URLs list, displayed when consuming Shlink >=2.6.0. +* [#368](https://github.com/shlinkio/shlink-web-client/issues/368) Added new settings to define the default interval for visits pages. ### Changed * [#382](https://github.com/shlinkio/shlink-web-client/issues/382) Ensured short URL tags are edited through the `PATCH /short-urls/{shortCode}` endpoint when using Shlink 2.6.0 or higher.