shlink-web-client/shlink-web-component/short-urls/reducers/shortUrlsList.ts

113 lines
4.1 KiB
TypeScript

import { createSlice } from '@reduxjs/toolkit';
import { assocPath, last, pipe, reject } from 'ramda';
import type { ShlinkApiClient, ShlinkShortUrlsListParams, ShlinkShortUrlsResponse } from '../../api-contract';
import { createAsyncThunk } from '../../utils/redux';
import { createNewVisits } from '../../visits/reducers/visitCreation';
import type { ShortUrl } from '../data';
import { shortUrlMatches } from '../helpers';
import type { createShortUrl } from './shortUrlCreation';
import { shortUrlDeleted } from './shortUrlDeletion';
import type { editShortUrl } from './shortUrlEdition';
const REDUCER_PREFIX = 'shlink/shortUrlsList';
export const ITEMS_IN_OVERVIEW_PAGE = 5;
export interface ShortUrlsList {
shortUrls?: ShlinkShortUrlsResponse;
loading: boolean;
error: boolean;
}
const initialState: ShortUrlsList = {
loading: true,
error: false,
};
export const listShortUrls = (apiClientFactory: () => ShlinkApiClient) => createAsyncThunk(
`${REDUCER_PREFIX}/listShortUrls`,
(params: ShlinkShortUrlsListParams | void): Promise<ShlinkShortUrlsResponse> => apiClientFactory().listShortUrls(
params ?? {},
),
);
export const shortUrlsListReducerCreator = (
listShortUrlsThunk: ReturnType<typeof listShortUrls>,
editShortUrlThunk: ReturnType<typeof editShortUrl>,
createShortUrlThunk: ReturnType<typeof createShortUrl>,
) => createSlice({
name: REDUCER_PREFIX,
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(listShortUrlsThunk.pending, (state) => ({ ...state, loading: true, error: false }));
builder.addCase(listShortUrlsThunk.rejected, () => ({ loading: false, error: true }));
builder.addCase(
listShortUrlsThunk.fulfilled,
(_, { payload: shortUrls }) => ({ loading: false, error: false, shortUrls }),
);
builder.addCase(
createShortUrlThunk.fulfilled,
pipe(
// The only place where the list and the creation form coexist is the overview page.
// There we can assume we are displaying page 1, and therefore, we can safely prepend the new short URL.
// We can also remove the items above the amount that is displayed there.
(state, { payload }) => (!state.shortUrls ? state : assocPath(
['shortUrls', 'data'],
[payload, ...state.shortUrls.data.slice(0, ITEMS_IN_OVERVIEW_PAGE - 1)],
state,
)),
(state: ShortUrlsList) => (!state.shortUrls ? state : assocPath(
['shortUrls', 'pagination', 'totalItems'],
state.shortUrls.pagination.totalItems + 1,
state,
)),
),
);
builder.addCase(
editShortUrlThunk.fulfilled,
(state, { payload: editedShortUrl }) => (!state.shortUrls ? state : assocPath(
['shortUrls', 'data'],
state.shortUrls.data.map((shortUrl) => {
const { shortCode, domain } = editedShortUrl;
return shortUrlMatches(shortUrl, shortCode, domain) ? editedShortUrl : shortUrl;
}),
state,
)),
);
builder.addCase(
shortUrlDeleted,
pipe(
(state, { payload }) => (!state.shortUrls ? state : assocPath(
['shortUrls', 'data'],
reject<ShortUrl, ShortUrl[]>((shortUrl) =>
shortUrlMatches(shortUrl, payload.shortCode, payload.domain), state.shortUrls.data),
state,
)),
(state) => (!state.shortUrls ? state : assocPath(
['shortUrls', 'pagination', 'totalItems'],
state.shortUrls.pagination.totalItems - 1,
state,
)),
),
);
builder.addCase(
createNewVisits,
(state, { payload }) => assocPath(
['shortUrls', 'data'],
state.shortUrls?.data?.map(
// Find the last of the new visit for this short URL, and pick its short URL. It will have an up-to-date amount of visits.
(currentShortUrl) => last(
payload.createdVisits.filter(
({ shortUrl }) => shortUrl && shortUrlMatches(currentShortUrl, shortUrl.shortCode, shortUrl.domain),
),
)?.shortUrl ?? currentShortUrl,
),
state,
),
);
},
});