Added option to customize ordering in tags list

This commit is contained in:
Alejandro Celaya 2021-12-24 11:05:22 +01:00
parent e954a860bf
commit d8442e435d
6 changed files with 60 additions and 7 deletions

View file

@ -3,6 +3,8 @@ import { FormGroup } from 'reactstrap';
import { SimpleCard } from '../utils/SimpleCard'; import { SimpleCard } from '../utils/SimpleCard';
import { TagsModeDropdown } from '../tags/TagsModeDropdown'; import { TagsModeDropdown } from '../tags/TagsModeDropdown';
import { capitalize } from '../utils/utils'; import { capitalize } from '../utils/utils';
import SortingDropdown from '../utils/SortingDropdown';
import { SORTABLE_FIELDS } from '../tags/data/TagsListChildrenProps';
import { Settings, TagsSettings } from './reducers/settings'; import { Settings, TagsSettings } from './reducers/settings';
interface TagsProps { interface TagsProps {
@ -12,7 +14,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">
<FormGroup className="mb-0"> <FormGroup>
<label>Default display mode when managing tags:</label> <label>Default display mode when managing tags:</label>
<TagsModeDropdown <TagsModeDropdown
mode={tags?.defaultMode ?? 'cards'} mode={tags?.defaultMode ?? 'cards'}
@ -21,5 +23,13 @@ export const Tags: FC<TagsProps> = ({ settings: { tags }, setTagsSettings }) =>
/> />
<small className="form-text text-muted">Tags will be displayed as <b>{tags?.defaultMode ?? 'cards'}</b>.</small> <small className="form-text text-muted">Tags will be displayed as <b>{tags?.defaultMode ?? 'cards'}</b>.</small>
</FormGroup> </FormGroup>
<FormGroup className="mb-0">
<label>Default ordering for tags list:</label>
<SortingDropdown
items={SORTABLE_FIELDS}
order={tags?.defaultOrdering ?? {}}
onChange={(field, dir) => setTagsSettings({ ...tags, defaultOrdering: { field, dir } })}
/>
</FormGroup>
</SimpleCard> </SimpleCard>
); );

View file

