Added new settings card to customize short URLs lists

This commit is contained in:
Alejandro Celaya 2021-12-24 14:15:28 +01:00
parent d4356ba6e6
commit de32d899bc
10 changed files with 123 additions and 15 deletions

View file

@ -16,13 +16,20 @@ const SettingsSections: FC<{ items: ReactNode[][] }> = ({ items }) => (
</> </>
); );
const Settings = (RealTimeUpdates: FC, ShortUrlCreation: FC, UserInterface: FC, Visits: FC, Tags: FC) => () => ( const Settings = (
RealTimeUpdates: FC,
ShortUrlCreation: FC,
ShortUrlsList: FC,
UserInterface: FC,
Visits: FC,
Tags: FC,
) => () => (
<NoMenuLayout> <NoMenuLayout>
<SettingsSections <SettingsSections
items={[ items={[
[ <UserInterface /> ], // eslint-disable-line react/jsx-key [ <UserInterface />, <Visits /> ], // eslint-disable-line react/jsx-key
[ <ShortUrlCreation />, <RealTimeUpdates /> ], // eslint-disable-line react/jsx-key [ <ShortUrlCreation />, <ShortUrlsList /> ], // eslint-disable-line react/jsx-key
[ <Tags />, <Visits /> ], // eslint-disable-line react/jsx-key [ <Tags />, <RealTimeUpdates /> ], // eslint-disable-line react/jsx-key
]} ]}
/> />
</NoMenuLayout> </NoMenuLayout>

View file

@ -0,0 +1,24 @@
import { FC } from 'react';
import { FormGroup } from 'reactstrap';
import SortingDropdown from '../utils/SortingDropdown';
import { SORTABLE_FIELDS } from '../short-urls/data';
import { SimpleCard } from '../utils/SimpleCard';
import { DEFAULT_SHORT_URLS_ORDERING, Settings, ShortUrlsListSettings } from './reducers/settings';
interface ShortUrlsListProps {
settings: Settings;
setShortUrlsListSettings: (settings: ShortUrlsListSettings) => void;
}
export const ShortUrlsList: FC<ShortUrlsListProps> = ({ settings: { shortUrlsList }, setShortUrlsListSettings }) => (
<SimpleCard title="Short URLs list" className="h-100">
<FormGroup className="mb-0">
<label>Default ordering for short URLs list:</label>
<SortingDropdown
items={SORTABLE_FIELDS}
order={shortUrlsList?.defaultOrdering ?? DEFAULT_SHORT_URLS_ORDERING}
onChange={(field, dir) => setShortUrlsListSettings({ defaultOrdering: { field, dir } })}
/>
</FormGroup>
</SimpleCard>
);

View file

@ -13,7 +13,7 @@ interface TagsProps {
} }
export const Tags: FC<TagsProps> = ({ settings: { tags }, setTagsSettings }) => ( export const Tags: FC<TagsProps> = ({ settings: { tags }, setTagsSettings }) => (
<SimpleCard title="Tags"> <SimpleCard title="Tags" className="h-100">
<FormGroup> <FormGroup>
<label>Default display mode when managing tags:</label> <label>Default display mode when managing tags:</label>
<TagsModeDropdown <TagsModeDropdown

View file

@ -47,14 +47,14 @@ export interface TagsSettings {
defaultMode?: TagsMode; defaultMode?: TagsMode;
} }
export interface ShortUrlListSettings { export interface ShortUrlsListSettings {
defaultOrdering?: ShortUrlsOrder; defaultOrdering?: ShortUrlsOrder;
} }
export interface Settings { export interface Settings {
realTimeUpdates: RealTimeUpdatesSettings; realTimeUpdates: RealTimeUpdatesSettings;
shortUrlCreation?: ShortUrlCreationSettings; shortUrlCreation?: ShortUrlCreationSettings;
shortUrlList?: ShortUrlListSettings; shortUrlsList?: ShortUrlsListSettings;
ui?: UiSettings; ui?: UiSettings;
visits?: VisitsSettings; visits?: VisitsSettings;
tags?: TagsSettings; tags?: TagsSettings;
@ -73,7 +73,7 @@ const initialState: Settings = {
visits: { visits: {
defaultInterval: 'last30Days', defaultInterval: 'last30Days',
}, },
shortUrlList: { shortUrlsList: {
defaultOrdering: DEFAULT_SHORT_URLS_ORDERING, defaultOrdering: DEFAULT_SHORT_URLS_ORDERING,
}, },
}; };
@ -101,6 +101,11 @@ export const setShortUrlCreationSettings = (settings: ShortUrlCreationSettings):
shortUrlCreation: settings, shortUrlCreation: settings,
}); });
export const setShortUrlsListSettings = (settings: ShortUrlsListSettings): PartialSettingsAction => ({
type: SET_SETTINGS,
shortUrlsList: settings,
});
export const setUiSettings = (settings: UiSettings): PartialSettingsAction => ({ export const setUiSettings = (settings: UiSettings): PartialSettingsAction => ({
type: SET_SETTINGS, type: SET_SETTINGS,
ui: settings, ui: settings,

View file

@ -4,6 +4,7 @@ import Settings from '../Settings';
import { import {
setRealTimeUpdatesInterval, setRealTimeUpdatesInterval,
setShortUrlCreationSettings, setShortUrlCreationSettings,
setShortUrlsListSettings,
setTagsSettings, setTagsSettings,
setUiSettings, setUiSettings,
setVisitsSettings, setVisitsSettings,
@ -15,10 +16,20 @@ import { ShortUrlCreation } from '../ShortUrlCreation';
import { UserInterface } from '../UserInterface'; import { UserInterface } from '../UserInterface';
import { Visits } from '../Visits'; import { Visits } from '../Visits';
import { Tags } from '../Tags'; import { Tags } from '../Tags';
import { ShortUrlsList } from '../ShortUrlsList';
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
// Components // Components
bottle.serviceFactory('Settings', Settings, 'RealTimeUpdates', 'ShortUrlCreation', 'UserInterface', 'Visits', 'Tags'); bottle.serviceFactory(
'Settings',
Settings,
'RealTimeUpdates',
'ShortUrlCreation',
'ShortUrlsListSettings',
'UserInterface',
'Visits',
'Tags',
);
bottle.decorator('Settings', withoutSelectedServer); bottle.decorator('Settings', withoutSelectedServer);
bottle.decorator('Settings', connect(null, [ 'resetSelectedServer' ])); bottle.decorator('Settings', connect(null, [ 'resetSelectedServer' ]));
@ -40,10 +51,14 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('Tags', () => Tags); bottle.serviceFactory('Tags', () => Tags);
bottle.decorator('Tags', connect([ 'settings' ], [ 'setTagsSettings' ])); bottle.decorator('Tags', connect([ 'settings' ], [ 'setTagsSettings' ]));
bottle.serviceFactory('ShortUrlsListSettings', () => ShortUrlsList);
bottle.decorator('ShortUrlsListSettings', connect([ 'settings' ], [ 'setShortUrlsListSettings' ]));
// Actions // Actions
bottle.serviceFactory('toggleRealTimeUpdates', () => toggleRealTimeUpdates); bottle.serviceFactory('toggleRealTimeUpdates', () => toggleRealTimeUpdates);
bottle.serviceFactory('setRealTimeUpdatesInterval', () => setRealTimeUpdatesInterval); bottle.serviceFactory('setRealTimeUpdatesInterval', () => setRealTimeUpdatesInterval);
bottle.serviceFactory('setShortUrlCreationSettings', () => setShortUrlCreationSettings); bottle.serviceFactory('setShortUrlCreationSettings', () => setShortUrlCreationSettings);
bottle.serviceFactory('setShortUrlsListSettings', () => setShortUrlsListSettings);
bottle.serviceFactory('setUiSettings', () => setUiSettings); bottle.serviceFactory('setUiSettings', () => setUiSettings);
bottle.serviceFactory('setVisitsSettings', () => setVisitsSettings); bottle.serviceFactory('setVisitsSettings', () => setVisitsSettings);
bottle.serviceFactory('setTagsSettings', () => setTagsSettings); bottle.serviceFactory('setTagsSettings', () => setTagsSettings);

View file

@ -33,7 +33,7 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>, SearchBar: FC) =
settings, settings,
}: ShortUrlsListProps) => { }: ShortUrlsListProps) => {
const serverId = getServerId(selectedServer); const serverId = getServerId(selectedServer);
const initialOrderBy = settings.shortUrlList?.defaultOrdering ?? DEFAULT_SHORT_URLS_ORDERING; const initialOrderBy = settings.shortUrlsList?.defaultOrdering ?? DEFAULT_SHORT_URLS_ORDERING;
const [ order, setOrder ] = useState<ShortUrlsOrder>(initialOrderBy); const [ order, setOrder ] = useState<ShortUrlsOrder>(initialOrderBy);
const [{ tags, search, startDate, endDate }, toFirstPage ] = useShortUrlsQuery({ history, match, location }); const [{ tags, search, startDate, endDate }, toFirstPage ] = useShortUrlsQuery({ history, match, location });
const selectedTags = useMemo(() => tags?.split(',') ?? [], [ tags ]); const selectedTags = useMemo(() => tags?.split(',') ?? [], [ tags ]);

View file

@ -4,7 +4,7 @@ import NoMenuLayout from '../../src/common/NoMenuLayout';
describe('<Settings />', () => { describe('<Settings />', () => {
const Component = () => null; const Component = () => null;
const Settings = createSettings(Component, Component, Component, Component, Component); const Settings = createSettings(Component, Component, Component, Component, Component, Component);
it('renders a no-menu layout with the expected settings sections', () => { it('renders a no-menu layout with the expected settings sections', () => {
const wrapper = shallow(<Settings />); const wrapper = shallow(<Settings />);
@ -13,6 +13,6 @@ describe('<Settings />', () => {
expect(layout).toHaveLength(1); expect(layout).toHaveLength(1);
expect(sections).toHaveLength(1); expect(sections).toHaveLength(1);
expect((sections.prop('items') as any[]).flat()).toHaveLength(5); expect((sections.prop('items') as any[]).flat()).toHaveLength(6);
}); });
}); });

View file

@ -0,0 +1,48 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { Mock } from 'ts-mockery';
import { DEFAULT_SHORT_URLS_ORDERING, Settings, ShortUrlsListSettings } from '../../src/settings/reducers/settings';
import { ShortUrlsList } from '../../src/settings/ShortUrlsList';
import SortingDropdown from '../../src/utils/SortingDropdown';
import { ShortUrlsOrder } from '../../src/short-urls/data';
describe('<ShortUrlsList />', () => {
let wrapper: ShallowWrapper;
const setSettings = jest.fn();
const createWrapper = (shortUrlsList?: ShortUrlsListSettings) => {
wrapper = shallow(
<ShortUrlsList settings={Mock.of<Settings>({ shortUrlsList })} setShortUrlsListSettings={setSettings} />,
);
return wrapper;
};
afterEach(() => wrapper?.unmount());
afterEach(jest.clearAllMocks);
it.each([
[ undefined, DEFAULT_SHORT_URLS_ORDERING ],
[{}, DEFAULT_SHORT_URLS_ORDERING ],
[{ defaultOrdering: {} }, {}],
[{ defaultOrdering: { field: 'longUrl', dir: 'DESC' } as ShortUrlsOrder }, { field: 'longUrl', dir: 'DESC' }],
[{ defaultOrdering: { field: 'visits', dir: 'ASC' } as ShortUrlsOrder }, { field: 'visits', dir: 'ASC' }],
])('shows expected ordering', (shortUrlsList, expectedOrder) => {
const wrapper = createWrapper(shortUrlsList);
const dropdown = wrapper.find(SortingDropdown);
expect(dropdown.prop('order')).toEqual(expectedOrder);
});
it.each([
[ undefined, undefined ],
[ 'longUrl', 'ASC' ],
[ 'visits', undefined ],
[ 'title', 'DESC' ],
])('invokes setSettings when ordering changes', (field, dir) => {
const wrapper = createWrapper();
const dropdown = wrapper.find(SortingDropdown);
expect(setSettings).not.toHaveBeenCalled();
dropdown.simulate('change', field, dir);
expect(setSettings).toHaveBeenCalledWith({ defaultOrdering: { field, dir } });
});
});

View file

@ -7,6 +7,7 @@ import reducer, {
setUiSettings, setUiSettings,
setVisitsSettings, setVisitsSettings,
setTagsSettings, setTagsSettings,
setShortUrlsListSettings,
} from '../../../src/settings/reducers/settings'; } from '../../../src/settings/reducers/settings';
describe('settingsReducer', () => { describe('settingsReducer', () => {
@ -14,8 +15,8 @@ describe('settingsReducer', () => {
const shortUrlCreation = { validateUrls: false }; const shortUrlCreation = { validateUrls: false };
const ui = { theme: 'light' }; const ui = { theme: 'light' };
const visits = { defaultInterval: 'last30Days' }; const visits = { defaultInterval: 'last30Days' };
const shortUrlList = { defaultOrdering: DEFAULT_SHORT_URLS_ORDERING }; const shortUrlsList = { defaultOrdering: DEFAULT_SHORT_URLS_ORDERING };
const settings = { realTimeUpdates, shortUrlCreation, ui, visits, shortUrlList }; const settings = { realTimeUpdates, shortUrlCreation, ui, visits, shortUrlsList };
describe('reducer', () => { describe('reducer', () => {
it('returns realTimeUpdates when action is SET_SETTINGS', () => { it('returns realTimeUpdates when action is SET_SETTINGS', () => {
@ -70,4 +71,12 @@ describe('settingsReducer', () => {
expect(result).toEqual({ type: SET_SETTINGS, tags: { defaultMode: 'list' } }); expect(result).toEqual({ type: SET_SETTINGS, tags: { defaultMode: 'list' } });
}); });
}); });
describe('setShortUrlsListSettings', () => {
it('creates action to set short URLs list settings', () => {
const result = setShortUrlsListSettings({ defaultOrdering: DEFAULT_SHORT_URLS_ORDERING });
expect(result).toEqual({ type: SET_SETTINGS, shortUrlsList: { defaultOrdering: DEFAULT_SHORT_URLS_ORDERING } });
});
});
}); });

View file

@ -41,7 +41,7 @@ describe('<ShortUrlsList />', () => {
shortUrlsList={shortUrlsList} shortUrlsList={shortUrlsList}
history={Mock.of<History>({ push })} history={Mock.of<History>({ push })}
selectedServer={Mock.of<ReachableServer>({ id: '1' })} selectedServer={Mock.of<ReachableServer>({ id: '1' })}
settings={Mock.of<Settings>({ shortUrlList: { defaultOrdering } })} settings={Mock.of<Settings>({ shortUrlsList: { defaultOrdering } })}
/>, />,
).dive(); // Dive is needed as this component is wrapped in a HOC ).dive(); // Dive is needed as this component is wrapped in a HOC