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 => apiClientFactory().listShortUrls( params ?? {}, ), ); export const shortUrlsListReducerCreator = ( listShortUrlsThunk: ReturnType, editShortUrlThunk: ReturnType, createShortUrlThunk: ReturnType, ) => 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) => 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, ), ); }, });