Merge pull request #544 from acelaya-forks/feature/ordering-settings

Feature/ordering settings
This commit is contained in:
Alejandro Celaya 2021-12-24 15:21:04 +01:00 committed by GitHub
commit 2bf5f276f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 406 additions and 216 deletions

View file

@ -14,13 +14,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
* [#535](https://github.com/shlinkio/shlink-web-client/pull/535) Allowed editing default domain redirects when consuming Shlink 2.10 or newer.
* [#531](https://github.com/shlinkio/shlink-web-client/pull/531) Added custom slug field to the basic creation form in the Overview page.
* [#537](https://github.com/shlinkio/shlink-web-client/pull/537) Allowed to customize the ordering for every list in the app that supports it, being currently tags and short URLs.
### Changed
* [#534](https://github.com/shlinkio/shlink-web-client/pull/534) Updated axios.
* [#538](https://github.com/shlinkio/shlink-web-client/pull/538) Switched to the `<field>-<dir>` notation in `orderBy` param for short URLs list, in preparation for Shlink v3.0.0
* Fixed typo in identifier for "Last 180 days" interval.
If that was your default interval, you will see now "All visits" is selected instead. You will need to go to settings page and change it again to "Last 180 days".
### Deprecated
* *Nothing*

View file

@ -24,12 +24,11 @@ const buildShlinkBaseUrl = (url: string, apiVersion: number) => url ? `${url}/re
const rejectNilProps = reject(isNil);
const normalizeOrderByInParams = (params: ShlinkShortUrlsListParams): ShlinkShortUrlsListNormalizedParams => {
const { orderBy = {}, ...rest } = params;
const [ firstKey ] = Object.keys(orderBy);
const [ firstValue ] = Object.values(orderBy);
const { field, dir } = orderBy;
return !firstValue ? rest : {
return !dir ? rest : {
...rest,
orderBy: `${firstKey}-${firstValue}`,
orderBy: `${field}-${dir}`,
};
};

View file

@ -1,7 +1,6 @@
import { Visit } from '../../visits/types';
import { OptionalString } from '../../utils/utils';
import { ShortUrl, ShortUrlMeta } from '../../short-urls/data';
import { OrderBy } from '../../short-urls/reducers/shortUrlsListParams';
import { ShortUrl, ShortUrlMeta, ShortUrlsOrder } from '../../short-urls/data';
export interface ShlinkShortUrlsResponse {
data: ShortUrl[];
@ -94,7 +93,7 @@ export interface ShlinkShortUrlsListParams {
searchTerm?: string;
startDate?: string;
endDate?: string;
orderBy?: OrderBy;
orderBy?: ShortUrlsOrder;
}
export interface ShlinkShortUrlsListNormalizedParams extends Omit<ShlinkShortUrlsListParams, 'orderBy'> {

View file

@ -18,7 +18,8 @@ import { ConnectDecorator } from './types';
type LazyActionMap = Record<string, Function>;
const bottle = new Bottle();
const { container } = bottle;
export const { container } = bottle;
const lazyService = <T extends Function, K>(container: IContainer, serviceName: string) =>
(...args: any[]) => (container[serviceName] as T)(...args) as K;
@ -44,5 +45,3 @@ provideUtilsServices(bottle);
provideMercureServices(bottle);
provideSettingsServices(bottle, connect);
provideDomainsServices(bottle, connect);
export default container;

View file

@ -2,6 +2,8 @@ import ReduxThunk from 'redux-thunk';
import { applyMiddleware, compose, createStore } from 'redux';
import { save, load, RLSOptions } from 'redux-localstorage-simple';
import reducers from '../reducers';
import { migrateDeprecatedSettings } from '../settings/helpers';
import { ShlinkState } from './types';
const isProduction = process.env.NODE_ENV !== 'production';
const composeEnhancers: Function = !isProduction && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
@ -12,9 +14,8 @@ const localStorageConfig: RLSOptions = {
namespaceSeparator: '.',
debounce: 300,
};
const preloadedState = migrateDeprecatedSettings(load(localStorageConfig) as ShlinkState);
const store = createStore(reducers, load(localStorageConfig), composeEnhancers(
export const store = createStore(reducers, preloadedState, composeEnhancers(
applyMiddleware(save(localStorageConfig), ReduxThunk),
));
export default store;

View file

@ -4,7 +4,6 @@ import { Settings } from '../settings/reducers/settings';
import { ShortUrlCreation } from '../short-urls/reducers/shortUrlCreation';
import { ShortUrlDeletion } from '../short-urls/reducers/shortUrlDeletion';
import { ShortUrlEdition } from '../short-urls/reducers/shortUrlEdition';
import { ShortUrlsListParams } from '../short-urls/reducers/shortUrlsListParams';
import { ShortUrlsList } from '../short-urls/reducers/shortUrlsList';
import { TagDeletion } from '../tags/reducers/tagDelete';
import { TagEdition } from '../tags/reducers/tagEdit';
@ -20,7 +19,6 @@ export interface ShlinkState {
servers: ServersMap;
selectedServer: SelectedServer;
shortUrlsList: ShortUrlsList;
shortUrlsListParams: ShortUrlsListParams;
shortUrlCreationResult: ShortUrlCreation;
shortUrlDeletion: ShortUrlDeletion;
shortUrlEdition: ShortUrlEdition;

View file

@ -2,8 +2,8 @@ import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import { homepage } from '../package.json';
import container from './container';
import store from './container/store';
import { container } from './container';
import { store } from './container/store';
import { fixLeafletIcons } from './utils/helpers/leaflet';
import { register as registerServiceWorker } from './serviceWorkerRegistration';
import 'react-datepicker/dist/react-datepicker.css';

View file

@ -2,7 +2,6 @@ import { combineReducers } from 'redux';
import serversReducer from '../servers/reducers/servers';
import selectedServerReducer from '../servers/reducers/selectedServer';
import shortUrlsListReducer from '../short-urls/reducers/shortUrlsList';
import shortUrlsListParamsReducer from '../short-urls/reducers/shortUrlsListParams';
import shortUrlCreationReducer from '../short-urls/reducers/shortUrlCreation';
import shortUrlDeletionReducer from '../short-urls/reducers/shortUrlDeletion';
import shortUrlEditionReducer from '../short-urls/reducers/shortUrlEdition';
@ -24,7 +23,6 @@ export default combineReducers<ShlinkState>({
servers: serversReducer,
selectedServer: selectedServerReducer,
shortUrlsList: shortUrlsListReducer,
shortUrlsListParams: shortUrlsListParamsReducer,
shortUrlCreationResult: shortUrlCreationReducer,
shortUrlDeletion: shortUrlDeletionReducer,
shortUrlEdition: shortUrlEditionReducer,

View file

@ -44,7 +44,7 @@ export const Overview = (
const history = useHistory();
useEffect(() => {
listShortUrls({ itemsPerPage: 5, orderBy: { dateCreated: 'DESC' } });
listShortUrls({ itemsPerPage: 5, orderBy: { field: 'dateCreated', dir: 'DESC' } });
listTags();
loadVisitsOverview();
}, []);

View file

@ -1,6 +1,5 @@
import { identity, memoizeWith, pipe } from 'ramda';
import { Action, Dispatch } from 'redux';
import { resetShortUrlParams } from '../../short-urls/reducers/shortUrlsListParams';
import { versionToPrintable, versionToSemVer as toSemVer } from '../../utils/helpers/version';
import { SelectedServer } from '../data';
import { GetState } from '../../container/types';
@ -53,7 +52,6 @@ export const selectServer = (
getState: GetState,
) => {
dispatch(resetSelectedServer());
dispatch(resetShortUrlParams());
const { servers } = getState();
const selectedServer = servers[serverId];

View file

@ -7,7 +7,7 @@ const SettingsSections: FC<{ items: ReactNode[][] }> = ({ items }) => (
{items.map((child, index) => (
<Row key={index}>
{child.map((subChild, subIndex) => (
<div key={subIndex} className="col-lg-6 mb-3">
<div key={subIndex} className={`col-lg-${12 / child.length} mb-3`}>
{subChild}
</div>
))}
@ -16,12 +16,20 @@ const SettingsSections: FC<{ items: ReactNode[][] }> = ({ items }) => (
</>
);
const Settings = (RealTimeUpdates: FC, ShortUrlCreation: FC, UserInterface: FC, Visits: FC) => () => (
const Settings = (
RealTimeUpdates: FC,
ShortUrlCreation: FC,
ShortUrlsList: FC,
UserInterface: FC,
Visits: FC,
Tags: FC,
) => () => (
<NoMenuLayout>
<SettingsSections
items={[
[ <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 />, <RealTimeUpdates /> ], // eslint-disable-line react/jsx-key
]}
/>
</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>
);

35
src/settings/Tags.tsx Normal file
View file

@ -0,0 +1,35 @@
import { FC } from 'react';
import { FormGroup } from 'reactstrap';
import { SimpleCard } from '../utils/SimpleCard';
import { TagsModeDropdown } from '../tags/TagsModeDropdown';
import { capitalize } from '../utils/utils';
import SortingDropdown from '../utils/SortingDropdown';
import { SORTABLE_FIELDS } from '../tags/data/TagsListChildrenProps';
import { Settings, TagsSettings } from './reducers/settings';
interface TagsProps {
settings: Settings;
setTagsSettings: (settings: TagsSettings) => void;
}
export const Tags: FC<TagsProps> = ({ settings: { tags }, setTagsSettings }) => (
<SimpleCard title="Tags" className="h-100">
<FormGroup>
<label>Default display mode when managing tags:</label>
<TagsModeDropdown
mode={tags?.defaultMode ?? 'cards'}
renderTitle={(tagsMode) => capitalize(tagsMode)}
onChange={(defaultMode) => setTagsSettings({ ...tags, defaultMode })}
/>
<small className="form-text text-muted">Tags will be displayed as <b>{tags?.defaultMode ?? 'cards'}</b>.</small>
</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>
);

View file

@ -5,8 +5,6 @@ import { FormGroup } from 'reactstrap';
import { SimpleCard } from '../utils/SimpleCard';
import ToggleSwitch from '../utils/ToggleSwitch';
import { changeThemeInMarkup, Theme } from '../utils/theme';
import { TagsModeDropdown } from '../tags/TagsModeDropdown';
import { capitalize } from '../utils/utils';
import { Settings, UiSettings } from './reducers/settings';
import './UserInterface.scss';
@ -31,14 +29,5 @@ export const UserInterface: FC<UserInterfaceProps> = ({ settings: { ui }, setUiS
Use dark theme.
</ToggleSwitch>
</FormGroup>
<FormGroup className="mb-0">
<label>Default display mode when managing tags:</label>
<TagsModeDropdown
mode={ui?.tagsMode ?? 'cards'}
renderTitle={(tagsMode) => capitalize(tagsMode)}
onChange={(tagsMode) => setUiSettings({ ...ui ?? { theme: 'light' }, tagsMode })}
/>
<small className="form-text text-muted">Tags will be displayed as <b>{ui?.tagsMode ?? 'cards'}</b>.</small>
</FormGroup>
</SimpleCard>
);

View file

@ -0,0 +1,17 @@
import { ShlinkState } from '../../container/types';
export const migrateDeprecatedSettings = (state: ShlinkState): ShlinkState => {
// The "last180Days" interval had a typo, with a lowercase d
if ((state.settings.visits?.defaultInterval as any) === 'last180days') {
state.settings.visits && (state.settings.visits.defaultInterval = 'last180Days');
}
// The "tags display mode" option has been moved from "ui" to "tags"
state.settings.tags = {
...state.settings.tags,
defaultMode: state.settings.tags?.defaultMode ?? (state.settings.ui as any)?.tagsMode,
};
state.settings.ui && delete (state.settings.ui as any).tagsMode;
return state;
};

View file

@ -4,9 +4,16 @@ import { buildReducer } from '../../utils/helpers/redux';
import { RecursivePartial } from '../../utils/utils';
import { Theme } from '../../utils/theme';
import { DateInterval } from '../../utils/dates/types';
import { TagsOrder } from '../../tags/data/TagsListChildrenProps';
import { ShortUrlsOrder } from '../../short-urls/data';
export const SET_SETTINGS = 'shlink/realTimeUpdates/SET_SETTINGS';
export const DEFAULT_SHORT_URLS_ORDERING: ShortUrlsOrder = {
field: 'dateCreated',
dir: 'DESC',
};
/**
* Important! When adding new props in the main Settings interface or any of the nested props, they have to be set as
* optional, as old instances of the app will load partial objects from local storage until it is saved again.
@ -29,18 +36,28 @@ export type TagsMode = 'cards' | 'list';
export interface UiSettings {
theme: Theme;
tagsMode?: TagsMode;
}
export interface VisitsSettings {
defaultInterval: DateInterval;
}
export interface TagsSettings {
defaultOrdering?: TagsOrder;
defaultMode?: TagsMode;
}
export interface ShortUrlsListSettings {
defaultOrdering?: ShortUrlsOrder;
}
export interface Settings {
realTimeUpdates: RealTimeUpdatesSettings;
shortUrlCreation?: ShortUrlCreationSettings;
shortUrlsList?: ShortUrlsListSettings;
ui?: UiSettings;
visits?: VisitsSettings;
tags?: TagsSettings;
}
const initialState: Settings = {
@ -56,6 +73,9 @@ const initialState: Settings = {
visits: {
defaultInterval: 'last30Days',
},
shortUrlsList: {
defaultOrdering: DEFAULT_SHORT_URLS_ORDERING,
},
};
type SettingsAction = Action & Settings;
@ -81,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,
@ -90,3 +115,8 @@ export const setVisitsSettings = (settings: VisitsSettings): PartialSettingsActi
type: SET_SETTINGS,
visits: settings,
});
export const setTagsSettings = (settings: TagsSettings): PartialSettingsAction => ({
type: SET_SETTINGS,
tags: settings,
});

View file

@ -4,6 +4,8 @@ import Settings from '../Settings';
import {
setRealTimeUpdatesInterval,
setShortUrlCreationSettings,
setShortUrlsListSettings,
setTagsSettings,
setUiSettings,
setVisitsSettings,
toggleRealTimeUpdates,
@ -13,10 +15,21 @@ import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServ
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');
bottle.serviceFactory(
'Settings',
Settings,
'RealTimeUpdates',
'ShortUrlCreation',
'ShortUrlsListSettings',
'UserInterface',
'Visits',
'Tags',
);
bottle.decorator('Settings', withoutSelectedServer);
bottle.decorator('Settings', connect(null, [ 'resetSelectedServer' ]));
@ -35,12 +48,20 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('Visits', () => Visits);
bottle.decorator('Visits', connect([ 'settings' ], [ 'setVisitsSettings' ]));
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);
};
export default provideServices;

View file

@ -1,57 +1,45 @@
import { head, keys, pipe, values } from 'ramda';
import { pipe } from 'ramda';
import { FC, useEffect, useMemo, useState } from 'react';
import { RouteComponentProps } from 'react-router';
import { Card } from 'reactstrap';
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 { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { Topics } from '../mercure/helpers/Topics';
import { TableOrderIcon } from '../utils/table/TableOrderIcon';
import { ShlinkShortUrlsListParams } from '../api/types';
import { DEFAULT_SHORT_URLS_ORDERING, Settings } from '../settings/reducers/settings';
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
import { OrderableFields, ShortUrlsListParams, SORTABLE_FIELDS } from './reducers/shortUrlsListParams';
import { ShortUrlsTableProps } from './ShortUrlsTable';
import Paginator from './Paginator';
import { ShortUrlListRouteParams, useShortUrlsQuery } from './helpers/hooks';
import { OrderableFields, ShortUrlsOrder, SORTABLE_FIELDS } from './data';
interface ShortUrlsListProps extends RouteComponentProps<ShortUrlListRouteParams> {
selectedServer: SelectedServer;
shortUrlsList: ShortUrlsListState;
listShortUrls: (params: ShortUrlsListParams) => void;
shortUrlsListParams: ShortUrlsListParams;
resetShortUrlParams: () => void;
listShortUrls: (params: ShlinkShortUrlsListParams) => void;
settings: Settings;
}
type ShortUrlsOrder = Order<OrderableFields>;
const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>, SearchBar: FC) => boundToMercureHub(({
listShortUrls,
resetShortUrlParams,
shortUrlsListParams,
match,
location,
history,
shortUrlsList,
selectedServer,
settings,
}: ShortUrlsListProps) => {
const serverId = getServerId(selectedServer);
const { orderBy } = shortUrlsListParams;
const [ order, setOrder ] = useState<ShortUrlsOrder>({
field: orderBy && (head(keys(orderBy)) as OrderableFields),
dir: orderBy && head(values(orderBy)),
});
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 ]);
const { pagination } = shortUrlsList?.shortUrls ?? {};
const refreshList = (extraParams: ShlinkShortUrlsListParams) => listShortUrls(
{ ...shortUrlsListParams, ...extraParams },
);
const handleOrderBy = (field?: OrderableFields, dir?: OrderDir) => {
setOrder({ field, dir });
refreshList({ orderBy: field ? { [field]: dir } : undefined });
};
const handleOrderBy = (field?: OrderableFields, dir?: OrderDir) => setOrder({ field, dir });
const orderByColumn = (field: OrderableFields) => () =>
handleOrderBy(field, determineOrderDir(field, order.field, order.dir));
const renderOrderIcon = (field: OrderableFields) => <TableOrderIcon currentOrder={order} field={field} />;
@ -60,12 +48,17 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>, SearchBar: FC) =
(tags) => toFirstPage({ tags }),
);
useEffect(() => resetShortUrlParams, []);
useEffect(() => {
refreshList(
{ page: match.params.page, searchTerm: search, tags: selectedTags, itemsPerPage: undefined, startDate, endDate },
);
}, [ match.params.page, search, selectedTags, startDate, endDate ]);
listShortUrls({
page: match.params.page,
searchTerm: search,
tags: selectedTags,
itemsPerPage: undefined,
startDate,
endDate,
orderBy: order,
});
}, [ match.params.page, search, selectedTags, startDate, endDate, order ]);
return (
<>

View file

@ -5,7 +5,7 @@ import { SelectedServer } from '../servers/data';
import { supportsShortUrlTitle } from '../utils/helpers/features';
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
import { ShortUrlsRowProps } from './helpers/ShortUrlsRow';
import { OrderableFields } from './reducers/shortUrlsListParams';
import { OrderableFields } from './data';
import './ShortUrlsTable.scss';
export interface ShortUrlsTableProps {

View file

@ -1,4 +1,5 @@
import { Nullable, OptionalString } from '../../utils/utils';
import { Order } from '../../utils/helpers/ordering';
export interface EditShortUrlData {
longUrl?: string;
@ -50,3 +51,15 @@ export interface ShortUrlIdentifier {
shortCode: string;
domain: OptionalString;
}
export const SORTABLE_FIELDS = {
dateCreated: 'Created at',
shortCode: 'Short URL',
longUrl: 'Long URL',
title: 'Title',
visits: 'Visits',
};
export type OrderableFields = keyof typeof SORTABLE_FIELDS;
export type ShortUrlsOrder = Order<OrderableFields>;

View file

@ -7,7 +7,6 @@ import { GetState } from '../../container/types';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import { ShlinkShortUrlsListParams, ShlinkShortUrlsResponse } from '../../api/types';
import { DeleteShortUrlAction, SHORT_URL_DELETED } from './shortUrlDeletion';
import { ShortUrlsListParams } from './shortUrlsListParams';
import { CREATE_SHORT_URL, CreateShortUrlAction } from './shortUrlCreation';
import { SHORT_URL_EDITED, ShortUrlEditedAction } from './shortUrlEdition';
@ -25,7 +24,6 @@ export interface ShortUrlsList {
export interface ListShortUrlsAction extends Action<string> {
shortUrls: ShlinkShortUrlsResponse;
params: ShortUrlsListParams;
}
export type ListShortUrlsCombinedAction = (
@ -109,8 +107,8 @@ export const listShortUrls = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
try {
const shortUrls = await listShortUrls(params);
dispatch<ListShortUrlsAction>({ type: LIST_SHORT_URLS, shortUrls, params });
dispatch<ListShortUrlsAction>({ type: LIST_SHORT_URLS, shortUrls });
} catch (e) {
dispatch({ type: LIST_SHORT_URLS_ERROR, params });
dispatch({ type: LIST_SHORT_URLS_ERROR });
}
};

View file

@ -1,35 +0,0 @@
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
import { OrderDir } from '../../utils/helpers/ordering';
import { LIST_SHORT_URLS, ListShortUrlsAction } from './shortUrlsList';
export const RESET_SHORT_URL_PARAMS = 'shlink/shortUrlsListParams/RESET_SHORT_URL_PARAMS';
export const SORTABLE_FIELDS = {
dateCreated: 'Created at',
shortCode: 'Short URL',
longUrl: 'Long URL',
title: 'Title',
visits: 'Visits',
};
export type OrderableFields = keyof typeof SORTABLE_FIELDS;
export type OrderBy = Partial<Record<OrderableFields, OrderDir>>;
export interface ShortUrlsListParams {
page?: string;
itemsPerPage?: number;
orderBy?: OrderBy;
}
const initialState: ShortUrlsListParams = {
page: '1',
orderBy: { dateCreated: 'DESC' },
};
export default buildReducer<ShortUrlsListParams, ListShortUrlsAction>({
[LIST_SHORT_URLS]: (state, { params }) => ({ ...state, ...params }),
[RESET_SHORT_URL_PARAMS]: () => initialState,
}, initialState);
export const resetShortUrlParams = buildActionCreator(RESET_SHORT_URL_PARAMS);

View file

@ -9,7 +9,6 @@ import CreateShortUrlResult from '../helpers/CreateShortUrlResult';
import { listShortUrls } from '../reducers/shortUrlsList';
import { createShortUrl, resetCreateShortUrl } from '../reducers/shortUrlCreation';
import { deleteShortUrl, resetDeleteShortUrl } from '../reducers/shortUrlDeletion';
import { resetShortUrlParams } from '../reducers/shortUrlsListParams';
import { editShortUrl } from '../reducers/shortUrlEdition';
import { ConnectDecorator } from '../../container/types';
import { ShortUrlsTable } from '../ShortUrlsTable';
@ -22,8 +21,8 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter:
// Components
bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsTable', 'SearchBar');
bottle.decorator('ShortUrlsList', connect(
[ 'selectedServer', 'shortUrlsListParams', 'mercureInfo', 'shortUrlsList' ],
[ 'listShortUrls', 'resetShortUrlParams', 'createNewVisits', 'loadMercureInfo' ],
[ 'selectedServer', 'mercureInfo', 'shortUrlsList', 'settings' ],
[ 'listShortUrls', 'createNewVisits', 'loadMercureInfo' ],
));
bottle.serviceFactory('ShortUrlsTable', ShortUrlsTable, 'ShortUrlsRow');
@ -56,7 +55,6 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter:
// Actions
bottle.serviceFactory('listShortUrls', listShortUrls, 'buildShlinkApiClient');
bottle.serviceFactory('resetShortUrlParams', () => resetShortUrlParams);
bottle.serviceFactory('createShortUrl', createShortUrl, 'buildShlinkApiClient');
bottle.serviceFactory('resetCreateShortUrl', () => resetCreateShortUrl);

View file

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

View file

@ -3,9 +3,8 @@ import { Mock } from 'ts-mockery';
import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient';
import { OptionalString } from '../../../src/utils/utils';
import { ShlinkDomain, ShlinkVisitsOverview } from '../../../src/api/types';
import { ShortUrl } from '../../../src/short-urls/data';
import { ShortUrl, ShortUrlsOrder } from '../../../src/short-urls/data';
import { Visit } from '../../../src/visits/types';
import { OrderDir } from '../../../src/utils/helpers/ordering';
describe('ShlinkApiClient', () => {
const createAxios = (data: AxiosRequestConfig) => (async () => Promise.resolve(data)) as unknown as AxiosInstance;
@ -33,9 +32,9 @@ describe('ShlinkApiClient', () => {
});
it.each([
[{ visits: 'DESC' as OrderDir }, 'visits-DESC' ],
[{ longUrl: 'ASC' as OrderDir }, 'longUrl-ASC' ],
[{ longUrl: undefined as OrderDir }, undefined ],
[ { field: 'visits', dir: 'DESC' } as ShortUrlsOrder, 'visits-DESC' ],
[ { field: 'longUrl', dir: 'ASC' } as ShortUrlsOrder, 'longUrl-ASC' ],
[ { field: 'longUrl', dir: undefined } as ShortUrlsOrder, undefined ],
])('parses orderBy in params', async (orderBy, expectedOrderBy) => {
const axiosSpy = createAxiosMock({
data: expectedList,

View file

@ -8,7 +8,6 @@ import reducer, {
MAX_FALLBACK_VERSION,
MIN_FALLBACK_VERSION,
} from '../../../src/servers/reducers/selectedServer';
import { RESET_SHORT_URL_PARAMS } from '../../../src/short-urls/reducers/shortUrlsListParams';
import { ShlinkState } from '../../../src/container/types';
import { NonReachableServer, NotFoundServer, RegularServer } from '../../../src/servers/data';
@ -62,10 +61,9 @@ describe('selectedServerReducer', () => {
await selectServer(buildApiClient, loadMercureInfo)(id)(dispatch, getState);
expect(dispatch).toHaveBeenCalledTimes(4);
expect(dispatch).toHaveBeenCalledTimes(3);
expect(dispatch).toHaveBeenNthCalledWith(1, { type: RESET_SELECTED_SERVER });
expect(dispatch).toHaveBeenNthCalledWith(2, { type: RESET_SHORT_URL_PARAMS });
expect(dispatch).toHaveBeenNthCalledWith(3, { type: SELECT_SERVER, selectedServer: expectedSelectedServer });
expect(dispatch).toHaveBeenNthCalledWith(2, { type: SELECT_SERVER, selectedServer: expectedSelectedServer });
expect(loadMercureInfo).toHaveBeenCalledTimes(1);
});
@ -89,7 +87,7 @@ describe('selectedServerReducer', () => {
await selectServer(buildApiClient, loadMercureInfo)(id)(dispatch, getState);
expect(apiClientMock.health).toHaveBeenCalled();
expect(dispatch).toHaveBeenNthCalledWith(3, { type: SELECT_SERVER, selectedServer: expectedSelectedServer });
expect(dispatch).toHaveBeenNthCalledWith(2, { type: SELECT_SERVER, selectedServer: expectedSelectedServer });
expect(loadMercureInfo).not.toHaveBeenCalled();
});
@ -102,7 +100,7 @@ describe('selectedServerReducer', () => {
expect(getState).toHaveBeenCalled();
expect(apiClientMock.health).not.toHaveBeenCalled();
expect(dispatch).toHaveBeenNthCalledWith(3, { type: SELECT_SERVER, selectedServer: expectedSelectedServer });
expect(dispatch).toHaveBeenNthCalledWith(2, { type: SELECT_SERVER, selectedServer: expectedSelectedServer });
expect(loadMercureInfo).not.toHaveBeenCalled();
});
});

View file

@ -4,7 +4,7 @@ import NoMenuLayout from '../../src/common/NoMenuLayout';
describe('<Settings />', () => {
const Component = () => null;
const Settings = createSettings(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(4);
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

@ -0,0 +1,81 @@
import { shallow, ShallowWrapper } from 'enzyme';
import { Mock } from 'ts-mockery';
import { FormGroup } from 'reactstrap';
import { Settings, TagsMode, TagsSettings } from '../../src/settings/reducers/settings';
import { TagsModeDropdown } from '../../src/tags/TagsModeDropdown';
import { Tags } from '../../src/settings/Tags';
import SortingDropdown from '../../src/utils/SortingDropdown';
import { TagsOrder } from '../../src/tags/data/TagsListChildrenProps';
describe('<Tags />', () => {
let wrapper: ShallowWrapper;
const setTagsSettings = jest.fn();
const createWrapper = (tags?: TagsSettings) => {
wrapper = shallow(<Tags settings={Mock.of<Settings>({ tags })} setTagsSettings={setTagsSettings} />);
return wrapper;
};
afterEach(() => wrapper?.unmount());
afterEach(jest.clearAllMocks);
it('renders expected amount of groups', () => {
const wrapper = createWrapper();
const groups = wrapper.find(FormGroup);
expect(groups).toHaveLength(2);
});
it.each([
[ undefined, 'cards' ],
[{}, 'cards' ],
[{ defaultMode: 'cards' as TagsMode }, 'cards' ],
[{ defaultMode: 'list' as TagsMode }, 'list' ],
])('shows expected tags displaying mode', (tags, expectedMode) => {
const wrapper = createWrapper(tags);
const dropdown = wrapper.find(TagsModeDropdown);
const small = wrapper.find('small');
expect(dropdown.prop('mode')).toEqual(expectedMode);
expect(small.html()).toContain(`Tags will be displayed as <b>${expectedMode}</b>.`);
});
it.each([
[ 'cards' as TagsMode ],
[ 'list' as TagsMode ],
])('invokes setTagsSettings when tags mode changes', (defaultMode) => {
const wrapper = createWrapper();
const dropdown = wrapper.find(TagsModeDropdown);
expect(setTagsSettings).not.toHaveBeenCalled();
dropdown.simulate('change', 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 } });
});
});

View file

@ -2,11 +2,10 @@ import { shallow, ShallowWrapper } from 'enzyme';
import { Mock } from 'ts-mockery';
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Settings, TagsMode, UiSettings } from '../../src/settings/reducers/settings';
import { Settings, UiSettings } from '../../src/settings/reducers/settings';
import { UserInterface } from '../../src/settings/UserInterface';
import ToggleSwitch from '../../src/utils/ToggleSwitch';
import { Theme } from '../../src/utils/theme';
import { TagsModeDropdown } from '../../src/tags/TagsModeDropdown';
describe('<UserInterface />', () => {
let wrapper: ShallowWrapper;
@ -53,30 +52,4 @@ describe('<UserInterface />', () => {
toggle.simulate('change', checked);
expect(setUiSettings).toHaveBeenCalledWith({ theme });
});
it.each([
[ undefined, 'cards' ],
[{ theme: 'light' as Theme }, 'cards' ],
[{ theme: 'light' as Theme, tagsMode: 'cards' as TagsMode }, 'cards' ],
[{ theme: 'light' as Theme, tagsMode: 'list' as TagsMode }, 'list' ],
])('shows expected tags displaying mode', (ui, expectedMode) => {
const wrapper = createWrapper(ui);
const dropdown = wrapper.find(TagsModeDropdown);
const small = wrapper.find('small');
expect(dropdown.prop('mode')).toEqual(expectedMode);
expect(small.html()).toContain(`Tags will be displayed as <b>${expectedMode}</b>.`);
});
it.each([
[ 'cards' as TagsMode ],
[ 'list' as TagsMode ],
])('invokes setUiSettings when tags mode changes', (tagsMode) => {
const wrapper = createWrapper();
const dropdown = wrapper.find(TagsModeDropdown);
expect(setUiSettings).not.toHaveBeenCalled();
dropdown.simulate('change', tagsMode);
expect(setUiSettings).toHaveBeenCalledWith({ theme: 'light', tagsMode });
});
});

View file

@ -0,0 +1,31 @@
import { Mock } from 'ts-mockery';
import { migrateDeprecatedSettings } from '../../../src/settings/helpers';
import { ShlinkState } from '../../../src/container/types';
describe('settings-helpers', () => {
describe('migrateDeprecatedSettings', () => {
it('updates settings as expected', () => {
const state = Mock.of<ShlinkState>({
settings: {
visits: {
defaultInterval: 'last180days' as any,
},
ui: {
tagsMode: 'list',
} as any,
},
});
expect(migrateDeprecatedSettings(state)).toEqual(expect.objectContaining({
settings: expect.objectContaining({
visits: {
defaultInterval: 'last180Days',
},
tags: {
defaultMode: 'list',
},
}),
}));
});
});
});

View file

@ -1,10 +1,13 @@
import reducer, {
SET_SETTINGS,
DEFAULT_SHORT_URLS_ORDERING,
toggleRealTimeUpdates,
setRealTimeUpdatesInterval,
setShortUrlCreationSettings,
setUiSettings,
setVisitsSettings,
setTagsSettings,
setShortUrlsListSettings,
} from '../../../src/settings/reducers/settings';
describe('settingsReducer', () => {
@ -12,7 +15,8 @@ describe('settingsReducer', () => {
const shortUrlCreation = { validateUrls: false };
const ui = { theme: 'light' };
const visits = { defaultInterval: 'last30Days' };
const settings = { realTimeUpdates, shortUrlCreation, ui, visits };
const shortUrlsList = { defaultOrdering: DEFAULT_SHORT_URLS_ORDERING };
const settings = { realTimeUpdates, shortUrlCreation, ui, visits, shortUrlsList };
describe('reducer', () => {
it('returns realTimeUpdates when action is SET_SETTINGS', () => {
@ -59,4 +63,20 @@ describe('settingsReducer', () => {
expect(result).toEqual({ type: SET_SETTINGS, visits: { defaultInterval: 'last180Days' } });
});
});
describe('setTagsSettings', () => {
it('creates action to set tags settings', () => {
const result = setTagsSettings({ 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

@ -4,14 +4,14 @@ import { Mock } from 'ts-mockery';
import { History, Location } from 'history';
import { match } from 'react-router';
import shortUrlsListCreator from '../../src/short-urls/ShortUrlsList';
import { ShortUrl } from '../../src/short-urls/data';
import { OrderableFields, ShortUrl, ShortUrlsOrder } from '../../src/short-urls/data';
import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
import { ShortUrlsList as ShortUrlsListModel } from '../../src/short-urls/reducers/shortUrlsList';
import SortingDropdown from '../../src/utils/SortingDropdown';
import { OrderableFields, OrderBy } from '../../src/short-urls/reducers/shortUrlsListParams';
import Paginator from '../../src/short-urls/Paginator';
import { ReachableServer } from '../../src/servers/data';
import { ShortUrlListRouteParams } from '../../src/short-urls/helpers/hooks';
import { Settings } from '../../src/settings/reducers/settings';
describe('<ShortUrlsList />', () => {
let wrapper: ShallowWrapper;
@ -32,17 +32,16 @@ describe('<ShortUrlsList />', () => {
},
});
const ShortUrlsList = shortUrlsListCreator(ShortUrlsTable, SearchBar);
const createWrapper = (orderBy: OrderBy = {}) => shallow(
const createWrapper = (defaultOrdering: ShortUrlsOrder = {}) => shallow(
<ShortUrlsList
{...Mock.of<MercureBoundProps>({ mercureInfo: { loading: true } })}
listShortUrls={listShortUrlsMock}
resetShortUrlParams={jest.fn()}
shortUrlsListParams={{ page: '1', orderBy }}
match={Mock.of<match<ShortUrlListRouteParams>>({ params: {} })}
location={Mock.of<Location>({ search: '?tags=test%20tag&search=example.com' })}
shortUrlsList={shortUrlsList}
history={Mock.of<History>({ push })}
selectedServer={Mock.of<ReachableServer>({ id: '1' })}
settings={Mock.of<Settings>({ shortUrlsList: { defaultOrdering } })}
/>,
).dive(); // Dive is needed as this component is wrapped in a HOC
@ -91,20 +90,16 @@ describe('<ShortUrlsList />', () => {
it('handles order through table', () => {
const orderByColumn: (field: OrderableFields) => Function = wrapper.find(ShortUrlsTable).prop('orderByColumn');
orderByColumn('visits')();
orderByColumn('title')();
orderByColumn('shortCode')();
expect(wrapper.find(SortingDropdown).prop('order')).toEqual({});
expect(listShortUrlsMock).toHaveBeenCalledTimes(3);
expect(listShortUrlsMock).toHaveBeenNthCalledWith(1, expect.objectContaining({
orderBy: { visits: 'ASC' },
}));
expect(listShortUrlsMock).toHaveBeenNthCalledWith(2, expect.objectContaining({
orderBy: { title: 'ASC' },
}));
expect(listShortUrlsMock).toHaveBeenNthCalledWith(3, expect.objectContaining({
orderBy: { shortCode: 'ASC' },
}));
orderByColumn('visits')();
expect(wrapper.find(SortingDropdown).prop('order')).toEqual({ field: 'visits', dir: 'ASC' });
orderByColumn('title')();
expect(wrapper.find(SortingDropdown).prop('order')).toEqual({ field: 'title', dir: 'ASC' });
orderByColumn('shortCode')();
expect(wrapper.find(SortingDropdown).prop('order')).toEqual({ field: 'shortCode', dir: 'ASC' });
});
it('handles order through dropdown', () => {
@ -118,21 +113,12 @@ describe('<ShortUrlsList />', () => {
wrapper.find(SortingDropdown).simulate('change', undefined, undefined);
expect(wrapper.find(SortingDropdown).prop('order')).toEqual({});
expect(listShortUrlsMock).toHaveBeenCalledTimes(3);
expect(listShortUrlsMock).toHaveBeenNthCalledWith(1, expect.objectContaining({
orderBy: { visits: 'ASC' },
}));
expect(listShortUrlsMock).toHaveBeenNthCalledWith(2, expect.objectContaining({
orderBy: { shortCode: 'DESC' },
}));
expect(listShortUrlsMock).toHaveBeenNthCalledWith(3, expect.objectContaining({ orderBy: undefined }));
});
it.each([
[ Mock.of<OrderBy>({ visits: 'ASC' }), 'visits', 'ASC' ],
[ Mock.of<OrderBy>({ title: 'DESC' }), 'title', 'DESC' ],
[ Mock.of<OrderBy>(), undefined, undefined ],
[ Mock.of<ShortUrlsOrder>({ field: 'visits', dir: 'ASC' }), 'visits', 'ASC' ],
[ Mock.of<ShortUrlsOrder>({ field: 'title', dir: 'DESC' }), 'title', 'DESC' ],
[ Mock.of<ShortUrlsOrder>(), undefined, undefined ],
])('has expected initial ordering', (initialOrderBy, field, dir) => {
const wrapper = createWrapper(initialOrderBy);

View file

@ -2,10 +2,10 @@ import { shallow, ShallowWrapper } from 'enzyme';
import { Mock } from 'ts-mockery';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { ShortUrlsTable as shortUrlsTableCreator } from '../../src/short-urls/ShortUrlsTable';
import { OrderableFields, SORTABLE_FIELDS } from '../../src/short-urls/reducers/shortUrlsListParams';
import { ShortUrlsList } from '../../src/short-urls/reducers/shortUrlsList';
import { ReachableServer, SelectedServer } from '../../src/servers/data';
import { SemVer } from '../../src/utils/helpers/version';
import { OrderableFields, SORTABLE_FIELDS } from '../../src/short-urls/data';
describe('<ShortUrlsTable />', () => {
let wrapper: ShallowWrapper;

View file

@ -175,7 +175,7 @@ describe('shortUrlsListReducer', () => {
expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch).toHaveBeenNthCalledWith(1, { type: LIST_SHORT_URLS_START });
expect(dispatch).toHaveBeenNthCalledWith(2, { type: LIST_SHORT_URLS, shortUrls: [], params: {} });
expect(dispatch).toHaveBeenNthCalledWith(2, { type: LIST_SHORT_URLS, shortUrls: [] });
expect(listShortUrlsMock).toHaveBeenCalledTimes(1);
});
@ -188,7 +188,7 @@ describe('shortUrlsListReducer', () => {
expect(dispatch).toHaveBeenCalledTimes(2);
expect(dispatch).toHaveBeenNthCalledWith(1, { type: LIST_SHORT_URLS_START });
expect(dispatch).toHaveBeenNthCalledWith(2, { type: LIST_SHORT_URLS_ERROR, params: {} });
expect(dispatch).toHaveBeenNthCalledWith(2, { type: LIST_SHORT_URLS_ERROR });
expect(listShortUrlsMock).toHaveBeenCalledTimes(1);
});

View file

@ -1,27 +0,0 @@
import reducer, {
RESET_SHORT_URL_PARAMS,
resetShortUrlParams,
} from '../../../src/short-urls/reducers/shortUrlsListParams';
import { LIST_SHORT_URLS } from '../../../src/short-urls/reducers/shortUrlsList';
describe('shortUrlsListParamsReducer', () => {
describe('reducer', () => {
it('returns params when action is LIST_SHORT_URLS', () =>
expect(reducer(undefined, { type: LIST_SHORT_URLS, params: { searchTerm: 'foo', page: '2' } } as any)).toEqual({
page: '2',
searchTerm: 'foo',
orderBy: { dateCreated: 'DESC' },
}));
it('returns default value when action is RESET_SHORT_URL_PARAMS', () =>
expect(reducer(undefined, { type: RESET_SHORT_URL_PARAMS } as any)).toEqual({
page: '1',
orderBy: { dateCreated: 'DESC' },
}));
});
describe('resetShortUrlParams', () => {
it('returns proper action', () =>
expect(resetShortUrlParams()).toEqual({ type: RESET_SHORT_URL_PARAMS }));
});
});