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);