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) => (
+
+ ))}
+
+ ))}
+ >
+);
+
+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);