@ -5,6 +5,7 @@ import { RecursivePartial } from '../../utils/utils';
import { Theme } from '../../utils/theme'; import { Theme } from '../../utils/theme';
import { DateInterval } from '../../utils/dates/types'; import { DateInterval } from '../../utils/dates/types';
import { TagsOrder } from '../../tags/data/TagsListChildrenProps'; import { TagsOrder } from '../../tags/data/TagsListChildrenProps';
import { ShortUrlsOrder } from '../../short-urls/reducers/shortUrlsListParams';
export const SET_SETTINGS = 'shlink/realTimeUpdates/SET_SETTINGS'; export const SET_SETTINGS = 'shlink/realTimeUpdates/SET_SETTINGS';
@ -41,9 +42,14 @@ export interface TagsSettings {
defaultMode?: TagsMode; defaultMode?: TagsMode;
} }
export interface ShortUrlListSettings {
defaultOrdering?: ShortUrlsOrder;
}
export interface Settings { export interface Settings {
realTimeUpdates: RealTimeUpdatesSettings; realTimeUpdates: RealTimeUpdatesSettings;
shortUrlCreation?: ShortUrlCreationSettings; shortUrlCreation?: ShortUrlCreationSettings;
shortUrlList?: ShortUrlListSettings;
ui?: UiSettings; ui?: UiSettings;
visits?: VisitsSettings; visits?: VisitsSettings;
tags?: TagsSettings; tags?: TagsSettings;

View file

@ -3,14 +3,14 @@ import { FC, useEffect, useMemo, useState } from 'react';
import { RouteComponentProps } from 'react-router'; import { RouteComponentProps } from 'react-router';
import { Card } from 'reactstrap'; import { Card } from 'reactstrap';
import SortingDropdown from '../utils/SortingDropdown'; import SortingDropdown from '../utils/SortingDropdown';
import { determineOrderDir, Order, OrderDir } from '../utils/helpers/ordering'; import { determineOrderDir, OrderDir } from '../utils/helpers/ordering';
import { getServerId, SelectedServer } from '../servers/data'; import { getServerId, SelectedServer } from '../servers/data';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub'; import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { Topics } from '../mercure/helpers/Topics'; import { Topics } from '../mercure/helpers/Topics';
import { TableOrderIcon } from '../utils/table/TableOrderIcon'; import { TableOrderIcon } from '../utils/table/TableOrderIcon';
import { ShlinkShortUrlsListParams } from '../api/types'; import { ShlinkShortUrlsListParams } from '../api/types';
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList'; import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
import { OrderableFields, ShortUrlsListParams, SORTABLE_FIELDS } from './reducers/shortUrlsListParams'; import { OrderableFields, ShortUrlsListParams, ShortUrlsOrder, SORTABLE_FIELDS } from './reducers/shortUrlsListParams';
import { ShortUrlsTableProps } from './ShortUrlsTable'; import { ShortUrlsTableProps } from './ShortUrlsTable';
import Paginator from './Paginator'; import Paginator from './Paginator';
import { ShortUrlListRouteParams, useShortUrlsQuery } from './helpers/hooks'; import { ShortUrlListRouteParams, useShortUrlsQuery } from './helpers/hooks';
@ -23,8 +23,6 @@ interface ShortUrlsListProps extends RouteComponentProps<ShortUrlListRouteParams
resetShortUrlParams: () => void; resetShortUrlParams: () => void;
} }
type ShortUrlsOrder = Order<OrderableFields>;
const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>, SearchBar: FC) => boundToMercureHub(({ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>, SearchBar: FC) => boundToMercureHub(({
listShortUrls, listShortUrls,
resetShortUrlParams, resetShortUrlParams,

View file

@ -1,5 +1,5 @@
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux'; import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
import { OrderDir } from '../../utils/helpers/ordering'; import { Order, OrderDir } from '../../utils/helpers/ordering';
import { LIST_SHORT_URLS, ListShortUrlsAction } from './shortUrlsList'; import { LIST_SHORT_URLS, ListShortUrlsAction } from './shortUrlsList';
export const RESET_SHORT_URL_PARAMS = 'shlink/shortUrlsListParams/RESET_SHORT_URL_PARAMS'; export const RESET_SHORT_URL_PARAMS = 'shlink/shortUrlsListParams/RESET_SHORT_URL_PARAMS';
@ -14,6 +14,8 @@ export const SORTABLE_FIELDS = {
export type OrderableFields = keyof typeof SORTABLE_FIELDS; export type OrderableFields = keyof typeof SORTABLE_FIELDS;
export type ShortUrlsOrder = Order<OrderableFields>;
export type OrderBy = Partial<Record<OrderableFields, OrderDir>>; export type OrderBy = Partial<Record<OrderableFields, OrderDir>>;
export interface ShortUrlsListParams { export interface ShortUrlsListParams {

View file

@ -29,7 +29,7 @@ const TagsList = (TagsCards: FC<TagsListChildrenProps>, TagsTable: FC<TagsTableP
{ filterTags, forceListTags, tagsList, selectedServer, settings }: TagsListProps, { filterTags, forceListTags, tagsList, selectedServer, settings }: TagsListProps,
) => { ) => {
const [ mode, setMode ] = useState<TagsMode>(settings.tags?.defaultMode ?? 'cards'); const [ mode, setMode ] = useState<TagsMode>(settings.tags?.defaultMode ?? 'cards');
const [ order, setOrder ] = useState<TagsOrder>({}); const [ order, setOrder ] = useState<TagsOrder>(settings.tags?.defaultOrdering ?? {});
const resolveSortedTags = pipe( const resolveSortedTags = pipe(
() => tagsList.filteredTags.map((tag): NormalizedTag => ({ () => tagsList.filteredTags.map((tag): NormalizedTag => ({
tag, tag,

View file

@ -1,8 +1,11 @@
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { FormGroup } from 'reactstrap';
import { Settings, TagsMode, TagsSettings } from '../../src/settings/reducers/settings'; import { Settings, TagsMode, TagsSettings } from '../../src/settings/reducers/settings';
import { TagsModeDropdown } from '../../src/tags/TagsModeDropdown'; import { TagsModeDropdown } from '../../src/tags/TagsModeDropdown';
import { Tags } from '../../src/settings/Tags'; import { Tags } from '../../src/settings/Tags';
import SortingDropdown from '../../src/utils/SortingDropdown';
import { TagsOrder } from '../../src/tags/data/TagsListChildrenProps';
describe('<Tags />', () => { describe('<Tags />', () => {
let wrapper: ShallowWrapper; let wrapper: ShallowWrapper;
@ -16,6 +19,13 @@ describe('<Tags />', () => {
afterEach(() => wrapper?.unmount()); afterEach(() => wrapper?.unmount());
afterEach(jest.clearAllMocks); afterEach(jest.clearAllMocks);
it('renders expected amount of groups', () => {
const wrapper = createWrapper();
const groups = wrapper.find(FormGroup);
expect(groups).toHaveLength(2);
});
it.each([ it.each([
[ undefined, 'cards' ], [ undefined, 'cards' ],
[{}, 'cards' ], [{}, 'cards' ],
@ -41,4 +51,31 @@ describe('<Tags />', () => {
dropdown.simulate('change', defaultMode); dropdown.simulate('change', defaultMode);
expect(setTagsSettings).toHaveBeenCalledWith({ defaultMode }); expect(setTagsSettings).toHaveBeenCalledWith({ defaultMode });
}); });
it.each([
[ undefined, {}],
[{}, {}],
[{ defaultOrdering: {} }, {}],
[{ defaultOrdering: { field: 'tag', dir: 'DESC' } as TagsOrder }, { field: 'tag', dir: 'DESC' }],
[{ defaultOrdering: { field: 'visits', dir: 'ASC' } as TagsOrder }, { field: 'visits', dir: 'ASC' }],
])('shows expected ordering', (tags, expectedOrder) => {
const wrapper = createWrapper(tags);
const dropdown = wrapper.find(SortingDropdown);
expect(dropdown.prop('order')).toEqual(expectedOrder);
});
it.each([
[ undefined, undefined ],
[ 'tag', 'ASC' ],
[ 'visits', undefined ],
[ 'shortUrls', 'DESC' ],
])('invokes setTagsSettings when ordering changes', (field, dir) => {
const wrapper = createWrapper();
const dropdown = wrapper.find(SortingDropdown);
expect(setTagsSettings).not.toHaveBeenCalled();
dropdown.simulate('change', field, dir);
expect(setTagsSettings).toHaveBeenCalledWith({ defaultOrdering: { field, dir } });
});
}); });