Migrated tagsList reducer to RTK

This commit is contained in:
Alejandro Celaya
2022-11-08 22:48:53 +01:00
parent b7622b2b38
commit f9bfb742da
4 changed files with 96 additions and 99 deletions

View File

@@ -8,7 +8,6 @@ import tagVisitsReducer from '../visits/reducers/tagVisits';
import domainVisitsReducer from '../visits/reducers/domainVisits';
import orphanVisitsReducer from '../visits/reducers/orphanVisits';
import nonOrphanVisitsReducer from '../visits/reducers/nonOrphanVisits';
import tagsListReducer from '../tags/reducers/tagsList';
import { settingsReducer } from '../settings/reducers/settings';
import visitsOverviewReducer from '../visits/reducers/visitsOverview';
import { appUpdatesReducer } from '../app/reducers/appUpdates';
@@ -28,7 +27,7 @@ export default (container: IContainer) => combineReducers<ShlinkState>({
domainVisits: domainVisitsReducer,
orphanVisits: orphanVisitsReducer,
nonOrphanVisits: nonOrphanVisitsReducer,
tagsList: tagsListReducer,
tagsList: container.tagsListReducer,
tagDelete: container.tagDeleteReducer,
tagEdit: container.tagEditReducer,
mercureInfo: container.mercureInfoReducer,

View File

@@ -1,24 +1,19 @@
import { createAction, PayloadAction } from '@reduxjs/toolkit';
import { createAction, createSlice } from '@reduxjs/toolkit';
import { isEmpty, reject } from 'ramda';
import { Action, Dispatch } from 'redux';
import { createNewVisits, CreateVisitsAction } from '../../visits/reducers/visitCreation';
import { buildReducer } from '../../utils/helpers/redux';
import { createNewVisits } from '../../visits/reducers/visitCreation';
import { createAsyncThunk } from '../../utils/helpers/redux';
import { ShlinkTags } from '../../api/types';
import { GetState } from '../../container/types';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import { CreateVisit, Stats } from '../../visits/types';
import { parseApiError } from '../../api/utils';
import { TagStats } from '../data';
import { ApiErrorAction } from '../../api/types/actions';
import { CREATE_SHORT_URL, CreateShortUrlAction } from '../../short-urls/reducers/shortUrlCreation';
import { DeleteTagAction, tagDeleted } from './tagDelete';
import { EditTagAction, tagEdited } from './tagEdit';
import { CREATE_SHORT_URL } from '../../short-urls/reducers/shortUrlCreation';
import { tagDeleted } from './tagDelete';
import { tagEdited } from './tagEdit';
import { ProblemDetailsError } from '../../api/types/errors';
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';
const LIST_TAGS = 'shlink/tagsList/LIST_TAGS';
const FILTER_TAGS = 'shlink/tagsList/FILTER_TAGS';
type TagsStatsMap = Record<string, TagStats>;
@@ -31,22 +26,12 @@ export interface TagsList {
errorData?: ProblemDetailsError;
}
interface ListTagsAction extends Action<string> {
interface ListTags {
tags: string[];
stats: TagsStatsMap;
}
type FilterTagsAction = PayloadAction<string>;
type TagsCombinedAction = ListTagsAction
& DeleteTagAction
& CreateVisitsAction
& CreateShortUrlAction
& EditTagAction
& FilterTagsAction
& ApiErrorAction;
const initialState = {
const initialState: TagsList = {
tags: [],
filteredTags: [],
stats: {},
@@ -80,47 +65,15 @@ const calculateVisitsPerTag = (createdVisits: CreateVisit[]): TagIncrease[] => O
}, {}),
);
export default buildReducer<TagsList, TagsCombinedAction>({
[LIST_TAGS_START]: () => ({ ...initialState, loading: true }),
[LIST_TAGS_ERROR]: (_, { errorData }) => ({ ...initialState, error: true, errorData }),
[LIST_TAGS]: (_, { tags, stats }) => ({ ...initialState, stats, tags, filteredTags: tags }),
[tagDeleted.toString()]: (state, { payload: tag }) => ({
...state,
tags: rejectTag(state.tags, tag),
filteredTags: rejectTag(state.filteredTags, tag),
}),
[tagEdited.toString()]: (state, { payload }) => ({
...state,
tags: state.tags.map(renameTag(payload.oldName, payload.newName)).sort(),
filteredTags: state.filteredTags.map(renameTag(payload.oldName, payload.newName)).sort(),
}),
[FILTER_TAGS]: (state, { payload: searchTerm }) => ({
...state,
filteredTags: state.tags.filter((tag) => tag.toLowerCase().match(searchTerm.toLowerCase())),
}),
[createNewVisits.toString()]: (state, { payload }) => ({
...state,
stats: increaseVisitsForTags(calculateVisitsPerTag(payload.createdVisits), state.stats),
}),
[`${CREATE_SHORT_URL}/fulfilled`]: ({ tags: stateTags, ...rest }, { payload }) => ({ // TODO Do not hardcode action type here
...rest,
tags: stateTags.concat(payload.tags.filter((tag) => !stateTags.includes(tag))), // More performant than [ ...new Set(...) ]
}),
}, initialState);
export const listTags = (buildShlinkApiClient: ShlinkApiClientBuilder, force = true) => createAsyncThunk(
LIST_TAGS,
async (_: void, { getState }): Promise<ListTags> => {
const { tagsList } = getState();
export const listTags = (buildShlinkApiClient: ShlinkApiClientBuilder, force = true) => () => async (
dispatch: Dispatch,
getState: GetState,
) => {
const { tagsList } = getState();
if (!force && !isEmpty(tagsList.tags)) {
return tagsList;
}
if (!force && (tagsList.loading || !isEmpty(tagsList.tags))) {
return;
}
dispatch({ type: LIST_TAGS_START });
try {
const { listTags: shlinkListTags } = buildShlinkApiClient(getState);
const { tags, stats = [] }: ShlinkTags = await shlinkListTags();
const processedStats = stats.reduce<TagsStatsMap>((acc, { tag, shortUrlsCount, visitsCount }) => {
@@ -129,10 +82,49 @@ export const listTags = (buildShlinkApiClient: ShlinkApiClientBuilder, force = t
return acc;
}, {});
dispatch<ListTagsAction>({ tags, stats: processedStats, type: LIST_TAGS });
} catch (e: any) {
dispatch<ApiErrorAction>({ type: LIST_TAGS_ERROR, errorData: parseApiError(e) });
}
};
return { tags, stats: processedStats };
},
);
export const filterTags = createAction<string>(FILTER_TAGS);
export const reducer = (listTagsThunk: ReturnType<typeof listTags>) => createSlice({
name: 'shlink/tagsList',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(filterTags, (state, { payload: searchTerm }) => ({
...state,
filteredTags: state.tags.filter((tag) => tag.toLowerCase().match(searchTerm.toLowerCase())),
}));
builder.addCase(listTagsThunk.pending, (state) => ({ ...state, loading: true, error: false }));
builder.addCase(listTagsThunk.rejected, (_, { error }) => (
{ ...initialState, error: true, errorData: parseApiError(error) }
));
builder.addCase(listTagsThunk.fulfilled, (_, { payload }) => (
{ ...initialState, stats: payload.stats, tags: payload.tags, filteredTags: payload.tags }
));
builder.addCase(tagDeleted, (state, { payload: tag }) => ({
...state,
tags: rejectTag(state.tags, tag),
filteredTags: rejectTag(state.filteredTags, tag),
}));
builder.addCase(tagEdited, (state, { payload }) => ({
...state,
tags: state.tags.map(renameTag(payload.oldName, payload.newName)).sort(),
filteredTags: state.filteredTags.map(renameTag(payload.oldName, payload.newName)).sort(),
}));
builder.addCase(createNewVisits, (state, { payload }) => ({
...state,
stats: increaseVisitsForTags(calculateVisitsPerTag(payload.createdVisits), state.stats),
}));
// TODO Do not hardcode action type here. Inject async thunk instead
builder.addCase(`${CREATE_SHORT_URL}/fulfilled`, ({ tags: stateTags, ...rest }, { payload }: any) => ({
...rest,
tags: stateTags.concat(payload.tags.filter((tag: string) => !stateTags.includes(tag))), // More performant than [ ...new Set(...) ]
}));
},
}).reducer;

View File

@@ -5,7 +5,7 @@ import { TagCard } from '../TagCard';
import { DeleteTagConfirmModal } from '../helpers/DeleteTagConfirmModal';
import { EditTagModal } from '../helpers/EditTagModal';
import { TagsList } from '../TagsList';
import { filterTags, listTags } from '../reducers/tagsList';
import { filterTags, listTags, reducer } from '../reducers/tagsList';
import { tagDeleted, tagDeleteReducerCreator } from '../reducers/tagDelete';
import { tagEdited, tagEditReducerCreator } from '../reducers/tagEdit';
import { ConnectDecorator } from '../../container/types';
@@ -44,6 +44,8 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('tagDeleteReducerCreator', tagDeleteReducerCreator, 'buildShlinkApiClient');
bottle.serviceFactory('tagDeleteReducer', prop('reducer'), 'tagDeleteReducerCreator');
bottle.serviceFactory('tagsListReducer', reducer, 'listTags');
// Actions
const listTagsActionFactory = (force: boolean) =>
({ buildShlinkApiClient }: IContainer) => listTags(buildShlinkApiClient, force);