mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-03 14:57:22 +03:00
Migrated tags reducers to typescripts
This commit is contained in:
parent
83531666de
commit
eb3775859a
10 changed files with 246 additions and 160 deletions
|
@ -69,7 +69,8 @@
|
||||||
"ignoreComments": true
|
"ignoreComments": true
|
||||||
}],
|
}],
|
||||||
"no-mixed-operators": "off",
|
"no-mixed-operators": "off",
|
||||||
"react/display-name": "off"
|
"react/display-name": "off",
|
||||||
|
"@typescript-eslint/require-array-sort-compare": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -9,6 +9,9 @@ import { ShortUrlEdition } from '../short-urls/reducers/shortUrlEdition';
|
||||||
import { ShortUrlsListParams } from '../short-urls/reducers/shortUrlsListParams';
|
import { ShortUrlsListParams } from '../short-urls/reducers/shortUrlsListParams';
|
||||||
import { ShortUrlTags } from '../short-urls/reducers/shortUrlTags';
|
import { ShortUrlTags } from '../short-urls/reducers/shortUrlTags';
|
||||||
import { ShortUrlsList } from '../short-urls/reducers/shortUrlsList';
|
import { ShortUrlsList } from '../short-urls/reducers/shortUrlsList';
|
||||||
|
import { TagDeletion } from '../tags/reducers/tagDelete';
|
||||||
|
import { TagEdition } from '../tags/reducers/tagEdit';
|
||||||
|
import { TagsList } from '../tags/reducers/tagsList';
|
||||||
|
|
||||||
export interface ShlinkState {
|
export interface ShlinkState {
|
||||||
servers: ServersMap;
|
servers: ServersMap;
|
||||||
|
@ -23,9 +26,9 @@ export interface ShlinkState {
|
||||||
shortUrlVisits: any;
|
shortUrlVisits: any;
|
||||||
tagVisits: any;
|
tagVisits: any;
|
||||||
shortUrlDetail: any;
|
shortUrlDetail: any;
|
||||||
tagsList: any;
|
tagsList: TagsList;
|
||||||
tagDelete: any;
|
tagDelete: TagDeletion;
|
||||||
tagEdit: any;
|
tagEdit: TagEdition;
|
||||||
mercureInfo: MercureInfo;
|
mercureInfo: MercureInfo;
|
||||||
settings: Settings;
|
settings: Settings;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import { handleActions } from 'redux-actions';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { Action, Dispatch } from 'redux';
|
||||||
|
import { buildReducer } from '../../utils/helpers/redux';
|
||||||
|
import { GetState } from '../../container/types';
|
||||||
|
import { ShlinkApiClientBuilder } from '../../utils/services/types';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
/* eslint-disable padding-line-between-statements */
|
||||||
export const DELETE_TAG_START = 'shlink/deleteTag/DELETE_TAG_START';
|
export const DELETE_TAG_START = 'shlink/deleteTag/DELETE_TAG_START';
|
||||||
|
@ -8,23 +11,36 @@ export const DELETE_TAG = 'shlink/deleteTag/DELETE_TAG';
|
||||||
export const TAG_DELETED = 'shlink/deleteTag/TAG_DELETED';
|
export const TAG_DELETED = 'shlink/deleteTag/TAG_DELETED';
|
||||||
/* eslint-enable padding-line-between-statements */
|
/* eslint-enable padding-line-between-statements */
|
||||||
|
|
||||||
|
/** @deprecated Use TagDeletion interface */
|
||||||
export const tagDeleteType = PropTypes.shape({
|
export const tagDeleteType = PropTypes.shape({
|
||||||
deleting: PropTypes.bool,
|
deleting: PropTypes.bool,
|
||||||
error: PropTypes.bool,
|
error: PropTypes.bool,
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialState = {
|
export interface TagDeletion {
|
||||||
|
deleting: boolean;
|
||||||
|
error: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteTagAction extends Action<string> {
|
||||||
|
tag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: TagDeletion = {
|
||||||
deleting: false,
|
deleting: false,
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handleActions({
|
export default buildReducer({
|
||||||
[DELETE_TAG_START]: () => ({ deleting: true, error: false }),
|
[DELETE_TAG_START]: () => ({ deleting: true, error: false }),
|
||||||
[DELETE_TAG_ERROR]: () => ({ deleting: false, error: true }),
|
[DELETE_TAG_ERROR]: () => ({ deleting: false, error: true }),
|
||||||
[DELETE_TAG]: () => ({ deleting: false, error: false }),
|
[DELETE_TAG]: () => ({ deleting: false, error: false }),
|
||||||
}, initialState);
|
}, initialState);
|
||||||
|
|
||||||
export const deleteTag = (buildShlinkApiClient) => (tag) => async (dispatch, getState) => {
|
export const deleteTag = (buildShlinkApiClient: ShlinkApiClientBuilder) => (tag: string) => async (
|
||||||
|
dispatch: Dispatch,
|
||||||
|
getState: GetState,
|
||||||
|
) => {
|
||||||
dispatch({ type: DELETE_TAG_START });
|
dispatch({ type: DELETE_TAG_START });
|
||||||
const { deleteTags } = buildShlinkApiClient(getState);
|
const { deleteTags } = buildShlinkApiClient(getState);
|
||||||
|
|
||||||
|
@ -38,4 +54,4 @@ export const deleteTag = (buildShlinkApiClient) => (tag) => async (dispatch, get
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const tagDeleted = (tag) => ({ type: TAG_DELETED, tag });
|
export const tagDeleted = (tag: string): DeleteTagAction => ({ type: TAG_DELETED, tag });
|
|
@ -1,5 +1,9 @@
|
||||||
import { pick } from 'ramda';
|
import { pick } from 'ramda';
|
||||||
import { handleActions } from 'redux-actions';
|
import { Action, Dispatch } from 'redux';
|
||||||
|
import { buildReducer } from '../../utils/helpers/redux';
|
||||||
|
import { GetState } from '../../container/types';
|
||||||
|
import { ShlinkApiClientBuilder } from '../../utils/services/types';
|
||||||
|
import ColorGenerator from '../../utils/services/ColorGenerator';
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
/* eslint-disable padding-line-between-statements */
|
||||||
export const EDIT_TAG_START = 'shlink/editTag/EDIT_TAG_START';
|
export const EDIT_TAG_START = 'shlink/editTag/EDIT_TAG_START';
|
||||||
|
@ -9,27 +13,41 @@ export const EDIT_TAG = 'shlink/editTag/EDIT_TAG';
|
||||||
|
|
||||||
export const TAG_EDITED = 'shlink/editTag/TAG_EDITED';
|
export const TAG_EDITED = 'shlink/editTag/TAG_EDITED';
|
||||||
|
|
||||||
const initialState = {
|
export interface TagEdition {
|
||||||
|
oldName: string;
|
||||||
|
newName: string;
|
||||||
|
editing: boolean;
|
||||||
|
error: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EditTagAction extends Action<string> {
|
||||||
|
oldName: string;
|
||||||
|
newName: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: TagEdition = {
|
||||||
oldName: '',
|
oldName: '',
|
||||||
newName: '',
|
newName: '',
|
||||||
editing: false,
|
editing: false,
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handleActions({
|
export default buildReducer<TagEdition, EditTagAction>({
|
||||||
[EDIT_TAG_START]: (state) => ({ ...state, editing: true, error: false }),
|
[EDIT_TAG_START]: (state) => ({ ...state, editing: true, error: false }),
|
||||||
[EDIT_TAG_ERROR]: (state) => ({ ...state, editing: false, error: true }),
|
[EDIT_TAG_ERROR]: (state) => ({ ...state, editing: false, error: true }),
|
||||||
[EDIT_TAG]: (state, action) => ({
|
[EDIT_TAG]: (_, action) => ({
|
||||||
...pick([ 'oldName', 'newName' ], action),
|
...pick([ 'oldName', 'newName' ], action),
|
||||||
editing: false,
|
editing: false,
|
||||||
error: false,
|
error: false,
|
||||||
}),
|
}),
|
||||||
}, initialState);
|
}, initialState);
|
||||||
|
|
||||||
export const editTag = (buildShlinkApiClient, colorGenerator) => (oldName, newName, color) => async (
|
export const editTag = (buildShlinkApiClient: ShlinkApiClientBuilder, colorGenerator: ColorGenerator) => (
|
||||||
dispatch,
|
oldName: string,
|
||||||
getState,
|
newName: string,
|
||||||
) => {
|
color: string,
|
||||||
|
) => async (dispatch: Dispatch, getState: GetState) => {
|
||||||
dispatch({ type: EDIT_TAG_START });
|
dispatch({ type: EDIT_TAG_START });
|
||||||
const { editTag } = buildShlinkApiClient(getState);
|
const { editTag } = buildShlinkApiClient(getState);
|
||||||
|
|
||||||
|
@ -44,7 +62,7 @@ export const editTag = (buildShlinkApiClient, colorGenerator) => (oldName, newNa
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const tagEdited = (oldName, newName, color) => ({
|
export const tagEdited = (oldName: string, newName: string, color: string): EditTagAction => ({
|
||||||
type: TAG_EDITED,
|
type: TAG_EDITED,
|
||||||
oldName,
|
oldName,
|
||||||
newName,
|
newName,
|
|
@ -1,99 +0,0 @@
|
||||||
import { handleActions } from 'redux-actions';
|
|
||||||
import { isEmpty, reject } from 'ramda';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { CREATE_VISIT } from '../../visits/reducers/visitCreation';
|
|
||||||
import { TAG_DELETED } from './tagDelete';
|
|
||||||
import { TAG_EDITED } from './tagEdit';
|
|
||||||
|
|
||||||
/* eslint-disable padding-line-between-statements */
|
|
||||||
export const LIST_TAGS_START = 'shlink/tagsList/LIST_TAGS_START';
|
|
||||||
export const LIST_TAGS_ERROR = 'shlink/tagsList/LIST_TAGS_ERROR';
|
|
||||||
export const LIST_TAGS = 'shlink/tagsList/LIST_TAGS';
|
|
||||||
export const FILTER_TAGS = 'shlink/tagsList/FILTER_TAGS';
|
|
||||||
/* eslint-enable padding-line-between-statements */
|
|
||||||
|
|
||||||
const TagStatsType = PropTypes.shape({
|
|
||||||
shortUrlsCount: PropTypes.number,
|
|
||||||
visitsCount: PropTypes.number,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const TagsListType = PropTypes.shape({
|
|
||||||
tags: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
filteredTags: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
stats: PropTypes.objectOf(TagStatsType), // Record
|
|
||||||
loading: PropTypes.bool,
|
|
||||||
error: PropTypes.bool,
|
|
||||||
});
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
tags: [],
|
|
||||||
filteredTags: [],
|
|
||||||
stats: {},
|
|
||||||
loading: false,
|
|
||||||
error: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const renameTag = (oldName, newName) => (tag) => tag === oldName ? newName : tag;
|
|
||||||
const rejectTag = (tags, tagToReject) => reject((tag) => tag === tagToReject, tags);
|
|
||||||
const increaseVisitsForTags = (tags, stats) => tags.reduce((stats, tag) => {
|
|
||||||
if (!stats[tag]) {
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tagStats = stats[tag];
|
|
||||||
|
|
||||||
tagStats.visitsCount = tagStats.visitsCount + 1;
|
|
||||||
stats[tag] = tagStats;
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
}, { ...stats });
|
|
||||||
|
|
||||||
export default handleActions({
|
|
||||||
[LIST_TAGS_START]: () => ({ ...initialState, loading: true }),
|
|
||||||
[LIST_TAGS_ERROR]: () => ({ ...initialState, error: true }),
|
|
||||||
[LIST_TAGS]: (state, { tags, stats }) => ({ ...initialState, stats, tags, filteredTags: tags }),
|
|
||||||
[TAG_DELETED]: (state, { tag }) => ({
|
|
||||||
...state,
|
|
||||||
tags: rejectTag(state.tags, tag),
|
|
||||||
filteredTags: rejectTag(state.filteredTags, tag),
|
|
||||||
}),
|
|
||||||
[TAG_EDITED]: (state, { oldName, newName }) => ({
|
|
||||||
...state,
|
|
||||||
tags: state.tags.map(renameTag(oldName, newName)).sort(),
|
|
||||||
filteredTags: state.filteredTags.map(renameTag(oldName, newName)).sort(),
|
|
||||||
}),
|
|
||||||
[FILTER_TAGS]: (state, { searchTerm }) => ({
|
|
||||||
...state,
|
|
||||||
filteredTags: state.tags.filter((tag) => tag.toLowerCase().match(searchTerm)),
|
|
||||||
}),
|
|
||||||
[CREATE_VISIT]: (state, { shortUrl }) => ({
|
|
||||||
...state,
|
|
||||||
stats: increaseVisitsForTags(shortUrl.tags, state.stats),
|
|
||||||
}),
|
|
||||||
}, initialState);
|
|
||||||
|
|
||||||
export const listTags = (buildShlinkApiClient, force = true) => () => async (dispatch, getState) => {
|
|
||||||
const { tagsList } = getState();
|
|
||||||
|
|
||||||
if (!force && (tagsList.loading || !isEmpty(tagsList.tags))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({ type: LIST_TAGS_START });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { listTags } = buildShlinkApiClient(getState);
|
|
||||||
const { tags, stats = [] } = await listTags();
|
|
||||||
const processedStats = stats.reduce((acc, { tag, shortUrlsCount, visitsCount }) => {
|
|
||||||
acc[tag] = { shortUrlsCount, visitsCount };
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
dispatch({ tags, stats: processedStats, type: LIST_TAGS });
|
|
||||||
} catch (e) {
|
|
||||||
dispatch({ type: LIST_TAGS_ERROR });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const filterTags = (searchTerm) => ({ type: FILTER_TAGS, searchTerm });
|
|
125
src/tags/reducers/tagsList.ts
Normal file
125
src/tags/reducers/tagsList.ts
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
import { isEmpty, reject } from 'ramda';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Action, Dispatch } from 'redux';
|
||||||
|
import { CREATE_VISIT, CreateVisitAction } from '../../visits/reducers/visitCreation';
|
||||||
|
import { buildReducer } from '../../utils/helpers/redux';
|
||||||
|
import { ShlinkApiClientBuilder, ShlinkTags } from '../../utils/services/types';
|
||||||
|
import { GetState } from '../../container/types';
|
||||||
|
import { DeleteTagAction, TAG_DELETED } from './tagDelete';
|
||||||
|
import { EditTagAction, TAG_EDITED } from './tagEdit';
|
||||||
|
|
||||||
|
/* eslint-disable padding-line-between-statements */
|
||||||
|
export const LIST_TAGS_START = 'shlink/tagsList/LIST_TAGS_START';
|
||||||
|
export const LIST_TAGS_ERROR = 'shlink/tagsList/LIST_TAGS_ERROR';
|
||||||
|
export const LIST_TAGS = 'shlink/tagsList/LIST_TAGS';
|
||||||
|
export const FILTER_TAGS = 'shlink/tagsList/FILTER_TAGS';
|
||||||
|
/* eslint-enable padding-line-between-statements */
|
||||||
|
|
||||||
|
/** @deprecated Use TagsList interface instead */
|
||||||
|
export const TagsListType = PropTypes.shape({
|
||||||
|
tags: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
filteredTags: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
stats: PropTypes.objectOf(PropTypes.shape({
|
||||||
|
shortUrlsCount: PropTypes.number,
|
||||||
|
visitsCount: PropTypes.number,
|
||||||
|
})), // Record
|
||||||
|
loading: PropTypes.bool,
|
||||||
|
error: PropTypes.bool,
|
||||||
|
});
|
||||||
|
|
||||||
|
type TagsStats = Record<string, { shortUrlsCount: number; visitsCount: number }>;
|
||||||
|
|
||||||
|
export interface TagsList {
|
||||||
|
tags: string[];
|
||||||
|
filteredTags: string[];
|
||||||
|
stats: TagsStats;
|
||||||
|
loading: boolean;
|
||||||
|
error: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListTagsAction extends Action<string> {
|
||||||
|
tags: string[];
|
||||||
|
stats: TagsStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilterTagsAction extends Action<string> {
|
||||||
|
searchTerm: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListTagsCombinedAction = ListTagsAction & DeleteTagAction & CreateVisitAction & EditTagAction & FilterTagsAction;
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
tags: [],
|
||||||
|
filteredTags: [],
|
||||||
|
stats: {},
|
||||||
|
loading: false,
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const renameTag = (oldName: string, newName: string) => (tag: string) => tag === oldName ? newName : tag;
|
||||||
|
const rejectTag = (tags: string[], tagToReject: string) => reject((tag) => tag === tagToReject, tags);
|
||||||
|
const increaseVisitsForTags = (tags: string[], stats: TagsStats) => tags.reduce((stats, tag) => {
|
||||||
|
if (!stats[tag]) {
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagStats = stats[tag];
|
||||||
|
|
||||||
|
tagStats.visitsCount = tagStats.visitsCount + 1;
|
||||||
|
stats[tag] = tagStats;
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}, { ...stats });
|
||||||
|
|
||||||
|
export default buildReducer<TagsList, ListTagsCombinedAction>({
|
||||||
|
[LIST_TAGS_START]: () => ({ ...initialState, loading: true }),
|
||||||
|
[LIST_TAGS_ERROR]: () => ({ ...initialState, error: true }),
|
||||||
|
[LIST_TAGS]: (_, { tags, stats }) => ({ ...initialState, stats, tags, filteredTags: tags }),
|
||||||
|
[TAG_DELETED]: (state, { tag }) => ({
|
||||||
|
...state,
|
||||||
|
tags: rejectTag(state.tags, tag),
|
||||||
|
filteredTags: rejectTag(state.filteredTags, tag),
|
||||||
|
}),
|
||||||
|
[TAG_EDITED]: (state, { oldName, newName }) => ({
|
||||||
|
...state,
|
||||||
|
tags: state.tags.map(renameTag(oldName, newName)).sort(),
|
||||||
|
filteredTags: state.filteredTags.map(renameTag(oldName, newName)).sort(),
|
||||||
|
}),
|
||||||
|
[FILTER_TAGS]: (state, { searchTerm }) => ({
|
||||||
|
...state,
|
||||||
|
filteredTags: state.tags.filter((tag) => tag.toLowerCase().match(searchTerm)),
|
||||||
|
}),
|
||||||
|
[CREATE_VISIT]: (state, { shortUrl }) => ({
|
||||||
|
...state,
|
||||||
|
stats: increaseVisitsForTags(shortUrl.tags, state.stats),
|
||||||
|
}),
|
||||||
|
}, initialState);
|
||||||
|
|
||||||
|
export const listTags = (buildShlinkApiClient: ShlinkApiClientBuilder, force = true) => () => async (
|
||||||
|
dispatch: Dispatch,
|
||||||
|
getState: GetState,
|
||||||
|
) => {
|
||||||
|
const { tagsList } = getState();
|
||||||
|
|
||||||
|
if (!force && (tagsList.loading || !isEmpty(tagsList.tags))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({ type: LIST_TAGS_START });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { listTags } = buildShlinkApiClient(getState);
|
||||||
|
const { tags, stats = [] }: ShlinkTags = await listTags();
|
||||||
|
const processedStats = stats.reduce<TagsStats>((acc, { tag, shortUrlsCount, visitsCount }) => {
|
||||||
|
acc[tag] = { shortUrlsCount, visitsCount };
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
dispatch<ListTagsAction>({ tags, stats: processedStats, type: LIST_TAGS });
|
||||||
|
} catch (e) {
|
||||||
|
dispatch({ type: LIST_TAGS_ERROR });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterTags = (searchTerm: string): FilterTagsAction => ({ type: FILTER_TAGS, searchTerm });
|
|
@ -15,6 +15,17 @@ export interface ShlinkHealth {
|
||||||
version: string;
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ShlinkTagsStats {
|
||||||
|
tag: string;
|
||||||
|
shortUrlsCount: number;
|
||||||
|
visitsCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ShlinkTags {
|
||||||
|
tags: string[];
|
||||||
|
stats?: ShlinkTagsStats[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProblemDetailsError {
|
export interface ProblemDetailsError {
|
||||||
type: string;
|
type: string;
|
||||||
detail: string;
|
detail: string;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
import reducer, {
|
import reducer, {
|
||||||
DELETE_TAG_START,
|
DELETE_TAG_START,
|
||||||
DELETE_TAG_ERROR,
|
DELETE_TAG_ERROR,
|
||||||
|
@ -6,25 +7,27 @@ import reducer, {
|
||||||
tagDeleted,
|
tagDeleted,
|
||||||
deleteTag,
|
deleteTag,
|
||||||
} from '../../../src/tags/reducers/tagDelete';
|
} from '../../../src/tags/reducers/tagDelete';
|
||||||
|
import ShlinkApiClient from '../../../src/utils/services/ShlinkApiClient';
|
||||||
|
import { ShlinkState } from '../../../src/container/types';
|
||||||
|
|
||||||
describe('tagDeleteReducer', () => {
|
describe('tagDeleteReducer', () => {
|
||||||
describe('reducer', () => {
|
describe('reducer', () => {
|
||||||
it('returns loading on DELETE_TAG_START', () => {
|
it('returns loading on DELETE_TAG_START', () => {
|
||||||
expect(reducer({}, { type: DELETE_TAG_START })).toEqual({
|
expect(reducer(undefined, { type: DELETE_TAG_START })).toEqual({
|
||||||
deleting: true,
|
deleting: true,
|
||||||
error: false,
|
error: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns error on DELETE_TAG_ERROR', () => {
|
it('returns error on DELETE_TAG_ERROR', () => {
|
||||||
expect(reducer({}, { type: DELETE_TAG_ERROR })).toEqual({
|
expect(reducer(undefined, { type: DELETE_TAG_ERROR })).toEqual({
|
||||||
deleting: false,
|
deleting: false,
|
||||||
error: true,
|
error: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns tag names on DELETE_TAG', () => {
|
it('returns tag names on DELETE_TAG', () => {
|
||||||
expect(reducer({}, { type: DELETE_TAG })).toEqual({
|
expect(reducer(undefined, { type: DELETE_TAG })).toEqual({
|
||||||
deleting: false,
|
deleting: false,
|
||||||
error: false,
|
error: false,
|
||||||
});
|
});
|
||||||
|
@ -40,11 +43,11 @@ describe('tagDeleteReducer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteTag', () => {
|
describe('deleteTag', () => {
|
||||||
const createApiClientMock = (result) => ({
|
const createApiClientMock = (result: Promise<void>) => Mock.of<ShlinkApiClient>({
|
||||||
deleteTags: jest.fn(() => result),
|
deleteTags: jest.fn(async () => result),
|
||||||
});
|
});
|
||||||
const dispatch = jest.fn();
|
const dispatch = jest.fn();
|
||||||
const getState = () => ({});
|
const getState = () => Mock.all<ShlinkState>();
|
||||||
|
|
||||||
afterEach(() => dispatch.mockReset());
|
afterEach(() => dispatch.mockReset());
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
import reducer, {
|
import reducer, {
|
||||||
EDIT_TAG_START,
|
EDIT_TAG_START,
|
||||||
EDIT_TAG_ERROR,
|
EDIT_TAG_ERROR,
|
||||||
|
@ -5,26 +6,38 @@ import reducer, {
|
||||||
TAG_EDITED,
|
TAG_EDITED,
|
||||||
tagEdited,
|
tagEdited,
|
||||||
editTag,
|
editTag,
|
||||||
|
EditTagAction,
|
||||||
} from '../../../src/tags/reducers/tagEdit';
|
} from '../../../src/tags/reducers/tagEdit';
|
||||||
|
import ShlinkApiClient from '../../../src/utils/services/ShlinkApiClient';
|
||||||
|
import ColorGenerator from '../../../src/utils/services/ColorGenerator';
|
||||||
|
import { ShlinkState } from '../../../src/container/types';
|
||||||
|
|
||||||
describe('tagEditReducer', () => {
|
describe('tagEditReducer', () => {
|
||||||
|
const oldName = 'foo';
|
||||||
|
const newName = 'bar';
|
||||||
|
const color = '#ff0000';
|
||||||
|
|
||||||
describe('reducer', () => {
|
describe('reducer', () => {
|
||||||
it('returns loading on EDIT_TAG_START', () => {
|
it('returns loading on EDIT_TAG_START', () => {
|
||||||
expect(reducer({}, { type: EDIT_TAG_START })).toEqual({
|
expect(reducer(undefined, Mock.of<EditTagAction>({ type: EDIT_TAG_START }))).toEqual({
|
||||||
editing: true,
|
editing: true,
|
||||||
error: false,
|
error: false,
|
||||||
|
oldName: '',
|
||||||
|
newName: '',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns error on EDIT_TAG_ERROR', () => {
|
it('returns error on EDIT_TAG_ERROR', () => {
|
||||||
expect(reducer({}, { type: EDIT_TAG_ERROR })).toEqual({
|
expect(reducer(undefined, Mock.of<EditTagAction>({ type: EDIT_TAG_ERROR }))).toEqual({
|
||||||
editing: false,
|
editing: false,
|
||||||
error: true,
|
error: true,
|
||||||
|
oldName: '',
|
||||||
|
newName: '',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns tag names on EDIT_TAG', () => {
|
it('returns tag names on EDIT_TAG', () => {
|
||||||
expect(reducer({}, { type: EDIT_TAG, oldName: 'foo', newName: 'bar' })).toEqual({
|
expect(reducer(undefined, { type: EDIT_TAG, oldName, newName, color })).toEqual({
|
||||||
editing: false,
|
editing: false,
|
||||||
error: false,
|
error: false,
|
||||||
oldName: 'foo',
|
oldName: 'foo',
|
||||||
|
@ -44,24 +57,18 @@ describe('tagEditReducer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('editTag', () => {
|
describe('editTag', () => {
|
||||||
const createApiClientMock = (result) => ({
|
const createApiClientMock = (result: Promise<void>) => Mock.of<ShlinkApiClient>({
|
||||||
editTag: jest.fn(() => result),
|
editTag: jest.fn(async () => result),
|
||||||
});
|
});
|
||||||
const colorGenerator = {
|
const colorGenerator = Mock.of<ColorGenerator>({
|
||||||
setColorForKey: jest.fn(),
|
setColorForKey: jest.fn(),
|
||||||
};
|
|
||||||
const dispatch = jest.fn();
|
|
||||||
const getState = () => ({});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
colorGenerator.setColorForKey.mockReset();
|
|
||||||
dispatch.mockReset();
|
|
||||||
});
|
});
|
||||||
|
const dispatch = jest.fn();
|
||||||
|
const getState = () => Mock.of<ShlinkState>();
|
||||||
|
|
||||||
|
afterEach(jest.clearAllMocks);
|
||||||
|
|
||||||
it('calls API on success', async () => {
|
it('calls API on success', async () => {
|
||||||
const oldName = 'foo';
|
|
||||||
const newName = 'bar';
|
|
||||||
const color = '#ff0000';
|
|
||||||
const apiClientMock = createApiClientMock(Promise.resolve());
|
const apiClientMock = createApiClientMock(Promise.resolve());
|
||||||
const dispatchable = editTag(() => apiClientMock, colorGenerator)(oldName, newName, color);
|
const dispatchable = editTag(() => apiClientMock, colorGenerator)(oldName, newName, color);
|
||||||
|
|
||||||
|
@ -80,9 +87,6 @@ describe('tagEditReducer', () => {
|
||||||
|
|
||||||
it('throws on error', async () => {
|
it('throws on error', async () => {
|
||||||
const error = 'Error';
|
const error = 'Error';
|
||||||
const oldName = 'foo';
|
|
||||||
const newName = 'bar';
|
|
||||||
const color = '#ff0000';
|
|
||||||
const apiClientMock = createApiClientMock(Promise.reject(error));
|
const apiClientMock = createApiClientMock(Promise.reject(error));
|
||||||
const dispatchable = editTag(() => apiClientMock, colorGenerator)(oldName, newName, color);
|
const dispatchable = editTag(() => apiClientMock, colorGenerator)(oldName, newName, color);
|
||||||
|
|
|
@ -1,24 +1,30 @@
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
import reducer, {
|
import reducer, {
|
||||||
FILTER_TAGS,
|
FILTER_TAGS,
|
||||||
filterTags,
|
filterTags,
|
||||||
LIST_TAGS,
|
LIST_TAGS,
|
||||||
LIST_TAGS_ERROR,
|
LIST_TAGS_ERROR,
|
||||||
LIST_TAGS_START, listTags,
|
LIST_TAGS_START,
|
||||||
|
listTags,
|
||||||
|
TagsList,
|
||||||
} from '../../../src/tags/reducers/tagsList';
|
} from '../../../src/tags/reducers/tagsList';
|
||||||
import { TAG_DELETED } from '../../../src/tags/reducers/tagDelete';
|
import { TAG_DELETED } from '../../../src/tags/reducers/tagDelete';
|
||||||
import { TAG_EDITED } from '../../../src/tags/reducers/tagEdit';
|
import { TAG_EDITED } from '../../../src/tags/reducers/tagEdit';
|
||||||
|
import { ShlinkState } from '../../../src/container/types';
|
||||||
|
|
||||||
describe('tagsListReducer', () => {
|
describe('tagsListReducer', () => {
|
||||||
|
const state = (props: Partial<TagsList>) => Mock.of<TagsList>(props);
|
||||||
|
|
||||||
describe('reducer', () => {
|
describe('reducer', () => {
|
||||||
it('returns loading on LIST_TAGS_START', () => {
|
it('returns loading on LIST_TAGS_START', () => {
|
||||||
expect(reducer({}, { type: LIST_TAGS_START })).toEqual(expect.objectContaining({
|
expect(reducer(undefined, { type: LIST_TAGS_START } as any)).toEqual(expect.objectContaining({
|
||||||
loading: true,
|
loading: true,
|
||||||
error: false,
|
error: false,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns error on LIST_TAGS_ERROR', () => {
|
it('returns error on LIST_TAGS_ERROR', () => {
|
||||||
expect(reducer({}, { type: LIST_TAGS_ERROR })).toEqual(expect.objectContaining({
|
expect(reducer(undefined, { type: LIST_TAGS_ERROR } as any)).toEqual(expect.objectContaining({
|
||||||
loading: false,
|
loading: false,
|
||||||
error: true,
|
error: true,
|
||||||
}));
|
}));
|
||||||
|
@ -27,7 +33,7 @@ describe('tagsListReducer', () => {
|
||||||
it('returns provided tags as filtered and regular tags on LIST_TAGS', () => {
|
it('returns provided tags as filtered and regular tags on LIST_TAGS', () => {
|
||||||
const tags = [ 'foo', 'bar', 'baz' ];
|
const tags = [ 'foo', 'bar', 'baz' ];
|
||||||
|
|
||||||
expect(reducer({}, { type: LIST_TAGS, tags })).toEqual({
|
expect(reducer(undefined, { type: LIST_TAGS, tags } as any)).toEqual({
|
||||||
tags,
|
tags,
|
||||||
filteredTags: tags,
|
filteredTags: tags,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
@ -40,7 +46,7 @@ describe('tagsListReducer', () => {
|
||||||
const tag = 'foo';
|
const tag = 'foo';
|
||||||
const expectedTags = [ 'bar', 'baz' ];
|
const expectedTags = [ 'bar', 'baz' ];
|
||||||
|
|
||||||
expect(reducer({ tags, filteredTags: tags }, { type: TAG_DELETED, tag })).toEqual({
|
expect(reducer(state({ tags, filteredTags: tags }), { type: TAG_DELETED, tag } as any)).toEqual({
|
||||||
tags: expectedTags,
|
tags: expectedTags,
|
||||||
filteredTags: expectedTags,
|
filteredTags: expectedTags,
|
||||||
});
|
});
|
||||||
|
@ -52,7 +58,7 @@ describe('tagsListReducer', () => {
|
||||||
const newName = 'renamed';
|
const newName = 'renamed';
|
||||||
const expectedTags = [ 'foo', 'renamed', 'baz' ].sort();
|
const expectedTags = [ 'foo', 'renamed', 'baz' ].sort();
|
||||||
|
|
||||||
expect(reducer({ tags, filteredTags: tags }, { type: TAG_EDITED, oldName, newName })).toEqual({
|
expect(reducer(state({ tags, filteredTags: tags }), { type: TAG_EDITED, oldName, newName } as any)).toEqual({
|
||||||
tags: expectedTags,
|
tags: expectedTags,
|
||||||
filteredTags: expectedTags,
|
filteredTags: expectedTags,
|
||||||
});
|
});
|
||||||
|
@ -63,7 +69,7 @@ describe('tagsListReducer', () => {
|
||||||
const searchTerm = 'fo';
|
const searchTerm = 'fo';
|
||||||
const filteredTags = [ 'foo', 'foo2', 'fo' ];
|
const filteredTags = [ 'foo', 'foo2', 'fo' ];
|
||||||
|
|
||||||
expect(reducer({ tags }, { type: FILTER_TAGS, searchTerm })).toEqual({
|
expect(reducer(state({ tags }), { type: FILTER_TAGS, searchTerm } as any)).toEqual({
|
||||||
tags,
|
tags,
|
||||||
filteredTags,
|
filteredTags,
|
||||||
});
|
});
|
||||||
|
@ -76,19 +82,14 @@ describe('tagsListReducer', () => {
|
||||||
|
|
||||||
describe('listTags', () => {
|
describe('listTags', () => {
|
||||||
const dispatch = jest.fn();
|
const dispatch = jest.fn();
|
||||||
const getState = jest.fn(() => ({}));
|
const getState = jest.fn(() => Mock.all<ShlinkState>());
|
||||||
const buildShlinkApiClient = jest.fn();
|
const buildShlinkApiClient = jest.fn();
|
||||||
const listTagsMock = jest.fn();
|
const listTagsMock = jest.fn();
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(jest.clearAllMocks);
|
||||||
dispatch.mockReset();
|
|
||||||
getState.mockClear();
|
|
||||||
buildShlinkApiClient.mockReset();
|
|
||||||
listTagsMock.mockReset();
|
|
||||||
});
|
|
||||||
|
|
||||||
const assertNoAction = async (tagsList) => {
|
const assertNoAction = async (tagsList: TagsList) => {
|
||||||
getState.mockReturnValue({ tagsList });
|
getState.mockReturnValue(Mock.of<ShlinkState>({ tagsList }));
|
||||||
|
|
||||||
await listTags(buildShlinkApiClient, false)()(dispatch, getState);
|
await listTags(buildShlinkApiClient, false)()(dispatch, getState);
|
||||||
|
|
||||||
|
@ -97,8 +98,11 @@ describe('tagsListReducer', () => {
|
||||||
expect(getState).toHaveBeenCalledTimes(1);
|
expect(getState).toHaveBeenCalledTimes(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
it('does nothing when loading', async () => await assertNoAction({ loading: true }));
|
it('does nothing when loading', async () => assertNoAction(state({ loading: true })));
|
||||||
it('does nothing when list is not empty', async () => await assertNoAction({ loading: false, tags: [ 'foo', 'bar' ] }));
|
it(
|
||||||
|
'does nothing when list is not empty',
|
||||||
|
async () => assertNoAction(state({ loading: false, tags: [ 'foo', 'bar' ] })),
|
||||||
|
);
|
||||||
|
|
||||||
it('dispatches loaded lists when no error occurs', async () => {
|
it('dispatches loaded lists when no error occurs', async () => {
|
||||||
const tags = [ 'foo', 'bar', 'baz' ];
|
const tags = [ 'foo', 'bar', 'baz' ];
|
Loading…
Reference in a new issue