mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-02-28 04:36:45 +00:00
Migrated tags reducers to typescripts
This commit is contained in:
@@ -9,6 +9,9 @@ import { ShortUrlEdition } from '../short-urls/reducers/shortUrlEdition';
|
||||
import { ShortUrlsListParams } from '../short-urls/reducers/shortUrlsListParams';
|
||||
import { ShortUrlTags } from '../short-urls/reducers/shortUrlTags';
|
||||
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 {
|
||||
servers: ServersMap;
|
||||
@@ -23,9 +26,9 @@ export interface ShlinkState {
|
||||
shortUrlVisits: any;
|
||||
tagVisits: any;
|
||||
shortUrlDetail: any;
|
||||
tagsList: any;
|
||||
tagDelete: any;
|
||||
tagEdit: any;
|
||||
tagsList: TagsList;
|
||||
tagDelete: TagDeletion;
|
||||
tagEdit: TagEdition;
|
||||
mercureInfo: MercureInfo;
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { handleActions } from 'redux-actions';
|
||||
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 */
|
||||
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';
|
||||
/* eslint-enable padding-line-between-statements */
|
||||
|
||||
/** @deprecated Use TagDeletion interface */
|
||||
export const tagDeleteType = PropTypes.shape({
|
||||
deleting: 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,
|
||||
error: false,
|
||||
};
|
||||
|
||||
export default handleActions({
|
||||
export default buildReducer({
|
||||
[DELETE_TAG_START]: () => ({ deleting: true, error: false }),
|
||||
[DELETE_TAG_ERROR]: () => ({ deleting: false, error: true }),
|
||||
[DELETE_TAG]: () => ({ deleting: false, error: false }),
|
||||
}, 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 });
|
||||
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 { 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 */
|
||||
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';
|
||||
|
||||
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: '',
|
||||
newName: '',
|
||||
editing: false,
|
||||
error: false,
|
||||
};
|
||||
|
||||
export default handleActions({
|
||||
export default buildReducer<TagEdition, EditTagAction>({
|
||||
[EDIT_TAG_START]: (state) => ({ ...state, editing: true, error: false }),
|
||||
[EDIT_TAG_ERROR]: (state) => ({ ...state, editing: false, error: true }),
|
||||
[EDIT_TAG]: (state, action) => ({
|
||||
[EDIT_TAG]: (_, action) => ({
|
||||
...pick([ 'oldName', 'newName' ], action),
|
||||
editing: false,
|
||||
error: false,
|
||||
}),
|
||||
}, initialState);
|
||||
|
||||
export const editTag = (buildShlinkApiClient, colorGenerator) => (oldName, newName, color) => async (
|
||||
dispatch,
|
||||
getState,
|
||||
) => {
|
||||
export const editTag = (buildShlinkApiClient: ShlinkApiClientBuilder, colorGenerator: ColorGenerator) => (
|
||||
oldName: string,
|
||||
newName: string,
|
||||
color: string,
|
||||
) => async (dispatch: Dispatch, getState: GetState) => {
|
||||
dispatch({ type: EDIT_TAG_START });
|
||||
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,
|
||||
oldName,
|
||||
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;
|
||||
}
|
||||
|
||||
interface ShlinkTagsStats {
|
||||
tag: string;
|
||||
shortUrlsCount: number;
|
||||
visitsCount: number;
|
||||
}
|
||||
|
||||
export interface ShlinkTags {
|
||||
tags: string[];
|
||||
stats?: ShlinkTagsStats[];
|
||||
}
|
||||
|
||||
export interface ProblemDetailsError {
|
||||
type: string;
|
||||
detail: string;
|
||||
|
||||
Reference in New Issue
Block a user