mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-11 02:37:22 +03:00
Merge pull request #544 from acelaya-forks/feature/ordering-settings
Feature/ordering settings
This commit is contained in:
commit
2bf5f276f5
36 changed files with 406 additions and 216 deletions
|
@ -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.
|
* [#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.
|
* [#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
|
### Changed
|
||||||
* [#534](https://github.com/shlinkio/shlink-web-client/pull/534) Updated axios.
|
* [#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
|
* [#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
|
### Deprecated
|
||||||
* *Nothing*
|
* *Nothing*
|
||||||
|
|
|
@ -24,12 +24,11 @@ const buildShlinkBaseUrl = (url: string, apiVersion: number) => url ? `${url}/re
|
||||||
const rejectNilProps = reject(isNil);
|
const rejectNilProps = reject(isNil);
|
||||||
const normalizeOrderByInParams = (params: ShlinkShortUrlsListParams): ShlinkShortUrlsListNormalizedParams => {
|
const normalizeOrderByInParams = (params: ShlinkShortUrlsListParams): ShlinkShortUrlsListNormalizedParams => {
|
||||||
const { orderBy = {}, ...rest } = params;
|
const { orderBy = {}, ...rest } = params;
|
||||||
const [ firstKey ] = Object.keys(orderBy);
|
const { field, dir } = orderBy;
|
||||||
const [ firstValue ] = Object.values(orderBy);
|
|
||||||
|
|
||||||
return !firstValue ? rest : {
|
return !dir ? rest : {
|
||||||
...rest,
|
...rest,
|
||||||
orderBy: `${firstKey}-${firstValue}`,
|
orderBy: `${field}-${dir}`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { Visit } from '../../visits/types';
|
import { Visit } from '../../visits/types';
|
||||||
import { OptionalString } from '../../utils/utils';
|
import { OptionalString } from '../../utils/utils';
|
||||||
import { ShortUrl, ShortUrlMeta } from '../../short-urls/data';
|
import { ShortUrl, ShortUrlMeta, ShortUrlsOrder } from '../../short-urls/data';
|
||||||
import { OrderBy } from '../../short-urls/reducers/shortUrlsListParams';
|
|
||||||
|
|
||||||
export interface ShlinkShortUrlsResponse {
|
export interface ShlinkShortUrlsResponse {
|
||||||
data: ShortUrl[];
|
data: ShortUrl[];
|
||||||
|
@ -94,7 +93,7 @@ export interface ShlinkShortUrlsListParams {
|
||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
startDate?: string;
|
startDate?: string;
|
||||||
endDate?: string;
|
endDate?: string;
|
||||||
orderBy?: OrderBy;
|
orderBy?: ShortUrlsOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShlinkShortUrlsListNormalizedParams extends Omit<ShlinkShortUrlsListParams, 'orderBy'> {
|
export interface ShlinkShortUrlsListNormalizedParams extends Omit<ShlinkShortUrlsListParams, 'orderBy'> {
|
||||||
|
|
|
@ -18,7 +18,8 @@ import { ConnectDecorator } from './types';
|
||||||
type LazyActionMap = Record<string, Function>;
|
type LazyActionMap = Record<string, Function>;
|
||||||
|
|
||||||
const bottle = new Bottle();
|
const bottle = new Bottle();
|
||||||
const { container } = bottle;
|
|
||||||
|
export const { container } = bottle;
|
||||||
|
|
||||||
const lazyService = <T extends Function, K>(container: IContainer, serviceName: string) =>
|
const lazyService = <T extends Function, K>(container: IContainer, serviceName: string) =>
|
||||||
(...args: any[]) => (container[serviceName] as T)(...args) as K;
|
(...args: any[]) => (container[serviceName] as T)(...args) as K;
|
||||||
|
@ -44,5 +45,3 @@ provideUtilsServices(bottle);
|
||||||
provideMercureServices(bottle);
|
provideMercureServices(bottle);
|
||||||
provideSettingsServices(bottle, connect);
|
provideSettingsServices(bottle, connect);
|
||||||
provideDomainsServices(bottle, connect);
|
provideDomainsServices(bottle, connect);
|
||||||
|
|
||||||
export default container;
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ import ReduxThunk from 'redux-thunk';
|
||||||
import { applyMiddleware, compose, createStore } from 'redux';
|
import { applyMiddleware, compose, createStore } from 'redux';
|
||||||
import { save, load, RLSOptions } from 'redux-localstorage-simple';
|
import { save, load, RLSOptions } from 'redux-localstorage-simple';
|
||||||
import reducers from '../reducers';
|
import reducers from '../reducers';
|
||||||
|
import { migrateDeprecatedSettings } from '../settings/helpers';
|
||||||
|
import { ShlinkState } from './types';
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV !== 'production';
|
const isProduction = process.env.NODE_ENV !== 'production';
|
||||||
const composeEnhancers: Function = !isProduction && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
const composeEnhancers: Function = !isProduction && (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||||
|
@ -12,9 +14,8 @@ const localStorageConfig: RLSOptions = {
|
||||||
namespaceSeparator: '.',
|
namespaceSeparator: '.',
|
||||||
debounce: 300,
|
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),
|
applyMiddleware(save(localStorageConfig), ReduxThunk),
|
||||||
));
|
));
|
||||||
|
|
||||||
export default store;
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { Settings } from '../settings/reducers/settings';
|
||||||
import { ShortUrlCreation } from '../short-urls/reducers/shortUrlCreation';
|
import { ShortUrlCreation } from '../short-urls/reducers/shortUrlCreation';
|
||||||
import { ShortUrlDeletion } from '../short-urls/reducers/shortUrlDeletion';
|
import { ShortUrlDeletion } from '../short-urls/reducers/shortUrlDeletion';
|
||||||
import { ShortUrlEdition } from '../short-urls/reducers/shortUrlEdition';
|
import { ShortUrlEdition } from '../short-urls/reducers/shortUrlEdition';
|
||||||
import { ShortUrlsListParams } from '../short-urls/reducers/shortUrlsListParams';
|
|
||||||
import { ShortUrlsList } from '../short-urls/reducers/shortUrlsList';
|
import { ShortUrlsList } from '../short-urls/reducers/shortUrlsList';
|
||||||
import { TagDeletion } from '../tags/reducers/tagDelete';
|
import { TagDeletion } from '../tags/reducers/tagDelete';
|
||||||
import { TagEdition } from '../tags/reducers/tagEdit';
|
import { TagEdition } from '../tags/reducers/tagEdit';
|
||||||
|
@ -20,7 +19,6 @@ export interface ShlinkState {
|
||||||
servers: ServersMap;
|
servers: ServersMap;
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
shortUrlsList: ShortUrlsList;
|
shortUrlsList: ShortUrlsList;
|
||||||
shortUrlsListParams: ShortUrlsListParams;
|
|
||||||
shortUrlCreationResult: ShortUrlCreation;
|
shortUrlCreationResult: ShortUrlCreation;
|
||||||
shortUrlDeletion: ShortUrlDeletion;
|
shortUrlDeletion: ShortUrlDeletion;
|
||||||
shortUrlEdition: ShortUrlEdition;
|
shortUrlEdition: ShortUrlEdition;
|
||||||
|
|
|
@ -2,8 +2,8 @@ import { render } from 'react-dom';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { homepage } from '../package.json';
|
import { homepage } from '../package.json';
|
||||||
import container from './container';
|
import { container } from './container';
|
||||||
import store from './container/store';
|
import { store } from './container/store';
|
||||||
import { fixLeafletIcons } from './utils/helpers/leaflet';
|
import { fixLeafletIcons } from './utils/helpers/leaflet';
|
||||||
import { register as registerServiceWorker } from './serviceWorkerRegistration';
|
import { register as registerServiceWorker } from './serviceWorkerRegistration';
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
import 'react-datepicker/dist/react-datepicker.css';
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { combineReducers } from 'redux';
|
||||||
import serversReducer from '../servers/reducers/servers';
|
import serversReducer from '../servers/reducers/servers';
|
||||||
import selectedServerReducer from '../servers/reducers/selectedServer';
|
import selectedServerReducer from '../servers/reducers/selectedServer';
|
||||||
import shortUrlsListReducer from '../short-urls/reducers/shortUrlsList';
|
import shortUrlsListReducer from '../short-urls/reducers/shortUrlsList';
|
||||||
import shortUrlsListParamsReducer from '../short-urls/reducers/shortUrlsListParams';
|
|
||||||
import shortUrlCreationReducer from '../short-urls/reducers/shortUrlCreation';
|
import shortUrlCreationReducer from '../short-urls/reducers/shortUrlCreation';
|
||||||
import shortUrlDeletionReducer from '../short-urls/reducers/shortUrlDeletion';
|
import shortUrlDeletionReducer from '../short-urls/reducers/shortUrlDeletion';
|
||||||
import shortUrlEditionReducer from '../short-urls/reducers/shortUrlEdition';
|
import shortUrlEditionReducer from '../short-urls/reducers/shortUrlEdition';
|
||||||
|
@ -24,7 +23,6 @@ export default combineReducers<ShlinkState>({
|
||||||
servers: serversReducer,
|
servers: serversReducer,
|
||||||
selectedServer: selectedServerReducer,
|
selectedServer: selectedServerReducer,
|
||||||
shortUrlsList: shortUrlsListReducer,
|
shortUrlsList: shortUrlsListReducer,
|
||||||
shortUrlsListParams: shortUrlsListParamsReducer,
|
|
||||||
shortUrlCreationResult: shortUrlCreationReducer,
|
shortUrlCreationResult: shortUrlCreationReducer,
|
||||||
shortUrlDeletion: shortUrlDeletionReducer,
|
shortUrlDeletion: shortUrlDeletionReducer,
|
||||||
shortUrlEdition: shortUrlEditionReducer,
|
shortUrlEdition: shortUrlEditionReducer,
|
||||||
|
|
|
@ -44,7 +44,7 @@ export const Overview = (
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
listShortUrls({ itemsPerPage: 5, orderBy: { dateCreated: 'DESC' } });
|
listShortUrls({ itemsPerPage: 5, orderBy: { field: 'dateCreated', dir: 'DESC' } });
|
||||||
listTags();
|
listTags();
|
||||||
loadVisitsOverview();
|
loadVisitsOverview();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { identity, memoizeWith, pipe } from 'ramda';
|
import { identity, memoizeWith, pipe } from 'ramda';
|
||||||
import { Action, Dispatch } from 'redux';
|
import { Action, Dispatch } from 'redux';
|
||||||
import { resetShortUrlParams } from '../../short-urls/reducers/shortUrlsListParams';
|
|
||||||
import { versionToPrintable, versionToSemVer as toSemVer } from '../../utils/helpers/version';
|
import { versionToPrintable, versionToSemVer as toSemVer } from '../../utils/helpers/version';
|
||||||
import { SelectedServer } from '../data';
|
import { SelectedServer } from '../data';
|
||||||
import { GetState } from '../../container/types';
|
import { GetState } from '../../container/types';
|
||||||
|
@ -53,7 +52,6 @@ export const selectServer = (
|
||||||
getState: GetState,
|
getState: GetState,
|
||||||
) => {
|
) => {
|
||||||
dispatch(resetSelectedServer());
|
dispatch(resetSelectedServer());
|
||||||
dispatch(resetShortUrlParams());
|
|
||||||
|
|
||||||
const { servers } = getState();
|
const { servers } = getState();
|
||||||
const selectedServer = servers[serverId];
|
const selectedServer = servers[serverId];
|
||||||
|
|
|
@ -7,7 +7,7 @@ const SettingsSections: FC<{ items: ReactNode[][] }> = ({ items }) => (
|
||||||
{items.map((child, index) => (
|
{items.map((child, index) => (
|
||||||
<Row key={index}>
|
<Row key={index}>
|
||||||
{child.map((subChild, subIndex) => (
|
{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}
|
{subChild}
|
||||||
</div>
|
</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>
|
<NoMenuLayout>
|
||||||
<SettingsSections
|
<SettingsSections
|
||||||
items={[
|
items={[
|
||||||
[ <UserInterface />, <Visits /> ], // 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 />, <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>
|
||||||
|
);
|
35
src/settings/Tags.tsx
Normal file
35
src/settings/Tags.tsx
Normal 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>
|
||||||
|
);
|
|
@ -5,8 +5,6 @@ import { FormGroup } from 'reactstrap';
|
||||||
import { SimpleCard } from '../utils/SimpleCard';
|
import { SimpleCard } from '../utils/SimpleCard';
|
||||||
import ToggleSwitch from '../utils/ToggleSwitch';
|
import ToggleSwitch from '../utils/ToggleSwitch';
|
||||||
import { changeThemeInMarkup, Theme } from '../utils/theme';
|
import { changeThemeInMarkup, Theme } from '../utils/theme';
|
||||||
import { TagsModeDropdown } from '../tags/TagsModeDropdown';
|
|
||||||
import { capitalize } from '../utils/utils';
|
|
||||||
import { Settings, UiSettings } from './reducers/settings';
|
import { Settings, UiSettings } from './reducers/settings';
|
||||||
import './UserInterface.scss';
|
import './UserInterface.scss';
|
||||||
|
|
||||||
|
@ -31,14 +29,5 @@ export const UserInterface: FC<UserInterfaceProps> = ({ settings: { ui }, setUiS
|
||||||
Use dark theme.
|
Use dark theme.
|
||||||
</ToggleSwitch>
|
</ToggleSwitch>
|
||||||
</FormGroup>
|
</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>
|
</SimpleCard>
|
||||||
);
|
);
|
||||||
|
|
17
src/settings/helpers/index.ts
Normal file
17
src/settings/helpers/index.ts
Normal 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;
|
||||||
|
};
|
|
@ -4,9 +4,16 @@ import { buildReducer } from '../../utils/helpers/redux';
|
||||||
import { RecursivePartial } from '../../utils/utils';
|
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 { ShortUrlsOrder } from '../../short-urls/data';
|
||||||
|
|
||||||
export const SET_SETTINGS = 'shlink/realTimeUpdates/SET_SETTINGS';
|
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
|
* 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.
|
* 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 {
|
export interface UiSettings {
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
tagsMode?: TagsMode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VisitsSettings {
|
export interface VisitsSettings {
|
||||||
defaultInterval: DateInterval;
|
defaultInterval: DateInterval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TagsSettings {
|
||||||
|
defaultOrdering?: TagsOrder;
|
||||||
|
defaultMode?: TagsMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShortUrlsListSettings {
|
||||||
|
defaultOrdering?: ShortUrlsOrder;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
realTimeUpdates: RealTimeUpdatesSettings;
|
realTimeUpdates: RealTimeUpdatesSettings;
|
||||||
shortUrlCreation?: ShortUrlCreationSettings;
|
shortUrlCreation?: ShortUrlCreationSettings;
|
||||||
|
shortUrlsList?: ShortUrlsListSettings;
|
||||||
ui?: UiSettings;
|
ui?: UiSettings;
|
||||||
visits?: VisitsSettings;
|
visits?: VisitsSettings;
|
||||||
|
tags?: TagsSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: Settings = {
|
const initialState: Settings = {
|
||||||
|
@ -56,6 +73,9 @@ const initialState: Settings = {
|
||||||
visits: {
|
visits: {
|
||||||
defaultInterval: 'last30Days',
|
defaultInterval: 'last30Days',
|
||||||
},
|
},
|
||||||
|
shortUrlsList: {
|
||||||
|
defaultOrdering: DEFAULT_SHORT_URLS_ORDERING,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type SettingsAction = Action & Settings;
|
type SettingsAction = Action & Settings;
|
||||||
|
@ -81,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,
|
||||||
|
@ -90,3 +115,8 @@ export const setVisitsSettings = (settings: VisitsSettings): PartialSettingsActi
|
||||||
type: SET_SETTINGS,
|
type: SET_SETTINGS,
|
||||||
visits: settings,
|
visits: settings,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const setTagsSettings = (settings: TagsSettings): PartialSettingsAction => ({
|
||||||
|
type: SET_SETTINGS,
|
||||||
|
tags: settings,
|
||||||
|
});
|
||||||
|
|
|
@ -4,6 +4,8 @@ import Settings from '../Settings';
|
||||||
import {
|
import {
|
||||||
setRealTimeUpdatesInterval,
|
setRealTimeUpdatesInterval,
|
||||||
setShortUrlCreationSettings,
|
setShortUrlCreationSettings,
|
||||||
|
setShortUrlsListSettings,
|
||||||
|
setTagsSettings,
|
||||||
setUiSettings,
|
setUiSettings,
|
||||||
setVisitsSettings,
|
setVisitsSettings,
|
||||||
toggleRealTimeUpdates,
|
toggleRealTimeUpdates,
|
||||||
|
@ -13,10 +15,21 @@ import { withoutSelectedServer } from '../../servers/helpers/withoutSelectedServ
|
||||||
import { ShortUrlCreation } from '../ShortUrlCreation';
|
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 { 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');
|
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' ]));
|
||||||
|
|
||||||
|
@ -35,12 +48,20 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
bottle.serviceFactory('Visits', () => Visits);
|
bottle.serviceFactory('Visits', () => Visits);
|
||||||
bottle.decorator('Visits', connect([ 'settings' ], [ 'setVisitsSettings' ]));
|
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
|
// 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default provideServices;
|
export default provideServices;
|
||||||
|
|
|
@ -1,57 +1,45 @@
|
||||||
import { head, keys, pipe, values } from 'ramda';
|
import { pipe } from 'ramda';
|
||||||
import { FC, useEffect, useMemo, useState } from 'react';
|
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 { DEFAULT_SHORT_URLS_ORDERING, Settings } from '../settings/reducers/settings';
|
||||||
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
|
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
|
||||||
import { OrderableFields, ShortUrlsListParams, 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';
|
||||||
|
import { OrderableFields, ShortUrlsOrder, SORTABLE_FIELDS } from './data';
|
||||||
|
|
||||||
interface ShortUrlsListProps extends RouteComponentProps<ShortUrlListRouteParams> {
|
interface ShortUrlsListProps extends RouteComponentProps<ShortUrlListRouteParams> {
|
||||||
selectedServer: SelectedServer;
|
selectedServer: SelectedServer;
|
||||||
shortUrlsList: ShortUrlsListState;
|
shortUrlsList: ShortUrlsListState;
|
||||||
listShortUrls: (params: ShortUrlsListParams) => void;
|
listShortUrls: (params: ShlinkShortUrlsListParams) => void;
|
||||||
shortUrlsListParams: ShortUrlsListParams;
|
settings: Settings;
|
||||||
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,
|
|
||||||
shortUrlsListParams,
|
|
||||||
match,
|
match,
|
||||||
location,
|
location,
|
||||||
history,
|
history,
|
||||||
shortUrlsList,
|
shortUrlsList,
|
||||||
selectedServer,
|
selectedServer,
|
||||||
|
settings,
|
||||||
}: ShortUrlsListProps) => {
|
}: ShortUrlsListProps) => {
|
||||||
const serverId = getServerId(selectedServer);
|
const serverId = getServerId(selectedServer);
|
||||||
const { orderBy } = shortUrlsListParams;
|
const initialOrderBy = settings.shortUrlsList?.defaultOrdering ?? DEFAULT_SHORT_URLS_ORDERING;
|
||||||
const [ order, setOrder ] = useState<ShortUrlsOrder>({
|
const [ order, setOrder ] = useState<ShortUrlsOrder>(initialOrderBy);
|
||||||
field: orderBy && (head(keys(orderBy)) as OrderableFields),
|
|
||||||
dir: orderBy && head(values(orderBy)),
|
|
||||||
});
|
|
||||||
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 ]);
|
||||||
const { pagination } = shortUrlsList?.shortUrls ?? {};
|
const { pagination } = shortUrlsList?.shortUrls ?? {};
|
||||||
|
|
||||||
const refreshList = (extraParams: ShlinkShortUrlsListParams) => listShortUrls(
|
const handleOrderBy = (field?: OrderableFields, dir?: OrderDir) => setOrder({ field, dir });
|
||||||
{ ...shortUrlsListParams, ...extraParams },
|
|
||||||
);
|
|
||||||
const handleOrderBy = (field?: OrderableFields, dir?: OrderDir) => {
|
|
||||||
setOrder({ field, dir });
|
|
||||||
refreshList({ orderBy: field ? { [field]: dir } : undefined });
|
|
||||||
};
|
|
||||||
const orderByColumn = (field: OrderableFields) => () =>
|
const orderByColumn = (field: OrderableFields) => () =>
|
||||||
handleOrderBy(field, determineOrderDir(field, order.field, order.dir));
|
handleOrderBy(field, determineOrderDir(field, order.field, order.dir));
|
||||||
const renderOrderIcon = (field: OrderableFields) => <TableOrderIcon currentOrder={order} field={field} />;
|
const renderOrderIcon = (field: OrderableFields) => <TableOrderIcon currentOrder={order} field={field} />;
|
||||||
|
@ -60,12 +48,17 @@ const ShortUrlsList = (ShortUrlsTable: FC<ShortUrlsTableProps>, SearchBar: FC) =
|
||||||
(tags) => toFirstPage({ tags }),
|
(tags) => toFirstPage({ tags }),
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => resetShortUrlParams, []);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshList(
|
listShortUrls({
|
||||||
{ page: match.params.page, searchTerm: search, tags: selectedTags, itemsPerPage: undefined, startDate, endDate },
|
page: match.params.page,
|
||||||
);
|
searchTerm: search,
|
||||||
}, [ match.params.page, search, selectedTags, startDate, endDate ]);
|
tags: selectedTags,
|
||||||
|
itemsPerPage: undefined,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
orderBy: order,
|
||||||
|
});
|
||||||
|
}, [ match.params.page, search, selectedTags, startDate, endDate, order ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { SelectedServer } from '../servers/data';
|
||||||
import { supportsShortUrlTitle } from '../utils/helpers/features';
|
import { supportsShortUrlTitle } from '../utils/helpers/features';
|
||||||
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
|
import { ShortUrlsList as ShortUrlsListState } from './reducers/shortUrlsList';
|
||||||
import { ShortUrlsRowProps } from './helpers/ShortUrlsRow';
|
import { ShortUrlsRowProps } from './helpers/ShortUrlsRow';
|
||||||
import { OrderableFields } from './reducers/shortUrlsListParams';
|
import { OrderableFields } from './data';
|
||||||
import './ShortUrlsTable.scss';
|
import './ShortUrlsTable.scss';
|
||||||
|
|
||||||
export interface ShortUrlsTableProps {
|
export interface ShortUrlsTableProps {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Nullable, OptionalString } from '../../utils/utils';
|
import { Nullable, OptionalString } from '../../utils/utils';
|
||||||
|
import { Order } from '../../utils/helpers/ordering';
|
||||||
|
|
||||||
export interface EditShortUrlData {
|
export interface EditShortUrlData {
|
||||||
longUrl?: string;
|
longUrl?: string;
|
||||||
|
@ -50,3 +51,15 @@ export interface ShortUrlIdentifier {
|
||||||
shortCode: string;
|
shortCode: string;
|
||||||
domain: OptionalString;
|
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>;
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { GetState } from '../../container/types';
|
||||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||||
import { ShlinkShortUrlsListParams, ShlinkShortUrlsResponse } from '../../api/types';
|
import { ShlinkShortUrlsListParams, ShlinkShortUrlsResponse } from '../../api/types';
|
||||||
import { DeleteShortUrlAction, SHORT_URL_DELETED } from './shortUrlDeletion';
|
import { DeleteShortUrlAction, SHORT_URL_DELETED } from './shortUrlDeletion';
|
||||||
import { ShortUrlsListParams } from './shortUrlsListParams';
|
|
||||||
import { CREATE_SHORT_URL, CreateShortUrlAction } from './shortUrlCreation';
|
import { CREATE_SHORT_URL, CreateShortUrlAction } from './shortUrlCreation';
|
||||||
import { SHORT_URL_EDITED, ShortUrlEditedAction } from './shortUrlEdition';
|
import { SHORT_URL_EDITED, ShortUrlEditedAction } from './shortUrlEdition';
|
||||||
|
|
||||||
|
@ -25,7 +24,6 @@ export interface ShortUrlsList {
|
||||||
|
|
||||||
export interface ListShortUrlsAction extends Action<string> {
|
export interface ListShortUrlsAction extends Action<string> {
|
||||||
shortUrls: ShlinkShortUrlsResponse;
|
shortUrls: ShlinkShortUrlsResponse;
|
||||||
params: ShortUrlsListParams;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ListShortUrlsCombinedAction = (
|
export type ListShortUrlsCombinedAction = (
|
||||||
|
@ -109,8 +107,8 @@ export const listShortUrls = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
|
||||||
try {
|
try {
|
||||||
const shortUrls = await listShortUrls(params);
|
const shortUrls = await listShortUrls(params);
|
||||||
|
|
||||||
dispatch<ListShortUrlsAction>({ type: LIST_SHORT_URLS, shortUrls, params });
|
dispatch<ListShortUrlsAction>({ type: LIST_SHORT_URLS, shortUrls });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch({ type: LIST_SHORT_URLS_ERROR, params });
|
dispatch({ type: LIST_SHORT_URLS_ERROR });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
|
|
@ -9,7 +9,6 @@ import CreateShortUrlResult from '../helpers/CreateShortUrlResult';
|
||||||
import { listShortUrls } from '../reducers/shortUrlsList';
|
import { listShortUrls } from '../reducers/shortUrlsList';
|
||||||
import { createShortUrl, resetCreateShortUrl } from '../reducers/shortUrlCreation';
|
import { createShortUrl, resetCreateShortUrl } from '../reducers/shortUrlCreation';
|
||||||
import { deleteShortUrl, resetDeleteShortUrl } from '../reducers/shortUrlDeletion';
|
import { deleteShortUrl, resetDeleteShortUrl } from '../reducers/shortUrlDeletion';
|
||||||
import { resetShortUrlParams } from '../reducers/shortUrlsListParams';
|
|
||||||
import { editShortUrl } from '../reducers/shortUrlEdition';
|
import { editShortUrl } from '../reducers/shortUrlEdition';
|
||||||
import { ConnectDecorator } from '../../container/types';
|
import { ConnectDecorator } from '../../container/types';
|
||||||
import { ShortUrlsTable } from '../ShortUrlsTable';
|
import { ShortUrlsTable } from '../ShortUrlsTable';
|
||||||
|
@ -22,8 +21,8 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter:
|
||||||
// Components
|
// Components
|
||||||
bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsTable', 'SearchBar');
|
bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsTable', 'SearchBar');
|
||||||
bottle.decorator('ShortUrlsList', connect(
|
bottle.decorator('ShortUrlsList', connect(
|
||||||
[ 'selectedServer', 'shortUrlsListParams', 'mercureInfo', 'shortUrlsList' ],
|
[ 'selectedServer', 'mercureInfo', 'shortUrlsList', 'settings' ],
|
||||||
[ 'listShortUrls', 'resetShortUrlParams', 'createNewVisits', 'loadMercureInfo' ],
|
[ 'listShortUrls', 'createNewVisits', 'loadMercureInfo' ],
|
||||||
));
|
));
|
||||||
|
|
||||||
bottle.serviceFactory('ShortUrlsTable', ShortUrlsTable, 'ShortUrlsRow');
|
bottle.serviceFactory('ShortUrlsTable', ShortUrlsTable, 'ShortUrlsRow');
|
||||||
|
@ -56,7 +55,6 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator, withRouter:
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
bottle.serviceFactory('listShortUrls', listShortUrls, 'buildShlinkApiClient');
|
bottle.serviceFactory('listShortUrls', listShortUrls, 'buildShlinkApiClient');
|
||||||
bottle.serviceFactory('resetShortUrlParams', () => resetShortUrlParams);
|
|
||||||
|
|
||||||
bottle.serviceFactory('createShortUrl', createShortUrl, 'buildShlinkApiClient');
|
bottle.serviceFactory('createShortUrl', createShortUrl, 'buildShlinkApiClient');
|
||||||
bottle.serviceFactory('resetCreateShortUrl', () => resetCreateShortUrl);
|
bottle.serviceFactory('resetCreateShortUrl', () => resetCreateShortUrl);
|
||||||
|
|
|
@ -28,8 +28,8 @@ export interface TagsListProps {
|
||||||
const TagsList = (TagsCards: FC<TagsListChildrenProps>, TagsTable: FC<TagsTableProps>) => boundToMercureHub((
|
const TagsList = (TagsCards: FC<TagsListChildrenProps>, TagsTable: FC<TagsTableProps>) => boundToMercureHub((
|
||||||
{ filterTags, forceListTags, tagsList, selectedServer, settings }: TagsListProps,
|
{ filterTags, forceListTags, tagsList, selectedServer, settings }: TagsListProps,
|
||||||
) => {
|
) => {
|
||||||
const [ mode, setMode ] = useState<TagsMode>(settings.ui?.tagsMode ?? '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,
|
||||||
|
|
|
@ -3,9 +3,8 @@ import { Mock } from 'ts-mockery';
|
||||||
import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient';
|
import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient';
|
||||||
import { OptionalString } from '../../../src/utils/utils';
|
import { OptionalString } from '../../../src/utils/utils';
|
||||||
import { ShlinkDomain, ShlinkVisitsOverview } from '../../../src/api/types';
|
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 { Visit } from '../../../src/visits/types';
|
||||||
import { OrderDir } from '../../../src/utils/helpers/ordering';
|
|
||||||
|
|
||||||
describe('ShlinkApiClient', () => {
|
describe('ShlinkApiClient', () => {
|
||||||
const createAxios = (data: AxiosRequestConfig) => (async () => Promise.resolve(data)) as unknown as AxiosInstance;
|
const createAxios = (data: AxiosRequestConfig) => (async () => Promise.resolve(data)) as unknown as AxiosInstance;
|
||||||
|
@ -33,9 +32,9 @@ describe('ShlinkApiClient', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[{ visits: 'DESC' as OrderDir }, 'visits-DESC' ],
|
[ { field: 'visits', dir: 'DESC' } as ShortUrlsOrder, 'visits-DESC' ],
|
||||||
[{ longUrl: 'ASC' as OrderDir }, 'longUrl-ASC' ],
|
[ { field: 'longUrl', dir: 'ASC' } as ShortUrlsOrder, 'longUrl-ASC' ],
|
||||||
[{ longUrl: undefined as OrderDir }, undefined ],
|
[ { field: 'longUrl', dir: undefined } as ShortUrlsOrder, undefined ],
|
||||||
])('parses orderBy in params', async (orderBy, expectedOrderBy) => {
|
])('parses orderBy in params', async (orderBy, expectedOrderBy) => {
|
||||||
const axiosSpy = createAxiosMock({
|
const axiosSpy = createAxiosMock({
|
||||||
data: expectedList,
|
data: expectedList,
|
||||||
|
|
|
@ -8,7 +8,6 @@ import reducer, {
|
||||||
MAX_FALLBACK_VERSION,
|
MAX_FALLBACK_VERSION,
|
||||||
MIN_FALLBACK_VERSION,
|
MIN_FALLBACK_VERSION,
|
||||||
} from '../../../src/servers/reducers/selectedServer';
|
} from '../../../src/servers/reducers/selectedServer';
|
||||||
import { RESET_SHORT_URL_PARAMS } from '../../../src/short-urls/reducers/shortUrlsListParams';
|
|
||||||
import { ShlinkState } from '../../../src/container/types';
|
import { ShlinkState } from '../../../src/container/types';
|
||||||
import { NonReachableServer, NotFoundServer, RegularServer } from '../../../src/servers/data';
|
import { NonReachableServer, NotFoundServer, RegularServer } from '../../../src/servers/data';
|
||||||
|
|
||||||
|
@ -62,10 +61,9 @@ describe('selectedServerReducer', () => {
|
||||||
|
|
||||||
await selectServer(buildApiClient, loadMercureInfo)(id)(dispatch, getState);
|
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(1, { type: RESET_SELECTED_SERVER });
|
||||||
expect(dispatch).toHaveBeenNthCalledWith(2, { type: RESET_SHORT_URL_PARAMS });
|
expect(dispatch).toHaveBeenNthCalledWith(2, { type: SELECT_SERVER, selectedServer: expectedSelectedServer });
|
||||||
expect(dispatch).toHaveBeenNthCalledWith(3, { type: SELECT_SERVER, selectedServer: expectedSelectedServer });
|
|
||||||
expect(loadMercureInfo).toHaveBeenCalledTimes(1);
|
expect(loadMercureInfo).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -89,7 +87,7 @@ describe('selectedServerReducer', () => {
|
||||||
await selectServer(buildApiClient, loadMercureInfo)(id)(dispatch, getState);
|
await selectServer(buildApiClient, loadMercureInfo)(id)(dispatch, getState);
|
||||||
|
|
||||||
expect(apiClientMock.health).toHaveBeenCalled();
|
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();
|
expect(loadMercureInfo).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -102,7 +100,7 @@ describe('selectedServerReducer', () => {
|
||||||
|
|
||||||
expect(getState).toHaveBeenCalled();
|
expect(getState).toHaveBeenCalled();
|
||||||
expect(apiClientMock.health).not.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();
|
expect(loadMercureInfo).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
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(4);
|
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 } });
|
||||||
|
});
|
||||||
|
});
|
81
test/settings/Tags.test.tsx
Normal file
81
test/settings/Tags.test.tsx
Normal 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 } });
|
||||||
|
});
|
||||||
|
});
|
|
@ -2,11 +2,10 @@ import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons';
|
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
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 { UserInterface } from '../../src/settings/UserInterface';
|
||||||
import ToggleSwitch from '../../src/utils/ToggleSwitch';
|
import ToggleSwitch from '../../src/utils/ToggleSwitch';
|
||||||
import { Theme } from '../../src/utils/theme';
|
import { Theme } from '../../src/utils/theme';
|
||||||
import { TagsModeDropdown } from '../../src/tags/TagsModeDropdown';
|
|
||||||
|
|
||||||
describe('<UserInterface />', () => {
|
describe('<UserInterface />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
|
@ -53,30 +52,4 @@ describe('<UserInterface />', () => {
|
||||||
toggle.simulate('change', checked);
|
toggle.simulate('change', checked);
|
||||||
expect(setUiSettings).toHaveBeenCalledWith({ theme });
|
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 });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
31
test/settings/helpers/index.test.ts
Normal file
31
test/settings/helpers/index.test.ts
Normal 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',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,10 +1,13 @@
|
||||||
import reducer, {
|
import reducer, {
|
||||||
SET_SETTINGS,
|
SET_SETTINGS,
|
||||||
|
DEFAULT_SHORT_URLS_ORDERING,
|
||||||
toggleRealTimeUpdates,
|
toggleRealTimeUpdates,
|
||||||
setRealTimeUpdatesInterval,
|
setRealTimeUpdatesInterval,
|
||||||
setShortUrlCreationSettings,
|
setShortUrlCreationSettings,
|
||||||
setUiSettings,
|
setUiSettings,
|
||||||
setVisitsSettings,
|
setVisitsSettings,
|
||||||
|
setTagsSettings,
|
||||||
|
setShortUrlsListSettings,
|
||||||
} from '../../../src/settings/reducers/settings';
|
} from '../../../src/settings/reducers/settings';
|
||||||
|
|
||||||
describe('settingsReducer', () => {
|
describe('settingsReducer', () => {
|
||||||
|
@ -12,7 +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 settings = { realTimeUpdates, shortUrlCreation, ui, visits };
|
const shortUrlsList = { defaultOrdering: DEFAULT_SHORT_URLS_ORDERING };
|
||||||
|
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', () => {
|
||||||
|
@ -59,4 +63,20 @@ describe('settingsReducer', () => {
|
||||||
expect(result).toEqual({ type: SET_SETTINGS, visits: { defaultInterval: 'last180Days' } });
|
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 } });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,14 +4,14 @@ import { Mock } from 'ts-mockery';
|
||||||
import { History, Location } from 'history';
|
import { History, Location } from 'history';
|
||||||
import { match } from 'react-router';
|
import { match } from 'react-router';
|
||||||
import shortUrlsListCreator from '../../src/short-urls/ShortUrlsList';
|
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 { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
|
||||||
import { ShortUrlsList as ShortUrlsListModel } from '../../src/short-urls/reducers/shortUrlsList';
|
import { ShortUrlsList as ShortUrlsListModel } from '../../src/short-urls/reducers/shortUrlsList';
|
||||||
import SortingDropdown from '../../src/utils/SortingDropdown';
|
import SortingDropdown from '../../src/utils/SortingDropdown';
|
||||||
import { OrderableFields, OrderBy } from '../../src/short-urls/reducers/shortUrlsListParams';
|
|
||||||
import Paginator from '../../src/short-urls/Paginator';
|
import Paginator from '../../src/short-urls/Paginator';
|
||||||
import { ReachableServer } from '../../src/servers/data';
|
import { ReachableServer } from '../../src/servers/data';
|
||||||
import { ShortUrlListRouteParams } from '../../src/short-urls/helpers/hooks';
|
import { ShortUrlListRouteParams } from '../../src/short-urls/helpers/hooks';
|
||||||
|
import { Settings } from '../../src/settings/reducers/settings';
|
||||||
|
|
||||||
describe('<ShortUrlsList />', () => {
|
describe('<ShortUrlsList />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
|
@ -32,17 +32,16 @@ describe('<ShortUrlsList />', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const ShortUrlsList = shortUrlsListCreator(ShortUrlsTable, SearchBar);
|
const ShortUrlsList = shortUrlsListCreator(ShortUrlsTable, SearchBar);
|
||||||
const createWrapper = (orderBy: OrderBy = {}) => shallow(
|
const createWrapper = (defaultOrdering: ShortUrlsOrder = {}) => shallow(
|
||||||
<ShortUrlsList
|
<ShortUrlsList
|
||||||
{...Mock.of<MercureBoundProps>({ mercureInfo: { loading: true } })}
|
{...Mock.of<MercureBoundProps>({ mercureInfo: { loading: true } })}
|
||||||
listShortUrls={listShortUrlsMock}
|
listShortUrls={listShortUrlsMock}
|
||||||
resetShortUrlParams={jest.fn()}
|
|
||||||
shortUrlsListParams={{ page: '1', orderBy }}
|
|
||||||
match={Mock.of<match<ShortUrlListRouteParams>>({ params: {} })}
|
match={Mock.of<match<ShortUrlListRouteParams>>({ params: {} })}
|
||||||
location={Mock.of<Location>({ search: '?tags=test%20tag&search=example.com' })}
|
location={Mock.of<Location>({ search: '?tags=test%20tag&search=example.com' })}
|
||||||
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>({ 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
|
||||||
|
|
||||||
|
@ -91,20 +90,16 @@ describe('<ShortUrlsList />', () => {
|
||||||
it('handles order through table', () => {
|
it('handles order through table', () => {
|
||||||
const orderByColumn: (field: OrderableFields) => Function = wrapper.find(ShortUrlsTable).prop('orderByColumn');
|
const orderByColumn: (field: OrderableFields) => Function = wrapper.find(ShortUrlsTable).prop('orderByColumn');
|
||||||
|
|
||||||
orderByColumn('visits')();
|
expect(wrapper.find(SortingDropdown).prop('order')).toEqual({});
|
||||||
orderByColumn('title')();
|
|
||||||
orderByColumn('shortCode')();
|
|
||||||
|
|
||||||
expect(listShortUrlsMock).toHaveBeenCalledTimes(3);
|
orderByColumn('visits')();
|
||||||
expect(listShortUrlsMock).toHaveBeenNthCalledWith(1, expect.objectContaining({
|
expect(wrapper.find(SortingDropdown).prop('order')).toEqual({ field: 'visits', dir: 'ASC' });
|
||||||
orderBy: { visits: 'ASC' },
|
|
||||||
}));
|
orderByColumn('title')();
|
||||||
expect(listShortUrlsMock).toHaveBeenNthCalledWith(2, expect.objectContaining({
|
expect(wrapper.find(SortingDropdown).prop('order')).toEqual({ field: 'title', dir: 'ASC' });
|
||||||
orderBy: { title: 'ASC' },
|
|
||||||
}));
|
orderByColumn('shortCode')();
|
||||||
expect(listShortUrlsMock).toHaveBeenNthCalledWith(3, expect.objectContaining({
|
expect(wrapper.find(SortingDropdown).prop('order')).toEqual({ field: 'shortCode', dir: 'ASC' });
|
||||||
orderBy: { shortCode: 'ASC' },
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles order through dropdown', () => {
|
it('handles order through dropdown', () => {
|
||||||
|
@ -118,21 +113,12 @@ describe('<ShortUrlsList />', () => {
|
||||||
|
|
||||||
wrapper.find(SortingDropdown).simulate('change', undefined, undefined);
|
wrapper.find(SortingDropdown).simulate('change', undefined, undefined);
|
||||||
expect(wrapper.find(SortingDropdown).prop('order')).toEqual({});
|
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([
|
it.each([
|
||||||
[ Mock.of<OrderBy>({ visits: 'ASC' }), 'visits', 'ASC' ],
|
[ Mock.of<ShortUrlsOrder>({ field: 'visits', dir: 'ASC' }), 'visits', 'ASC' ],
|
||||||
[ Mock.of<OrderBy>({ title: 'DESC' }), 'title', 'DESC' ],
|
[ Mock.of<ShortUrlsOrder>({ field: 'title', dir: 'DESC' }), 'title', 'DESC' ],
|
||||||
[ Mock.of<OrderBy>(), undefined, undefined ],
|
[ Mock.of<ShortUrlsOrder>(), undefined, undefined ],
|
||||||
])('has expected initial ordering', (initialOrderBy, field, dir) => {
|
])('has expected initial ordering', (initialOrderBy, field, dir) => {
|
||||||
const wrapper = createWrapper(initialOrderBy);
|
const wrapper = createWrapper(initialOrderBy);
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,10 @@ import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { ShortUrlsTable as shortUrlsTableCreator } from '../../src/short-urls/ShortUrlsTable';
|
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 { ShortUrlsList } from '../../src/short-urls/reducers/shortUrlsList';
|
||||||
import { ReachableServer, SelectedServer } from '../../src/servers/data';
|
import { ReachableServer, SelectedServer } from '../../src/servers/data';
|
||||||
import { SemVer } from '../../src/utils/helpers/version';
|
import { SemVer } from '../../src/utils/helpers/version';
|
||||||
|
import { OrderableFields, SORTABLE_FIELDS } from '../../src/short-urls/data';
|
||||||
|
|
||||||
describe('<ShortUrlsTable />', () => {
|
describe('<ShortUrlsTable />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
|
|
|
@ -175,7 +175,7 @@ describe('shortUrlsListReducer', () => {
|
||||||
|
|
||||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||||
expect(dispatch).toHaveBeenNthCalledWith(1, { type: LIST_SHORT_URLS_START });
|
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);
|
expect(listShortUrlsMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
@ -188,7 +188,7 @@ describe('shortUrlsListReducer', () => {
|
||||||
|
|
||||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||||
expect(dispatch).toHaveBeenNthCalledWith(1, { type: LIST_SHORT_URLS_START });
|
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);
|
expect(listShortUrlsMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 }));
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Reference in a new issue