mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 17:40:23 +03:00
Added new settings card to customize short URLs lists
This commit is contained in:
parent
d4356ba6e6
commit
de32d899bc
10 changed files with 123 additions and 15 deletions
|
@ -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>
|
||||||
|
|
24
src/settings/ShortUrlsList.tsx
Normal file
24
src/settings/ShortUrlsList.tsx
Normal 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>
|
||||||
|
);
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 ]);
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
48
test/settings/ShortUrlsList.test.tsx
Normal file
48
test/settings/ShortUrlsList.test.tsx
Normal 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 } });
|
||||||
|
});
|
||||||
|
});
|
|
@ -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 } });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue