mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +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>
|
||||
<SettingsSections
|
||||
items={[
|
||||
[ <UserInterface /> ], // eslint-disable-line react/jsx-key
|
||||
[ <ShortUrlCreation />, <RealTimeUpdates /> ], // eslint-disable-line react/jsx-key
|
||||
[ <Tags />, <Visits /> ], // eslint-disable-line react/jsx-key
|
||||
[ <UserInterface />, <Visits /> ], // eslint-disable-line react/jsx-key
|
||||
[ <ShortUrlCreation />, <ShortUrlsList /> ], // eslint-disable-line react/jsx-key
|
||||
[ <Tags />, <RealTimeUpdates /> ], // eslint-disable-line react/jsx-key
|
||||
]}
|
||||
/>
|
||||
</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 }) => (
|
||||
<SimpleCard title="Tags">
|
||||
<SimpleCard title="Tags" className="h-100">
|
||||
<FormGroup>
|
||||
<label>Default display mode when managing tags:</label>
|
||||
<TagsModeDropdown
|
||||
|
|
|
@ -47,14 +47,14 @@ export interface TagsSettings {
|
|||
defaultMode?: TagsMode;
|
||||
}
|
||||
|
||||
export interface ShortUrlListSettings {
|
||||
export interface ShortUrlsListSettings {
|
||||
defaultOrdering?: ShortUrlsOrder;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
realTimeUpdates: RealTimeUpdatesSettings;
|
||||
shortUrlCreation?: ShortUrlCreationSettings;
|
||||
shortUrlList?: ShortUrlListSettings;
|
||||
shortUrlsList?: ShortUrlsListSettings;
|
||||
ui?: UiSettings;
|
||||
visits?: VisitsSettings;
|
||||
tags?: TagsSettings;
|
||||
|
@ -73,7 +73,7 @@ const initialState: Settings = {
|
|||
visits: {
|
||||
defaultInterval: 'last30Days',
|
||||
},
|
||||
shortUrlList: {
|
||||
shortUrlsList: {
|
||||
defaultOrdering: DEFAULT_SHORT_URLS_ORDERING,
|
||||
},
|
||||
};
|
||||
|
@ -101,6 +101,11 @@ export const setShortUrlCreationSettings = (settings: ShortUrlCreationSettings):
|
|||
shortUrlCreation: settings,
|
||||
});
|
||||
|
||||
export const setShortUrlsListSettings = (settings: ShortUrlsListSettings): PartialSettingsAction => ({
|
||||
type: SET_SETTINGS,
|
||||
shortUrlsList: settings,
|
||||
});
|
||||
|
||||
export const setUiSettings = (settings: UiSettings): PartialSettingsAction => ({
|
||||
type: SET_SETTINGS,
|
||||
ui: settings,
|
||||
|
|
|
@ -4,6 +4,7 @@ import Settings from '../Settings';
|
|||
import {
|
||||
setRealTimeUpdatesInterval,
|
||||
setShortUrlCreationSettings,
|
||||
setShortUrlsListSettings,
|
||||
setTagsSettings,
|
||||
setUiSettings,
|
||||
setVisitsSettings,
|
||||
|
@ -15,10 +16,20 @@ import { ShortUrlCreation } from '../ShortUrlCreation';
|
|||
import { UserInterface } from '../UserInterface';
|
||||
import { Visits } from '../Visits';
|
||||
import { Tags } from '../Tags';
|
||||
import { ShortUrlsList } from '../ShortUrlsList';
|
||||
|
||||
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||
// 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', connect(null, [ 'resetSelectedServer' ]));
|
||||
|
||||
|
@ -40,10 +51,14 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||
bottle.serviceFactory('Tags', () => Tags);
|
||||
bottle.decorator('Tags', connect([ 'settings' ], [ 'setTagsSettings' ]));
|
||||
|
||||
bottle.serviceFactory('ShortUrlsListSettings', () => ShortUrlsList);
|
||||
bottle.decorator('ShortUrlsListSettings', connect([ 'settings' ], [ 'setShortUrlsListSettings' ]));
|
||||
|
||||
// Actions
|
||||
bottle.serviceFactory('toggleRealTimeUpdates', () => toggleRealTimeUpdates);
|
||||
bottle.serviceFactory('setRealTimeUpdatesInterval', () => setRealTimeUpdatesInterval);
|
||||
bottle.serviceFactory('setShortUrlCreationSettings', () => setShortUrlCreationSettings);
|
||||
bottle.serviceFactory('setShortUrlsListSettings', () => setShortUrlsListSettings);
|
||||
bottle.serviceFactory('setUiSettings', () => setUiSettings);
|
||||
bottle.serviceFactory('setVisitsSettings', () => setVisitsSettings);
|
||||
bottle.serviceFactory('setTagsSettings', () => setTagsSettings);
|
||||
|
|
|
@ -33,7 +33,7 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>, SearchBar: FC) =
|
|||
settings,
|
||||
}: ShortUrlsListProps) => {
|
||||
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 [{ tags, search, startDate, endDate }, toFirstPage ] = useShortUrlsQuery({ history, match, location });
|
||||
const selectedTags = useMemo(() => tags?.split(',') ?? [], [ tags ]);
|
||||
|
|
|
@ -4,7 +4,7 @@ import NoMenuLayout from '../../src/common/NoMenuLayout';
|
|||
|
||||
describe('<Settings />', () => {
|
||||
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', () => {
|
||||
const wrapper = shallow(<Settings />);
|
||||
|
@ -13,6 +13,6 @@ describe('<Settings />', () => {
|
|||
|
||||
expect(layout).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,
|
||||
setVisitsSettings,
|
||||
setTagsSettings,
|
||||
setShortUrlsListSettings,
|
||||
} from '../../../src/settings/reducers/settings';
|
||||
|
||||
describe('settingsReducer', () => {
|
||||
|
@ -14,8 +15,8 @@ describe('settingsReducer', () => {
|
|||
const shortUrlCreation = { validateUrls: false };
|
||||
const ui = { theme: 'light' };
|
||||
const visits = { defaultInterval: 'last30Days' };
|
||||
const shortUrlList = { defaultOrdering: DEFAULT_SHORT_URLS_ORDERING };
|
||||
const settings = { realTimeUpdates, shortUrlCreation, ui, visits, shortUrlList };
|
||||
const shortUrlsList = { defaultOrdering: DEFAULT_SHORT_URLS_ORDERING };
|
||||
const settings = { realTimeUpdates, shortUrlCreation, ui, visits, shortUrlsList };
|
||||
|
||||
describe('reducer', () => {
|
||||
it('returns realTimeUpdates when action is SET_SETTINGS', () => {
|
||||
|
@ -70,4 +71,12 @@ describe('settingsReducer', () => {
|
|||
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}
|
||||
history={Mock.of<History>({ push })}
|
||||
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
|
||||
|
||||
|
|
Loading…
Reference in a new issue