mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 17:40:23 +03:00
Merge pull request #732 from acelaya-forks/feature/more-rtk
Feature/more rtk
This commit is contained in:
commit
ed4c03f154
24 changed files with 361 additions and 336 deletions
|
@ -1,7 +1,7 @@
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
const { actions, reducer } = createSlice({
|
const { actions, reducer } = createSlice({
|
||||||
name: 'appUpdatesReducer',
|
name: 'shlink/appUpdates',
|
||||||
initialState: false,
|
initialState: false,
|
||||||
reducers: {
|
reducers: {
|
||||||
appUpdateAvailable: () => true,
|
appUpdateAvailable: () => true,
|
||||||
|
|
|
@ -9,7 +9,7 @@ const initialState: Sidebar = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const { actions, reducer } = createSlice({
|
const { actions, reducer } = createSlice({
|
||||||
name: 'sidebarReducer',
|
name: 'shlink/sidebar',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
sidebarPresent: () => ({ sidebarPresent: true }),
|
sidebarPresent: () => ({ sidebarPresent: true }),
|
||||||
|
|
|
@ -9,9 +9,7 @@ import { ProblemDetailsError } from '../../api/types/errors';
|
||||||
import { parseApiError } from '../../api/utils';
|
import { parseApiError } from '../../api/utils';
|
||||||
import { EditDomainRedirects } from './domainRedirects';
|
import { EditDomainRedirects } from './domainRedirects';
|
||||||
|
|
||||||
const LIST_DOMAINS = 'shlink/domainsList/LIST_DOMAINS';
|
const REDUCER_PREFIX = 'shlink/domainsList';
|
||||||
const FILTER_DOMAINS = 'shlink/domainsList/FILTER_DOMAINS';
|
|
||||||
const VALIDATE_DOMAIN = 'shlink/domainsList/VALIDATE_DOMAIN';
|
|
||||||
|
|
||||||
export interface DomainsList {
|
export interface DomainsList {
|
||||||
domains: Domain[];
|
domains: Domain[];
|
||||||
|
@ -49,7 +47,7 @@ export const domainsListReducerCreator = (
|
||||||
buildShlinkApiClient: ShlinkApiClientBuilder,
|
buildShlinkApiClient: ShlinkApiClientBuilder,
|
||||||
editDomainRedirects: AsyncThunk<EditDomainRedirects, any, any>,
|
editDomainRedirects: AsyncThunk<EditDomainRedirects, any, any>,
|
||||||
) => {
|
) => {
|
||||||
const listDomains = createAsyncThunk(LIST_DOMAINS, async (_: void, { getState }): Promise<ListDomains> => {
|
const listDomains = createAsyncThunk(`${REDUCER_PREFIX}/listDomains`, async (_: void, { getState }): Promise<ListDomains> => {
|
||||||
const { listDomains: shlinkListDomains } = buildShlinkApiClient(getState);
|
const { listDomains: shlinkListDomains } = buildShlinkApiClient(getState);
|
||||||
const { data, defaultRedirects } = await shlinkListDomains();
|
const { data, defaultRedirects } = await shlinkListDomains();
|
||||||
|
|
||||||
|
@ -60,7 +58,7 @@ export const domainsListReducerCreator = (
|
||||||
});
|
});
|
||||||
|
|
||||||
const checkDomainHealth = createAsyncThunk(
|
const checkDomainHealth = createAsyncThunk(
|
||||||
VALIDATE_DOMAIN,
|
`${REDUCER_PREFIX}/checkDomainHealth`,
|
||||||
async (domain: string, { getState }): Promise<ValidateDomain> => {
|
async (domain: string, { getState }): Promise<ValidateDomain> => {
|
||||||
const { selectedServer } = getState();
|
const { selectedServer } = getState();
|
||||||
|
|
||||||
|
@ -84,10 +82,10 @@ export const domainsListReducerCreator = (
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const filterDomains = createAction<string>(FILTER_DOMAINS);
|
const filterDomains = createAction<string>(`${REDUCER_PREFIX}/filterDomains`);
|
||||||
|
|
||||||
const { reducer } = createSlice<DomainsList, SliceCaseReducers<DomainsList>>({
|
const { reducer } = createSlice<DomainsList, SliceCaseReducers<DomainsList>>({
|
||||||
name: 'domainsList',
|
name: REDUCER_PREFIX,
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {},
|
reducers: {},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
@import './utils/base';
|
@import './utils/base';
|
||||||
@import 'node_modules/bootstrap/scss/bootstrap.scss';
|
@import 'node_modules/bootstrap/scss/bootstrap.scss';
|
||||||
@import './common/react-tag-autocomplete.scss';
|
@import './common/react-tag-autocomplete.scss';
|
||||||
@import './theme/theme';
|
@import 'utils/theme/theme';
|
||||||
@import './utils/table/ResponsiveTable';
|
@import './utils/table/ResponsiveTable';
|
||||||
@import './utils/StickyCardPaginator';
|
@import './utils/StickyCardPaginator';
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { createAsyncThunk } from '../../utils/helpers/redux';
|
||||||
import { ShlinkMercureInfo } from '../../api/types';
|
import { ShlinkMercureInfo } from '../../api/types';
|
||||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||||
|
|
||||||
const GET_MERCURE_INFO = 'shlink/mercure/GET_MERCURE_INFO';
|
const REDUCER_PREFIX = 'shlink/mercure';
|
||||||
|
|
||||||
export interface MercureInfo extends Partial<ShlinkMercureInfo> {
|
export interface MercureInfo extends Partial<ShlinkMercureInfo> {
|
||||||
interval?: number;
|
interval?: number;
|
||||||
|
@ -17,17 +17,20 @@ const initialState: MercureInfo = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mercureInfoReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder) => {
|
export const mercureInfoReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder) => {
|
||||||
const loadMercureInfo = createAsyncThunk(GET_MERCURE_INFO, (_: void, { getState }): Promise<ShlinkMercureInfo> => {
|
const loadMercureInfo = createAsyncThunk(
|
||||||
const { settings } = getState();
|
`${REDUCER_PREFIX}/loadMercureInfo`,
|
||||||
if (!settings.realTimeUpdates.enabled) {
|
(_: void, { getState }): Promise<ShlinkMercureInfo> => {
|
||||||
throw new Error('Real time updates not enabled');
|
const { settings } = getState();
|
||||||
}
|
if (!settings.realTimeUpdates.enabled) {
|
||||||
|
throw new Error('Real time updates not enabled');
|
||||||
|
}
|
||||||
|
|
||||||
return buildShlinkApiClient(getState).mercureInfo();
|
return buildShlinkApiClient(getState).mercureInfo();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const { reducer } = createSlice({
|
const { reducer } = createSlice({
|
||||||
name: 'mercureInfoReducer',
|
name: REDUCER_PREFIX,
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {},
|
reducers: {},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
|
|
|
@ -2,13 +2,11 @@ import { IContainer } from 'bottlejs';
|
||||||
import { combineReducers } from 'redux';
|
import { combineReducers } from 'redux';
|
||||||
import { serversReducer } from '../servers/reducers/servers';
|
import { serversReducer } from '../servers/reducers/servers';
|
||||||
import selectedServerReducer from '../servers/reducers/selectedServer';
|
import selectedServerReducer from '../servers/reducers/selectedServer';
|
||||||
import shortUrlsListReducer from '../short-urls/reducers/shortUrlsList';
|
|
||||||
import shortUrlVisitsReducer from '../visits/reducers/shortUrlVisits';
|
import shortUrlVisitsReducer from '../visits/reducers/shortUrlVisits';
|
||||||
import tagVisitsReducer from '../visits/reducers/tagVisits';
|
import tagVisitsReducer from '../visits/reducers/tagVisits';
|
||||||
import domainVisitsReducer from '../visits/reducers/domainVisits';
|
import domainVisitsReducer from '../visits/reducers/domainVisits';
|
||||||
import orphanVisitsReducer from '../visits/reducers/orphanVisits';
|
import orphanVisitsReducer from '../visits/reducers/orphanVisits';
|
||||||
import nonOrphanVisitsReducer from '../visits/reducers/nonOrphanVisits';
|
import nonOrphanVisitsReducer from '../visits/reducers/nonOrphanVisits';
|
||||||
import tagsListReducer from '../tags/reducers/tagsList';
|
|
||||||
import { settingsReducer } from '../settings/reducers/settings';
|
import { settingsReducer } from '../settings/reducers/settings';
|
||||||
import visitsOverviewReducer from '../visits/reducers/visitsOverview';
|
import visitsOverviewReducer from '../visits/reducers/visitsOverview';
|
||||||
import { appUpdatesReducer } from '../app/reducers/appUpdates';
|
import { appUpdatesReducer } from '../app/reducers/appUpdates';
|
||||||
|
@ -18,7 +16,7 @@ import { ShlinkState } from '../container/types';
|
||||||
export default (container: IContainer) => combineReducers<ShlinkState>({
|
export default (container: IContainer) => combineReducers<ShlinkState>({
|
||||||
servers: serversReducer,
|
servers: serversReducer,
|
||||||
selectedServer: selectedServerReducer,
|
selectedServer: selectedServerReducer,
|
||||||
shortUrlsList: shortUrlsListReducer,
|
shortUrlsList: container.shortUrlsListReducer,
|
||||||
shortUrlCreation: container.shortUrlCreationReducer,
|
shortUrlCreation: container.shortUrlCreationReducer,
|
||||||
shortUrlDeletion: container.shortUrlDeletionReducer,
|
shortUrlDeletion: container.shortUrlDeletionReducer,
|
||||||
shortUrlEdition: container.shortUrlEditionReducer,
|
shortUrlEdition: container.shortUrlEditionReducer,
|
||||||
|
@ -28,7 +26,7 @@ export default (container: IContainer) => combineReducers<ShlinkState>({
|
||||||
domainVisits: domainVisitsReducer,
|
domainVisits: domainVisitsReducer,
|
||||||
orphanVisits: orphanVisitsReducer,
|
orphanVisits: orphanVisitsReducer,
|
||||||
nonOrphanVisits: nonOrphanVisitsReducer,
|
nonOrphanVisits: nonOrphanVisitsReducer,
|
||||||
tagsList: tagsListReducer,
|
tagsList: container.tagsListReducer,
|
||||||
tagDelete: container.tagDeleteReducer,
|
tagDelete: container.tagDeleteReducer,
|
||||||
tagEdit: container.tagEditReducer,
|
tagEdit: container.tagEditReducer,
|
||||||
mercureInfo: container.mercureInfoReducer,
|
mercureInfo: container.mercureInfoReducer,
|
||||||
|
|
|
@ -26,7 +26,7 @@ const serverWithId = (server: ServerWithId | ServerData): ServerWithId => {
|
||||||
const serversListToMap = reduce<ServerWithId, ServersMap>((acc, server) => assoc(server.id, server, acc), {});
|
const serversListToMap = reduce<ServerWithId, ServersMap>((acc, server) => assoc(server.id, server, acc), {});
|
||||||
|
|
||||||
export const { actions, reducer } = createSlice({
|
export const { actions, reducer } = createSlice({
|
||||||
name: 'serversReducer',
|
name: 'shlink/servers',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
editServer: {
|
editServer: {
|
||||||
|
|
|
@ -82,7 +82,7 @@ const toReducer = (prepare: SettingsPrepareAction) => ({ reducer: commonReducer,
|
||||||
const toPreparedAction: SettingsPrepareAction = (payload: Settings) => ({ payload });
|
const toPreparedAction: SettingsPrepareAction = (payload: Settings) => ({ payload });
|
||||||
|
|
||||||
const { reducer, actions } = createSlice({
|
const { reducer, actions } = createSlice({
|
||||||
name: 'settingsReducer',
|
name: 'shlink/settings',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
toggleRealTimeUpdates: toReducer((enabled: boolean) => toPreparedAction({ realTimeUpdates: { enabled } })),
|
toggleRealTimeUpdates: toReducer((enabled: boolean) => toPreparedAction({ realTimeUpdates: { enabled } })),
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilde
|
||||||
import { parseApiError } from '../../api/utils';
|
import { parseApiError } from '../../api/utils';
|
||||||
import { ProblemDetailsError } from '../../api/types/errors';
|
import { ProblemDetailsError } from '../../api/types/errors';
|
||||||
|
|
||||||
export const CREATE_SHORT_URL = 'shlink/createShortUrl/CREATE_SHORT_URL';
|
const REDUCER_PREFIX = 'shlink/shortUrlCreation';
|
||||||
|
|
||||||
export type ShortUrlCreation = {
|
export type ShortUrlCreation = {
|
||||||
saving: false;
|
saving: false;
|
||||||
|
@ -35,26 +35,29 @@ const initialState: ShortUrlCreation = {
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const shortUrlCreationReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder) => {
|
export const createShortUrl = (buildShlinkApiClient: ShlinkApiClientBuilder) => createAsyncThunk(
|
||||||
const createShortUrl = createAsyncThunk(CREATE_SHORT_URL, (data: ShortUrlData, { getState }): Promise<ShortUrl> => {
|
`${REDUCER_PREFIX}/createShortUrl`,
|
||||||
|
(data: ShortUrlData, { getState }): Promise<ShortUrl> => {
|
||||||
const { createShortUrl: shlinkCreateShortUrl } = buildShlinkApiClient(getState);
|
const { createShortUrl: shlinkCreateShortUrl } = buildShlinkApiClient(getState);
|
||||||
return shlinkCreateShortUrl(data);
|
return shlinkCreateShortUrl(data);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const shortUrlCreationReducerCreator = (createShortUrlThunk: ReturnType<typeof createShortUrl>) => {
|
||||||
const { reducer, actions } = createSlice({
|
const { reducer, actions } = createSlice({
|
||||||
name: 'shortUrlCreationReducer',
|
name: REDUCER_PREFIX,
|
||||||
initialState: initialState as ShortUrlCreation, // Without this casting it infers type ShortUrlCreationWaiting
|
initialState: initialState as ShortUrlCreation, // Without this casting it infers type ShortUrlCreationWaiting
|
||||||
reducers: {
|
reducers: {
|
||||||
resetCreateShortUrl: () => initialState,
|
resetCreateShortUrl: () => initialState,
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addCase(createShortUrl.pending, () => ({ saving: true, saved: false, error: false }));
|
builder.addCase(createShortUrlThunk.pending, () => ({ saving: true, saved: false, error: false }));
|
||||||
builder.addCase(
|
builder.addCase(
|
||||||
createShortUrl.rejected,
|
createShortUrlThunk.rejected,
|
||||||
(_, { error }) => ({ saving: false, saved: false, error: true, errorData: parseApiError(error) }),
|
(_, { error }) => ({ saving: false, saved: false, error: true, errorData: parseApiError(error) }),
|
||||||
);
|
);
|
||||||
builder.addCase(
|
builder.addCase(
|
||||||
createShortUrl.fulfilled,
|
createShortUrlThunk.fulfilled,
|
||||||
(_, { payload: result }) => ({ result, saving: false, saved: true, error: false }),
|
(_, { payload: result }) => ({ result, saving: false, saved: true, error: false }),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -64,7 +67,6 @@ export const shortUrlCreationReducerCreator = (buildShlinkApiClient: ShlinkApiCl
|
||||||
|
|
||||||
return {
|
return {
|
||||||
reducer,
|
reducer,
|
||||||
createShortUrl,
|
|
||||||
resetCreateShortUrl,
|
resetCreateShortUrl,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import { createAsyncThunk } from '../../utils/helpers/redux';
|
import { createAsyncThunk } from '../../utils/helpers/redux';
|
||||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||||
import { parseApiError } from '../../api/utils';
|
import { parseApiError } from '../../api/utils';
|
||||||
import { ProblemDetailsError } from '../../api/types/errors';
|
import { ProblemDetailsError } from '../../api/types/errors';
|
||||||
import { ShortUrlIdentifier } from '../data';
|
import { ShortUrlIdentifier } from '../data';
|
||||||
|
|
||||||
export const SHORT_URL_DELETED = 'shlink/deleteShortUrl/SHORT_URL_DELETED';
|
const REDUCER_PREFIX = 'shlink/shortUrlDeletion';
|
||||||
|
|
||||||
export interface ShortUrlDeletion {
|
export interface ShortUrlDeletion {
|
||||||
shortCode: string;
|
shortCode: string;
|
||||||
|
@ -15,8 +15,6 @@ export interface ShortUrlDeletion {
|
||||||
errorData?: ProblemDetailsError;
|
errorData?: ProblemDetailsError;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DeleteShortUrlAction = PayloadAction<ShortUrlIdentifier>;
|
|
||||||
|
|
||||||
const initialState: ShortUrlDeletion = {
|
const initialState: ShortUrlDeletion = {
|
||||||
shortCode: '',
|
shortCode: '',
|
||||||
loading: false,
|
loading: false,
|
||||||
|
@ -24,28 +22,31 @@ const initialState: ShortUrlDeletion = {
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const shortUrlDeletionReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder) => {
|
export const deleteShortUrl = (buildShlinkApiClient: ShlinkApiClientBuilder) => createAsyncThunk(
|
||||||
const deleteShortUrl = createAsyncThunk(
|
`${REDUCER_PREFIX}/deleteShortUrl`,
|
||||||
SHORT_URL_DELETED,
|
async ({ shortCode, domain }: ShortUrlIdentifier, { getState }): Promise<ShortUrlIdentifier> => {
|
||||||
async ({ shortCode, domain }: ShortUrlIdentifier, { getState }): Promise<ShortUrlIdentifier> => {
|
const { deleteShortUrl: shlinkDeleteShortUrl } = buildShlinkApiClient(getState);
|
||||||
const { deleteShortUrl: shlinkDeleteShortUrl } = buildShlinkApiClient(getState);
|
await shlinkDeleteShortUrl(shortCode, domain);
|
||||||
await shlinkDeleteShortUrl(shortCode, domain);
|
return { shortCode, domain };
|
||||||
return { shortCode, domain };
|
},
|
||||||
},
|
);
|
||||||
);
|
|
||||||
|
|
||||||
|
export const shortUrlDeletionReducerCreator = (deleteShortUrlThunk: ReturnType<typeof deleteShortUrl>) => {
|
||||||
const { actions, reducer } = createSlice({
|
const { actions, reducer } = createSlice({
|
||||||
name: 'shortUrlDeletion',
|
name: REDUCER_PREFIX,
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
resetDeleteShortUrl: () => initialState,
|
resetDeleteShortUrl: () => initialState,
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addCase(deleteShortUrl.pending, (state) => ({ ...state, loading: true, error: false, deleted: false }));
|
builder.addCase(
|
||||||
builder.addCase(deleteShortUrl.rejected, (state, { error }) => (
|
deleteShortUrlThunk.pending,
|
||||||
|
(state) => ({ ...state, loading: true, error: false, deleted: false }),
|
||||||
|
);
|
||||||
|
builder.addCase(deleteShortUrlThunk.rejected, (state, { error }) => (
|
||||||
{ ...state, errorData: parseApiError(error), loading: false, error: true, deleted: false }
|
{ ...state, errorData: parseApiError(error), loading: false, error: true, deleted: false }
|
||||||
));
|
));
|
||||||
builder.addCase(deleteShortUrl.fulfilled, (state, { payload }) => (
|
builder.addCase(deleteShortUrlThunk.fulfilled, (state, { payload }) => (
|
||||||
{ ...state, shortCode: payload.shortCode, loading: false, error: false, deleted: true }
|
{ ...state, shortCode: payload.shortCode, loading: false, error: false, deleted: true }
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
|
@ -53,5 +54,5 @@ export const shortUrlDeletionReducerCreator = (buildShlinkApiClient: ShlinkApiCl
|
||||||
|
|
||||||
const { resetDeleteShortUrl } = actions;
|
const { resetDeleteShortUrl } = actions;
|
||||||
|
|
||||||
return { reducer, deleteShortUrl, resetDeleteShortUrl };
|
return { reducer, resetDeleteShortUrl };
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { shortUrlMatches } from '../helpers';
|
||||||
import { parseApiError } from '../../api/utils';
|
import { parseApiError } from '../../api/utils';
|
||||||
import { ProblemDetailsError } from '../../api/types/errors';
|
import { ProblemDetailsError } from '../../api/types/errors';
|
||||||
|
|
||||||
const GET_SHORT_URL_DETAIL = 'shlink/shortUrlDetail/GET_SHORT_URL_DETAIL';
|
const REDUCER_PREFIX = 'shlink/shortUrlDetail';
|
||||||
|
|
||||||
export interface ShortUrlDetail {
|
export interface ShortUrlDetail {
|
||||||
shortUrl?: ShortUrl;
|
shortUrl?: ShortUrl;
|
||||||
|
@ -24,7 +24,7 @@ const initialState: ShortUrlDetail = {
|
||||||
|
|
||||||
export const shortUrlDetailReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder) => {
|
export const shortUrlDetailReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder) => {
|
||||||
const getShortUrlDetail = createAsyncThunk(
|
const getShortUrlDetail = createAsyncThunk(
|
||||||
GET_SHORT_URL_DETAIL,
|
`${REDUCER_PREFIX}/getShortUrlDetail`,
|
||||||
async ({ shortCode, domain }: ShortUrlIdentifier, { getState }): Promise<ShortUrl> => {
|
async ({ shortCode, domain }: ShortUrlIdentifier, { getState }): Promise<ShortUrl> => {
|
||||||
const { shortUrlsList } = getState();
|
const { shortUrlsList } = getState();
|
||||||
const alreadyLoaded = shortUrlsList?.shortUrls?.data.find((url) => shortUrlMatches(url, shortCode, domain));
|
const alreadyLoaded = shortUrlsList?.shortUrls?.data.find((url) => shortUrlMatches(url, shortCode, domain));
|
||||||
|
@ -34,7 +34,7 @@ export const shortUrlDetailReducerCreator = (buildShlinkApiClient: ShlinkApiClie
|
||||||
);
|
);
|
||||||
|
|
||||||
const { reducer } = createSlice({
|
const { reducer } = createSlice({
|
||||||
name: 'shortUrlDetailReducer',
|
name: REDUCER_PREFIX,
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {},
|
reducers: {},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilde
|
||||||
import { parseApiError } from '../../api/utils';
|
import { parseApiError } from '../../api/utils';
|
||||||
import { ProblemDetailsError } from '../../api/types/errors';
|
import { ProblemDetailsError } from '../../api/types/errors';
|
||||||
|
|
||||||
export const SHORT_URL_EDITED = 'shlink/shortUrlEdition/SHORT_URL_EDITED';
|
const REDUCER_PREFIX = 'shlink/shortUrlEdition';
|
||||||
|
|
||||||
export interface ShortUrlEdition {
|
export interface ShortUrlEdition {
|
||||||
shortUrl?: ShortUrl;
|
shortUrl?: ShortUrl;
|
||||||
|
@ -27,31 +27,27 @@ const initialState: ShortUrlEdition = {
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const shortUrlEditionReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder) => {
|
export const editShortUrl = (buildShlinkApiClient: ShlinkApiClientBuilder) => createAsyncThunk(
|
||||||
const editShortUrl = createAsyncThunk(
|
`${REDUCER_PREFIX}/editShortUrl`,
|
||||||
SHORT_URL_EDITED,
|
({ shortCode, domain, data }: EditShortUrl, { getState }): Promise<ShortUrl> => {
|
||||||
({ shortCode, domain, data }: EditShortUrl, { getState }): Promise<ShortUrl> => {
|
const { updateShortUrl } = buildShlinkApiClient(getState);
|
||||||
const { updateShortUrl } = buildShlinkApiClient(getState);
|
return updateShortUrl(shortCode, domain, data as any); // FIXME parse dates
|
||||||
return updateShortUrl(shortCode, domain, data as any); // FIXME parse dates
|
},
|
||||||
},
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const { reducer } = createSlice({
|
export const shortUrlEditionReducerCreator = (editShortUrlThunk: ReturnType<typeof editShortUrl>) => createSlice({
|
||||||
name: 'shortUrlEditionReducer',
|
name: REDUCER_PREFIX,
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {},
|
reducers: {},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addCase(editShortUrl.pending, (state) => ({ ...state, saving: true, error: false, saved: false }));
|
builder.addCase(editShortUrlThunk.pending, (state) => ({ ...state, saving: true, error: false, saved: false }));
|
||||||
builder.addCase(
|
builder.addCase(
|
||||||
editShortUrl.rejected,
|
editShortUrlThunk.rejected,
|
||||||
(state, { error }) => ({ ...state, saving: false, error: true, saved: false, errorData: parseApiError(error) }),
|
(state, { error }) => ({ ...state, saving: false, error: true, saved: false, errorData: parseApiError(error) }),
|
||||||
);
|
);
|
||||||
builder.addCase(
|
builder.addCase(
|
||||||
editShortUrl.fulfilled,
|
editShortUrlThunk.fulfilled,
|
||||||
(_, { payload: shortUrl }) => ({ shortUrl, saving: false, error: false, saved: true }),
|
(_, { payload: shortUrl }) => ({ shortUrl, saving: false, error: false, saved: true }),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return { reducer, editShortUrl };
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,20 +1,16 @@
|
||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import { assoc, assocPath, last, pipe, reject } from 'ramda';
|
import { assoc, assocPath, last, pipe, reject } from 'ramda';
|
||||||
import { Action, Dispatch } from 'redux';
|
|
||||||
import { shortUrlMatches } from '../helpers';
|
import { shortUrlMatches } from '../helpers';
|
||||||
import { createNewVisits, CreateVisitsAction } from '../../visits/reducers/visitCreation';
|
import { createNewVisits } from '../../visits/reducers/visitCreation';
|
||||||
import { buildReducer } from '../../utils/helpers/redux';
|
import { createAsyncThunk } from '../../utils/helpers/redux';
|
||||||
import { GetState } from '../../container/types';
|
|
||||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||||
import { ShlinkShortUrlsListParams, ShlinkShortUrlsResponse } from '../../api/types';
|
import { ShlinkShortUrlsListParams, ShlinkShortUrlsResponse } from '../../api/types';
|
||||||
import { DeleteShortUrlAction, SHORT_URL_DELETED } from './shortUrlDeletion';
|
import { deleteShortUrl } from './shortUrlDeletion';
|
||||||
import { CREATE_SHORT_URL, CreateShortUrlAction } from './shortUrlCreation';
|
import { createShortUrl } from './shortUrlCreation';
|
||||||
import { SHORT_URL_EDITED, ShortUrlEditedAction } from './shortUrlEdition';
|
import { editShortUrl } from './shortUrlEdition';
|
||||||
import { ShortUrl } from '../data';
|
import { ShortUrl } from '../data';
|
||||||
|
|
||||||
export const LIST_SHORT_URLS_START = 'shlink/shortUrlsList/LIST_SHORT_URLS_START';
|
const REDUCER_PREFIX = 'shlink/shortUrlsList';
|
||||||
export const LIST_SHORT_URLS_ERROR = 'shlink/shortUrlsList/LIST_SHORT_URLS_ERROR';
|
|
||||||
export const LIST_SHORT_URLS = 'shlink/shortUrlsList/LIST_SHORT_URLS';
|
|
||||||
|
|
||||||
export const ITEMS_IN_OVERVIEW_PAGE = 5;
|
export const ITEMS_IN_OVERVIEW_PAGE = 5;
|
||||||
|
|
||||||
export interface ShortUrlsList {
|
export interface ShortUrlsList {
|
||||||
|
@ -23,96 +19,104 @@ export interface ShortUrlsList {
|
||||||
error: boolean;
|
error: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListShortUrlsAction extends Action<string> {
|
|
||||||
shortUrls: ShlinkShortUrlsResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ListShortUrlsCombinedAction = (
|
|
||||||
ListShortUrlsAction
|
|
||||||
& CreateVisitsAction
|
|
||||||
& CreateShortUrlAction
|
|
||||||
& DeleteShortUrlAction
|
|
||||||
& ShortUrlEditedAction
|
|
||||||
);
|
|
||||||
|
|
||||||
const initialState: ShortUrlsList = {
|
const initialState: ShortUrlsList = {
|
||||||
loading: true,
|
loading: true,
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default buildReducer<ShortUrlsList, ListShortUrlsCombinedAction>({
|
export const listShortUrls = (buildShlinkApiClient: ShlinkApiClientBuilder) => createAsyncThunk(
|
||||||
[LIST_SHORT_URLS_START]: (state) => ({ ...state, loading: true, error: false }),
|
`${REDUCER_PREFIX}/listShortUrls`,
|
||||||
[LIST_SHORT_URLS_ERROR]: () => ({ loading: false, error: true }),
|
(params: ShlinkShortUrlsListParams | void, { getState }): Promise<ShlinkShortUrlsResponse> => {
|
||||||
[LIST_SHORT_URLS]: (_, { shortUrls }) => ({ loading: false, error: false, shortUrls }),
|
const { listShortUrls: shlinkListShortUrls } = buildShlinkApiClient(getState);
|
||||||
[`${SHORT_URL_DELETED}/fulfilled`]: pipe( // TODO Do not hardcode action type here
|
return shlinkListShortUrls(params ?? {});
|
||||||
(state: ShortUrlsList, { payload }: DeleteShortUrlAction) => (!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,
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
[createNewVisits.toString()]: (state, { payload }) => assocPath(
|
|
||||||
['shortUrls', 'data'],
|
|
||||||
state.shortUrls?.data?.map(
|
|
||||||
(currentShortUrl) => {
|
|
||||||
// Find the last of the new visit for this short URL, and pick the amount of visits from it
|
|
||||||
const lastVisit = last(
|
|
||||||
payload.createdVisits.filter(
|
|
||||||
({ shortUrl }) => shortUrl && shortUrlMatches(currentShortUrl, shortUrl.shortCode, shortUrl.domain),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return lastVisit?.shortUrl
|
export const shortUrlsListReducerCreator = (
|
||||||
? assoc('visitsCount', lastVisit.shortUrl.visitsCount, currentShortUrl)
|
listShortUrlsThunk: ReturnType<typeof listShortUrls>,
|
||||||
: currentShortUrl;
|
editShortUrlThunk: ReturnType<typeof editShortUrl>,
|
||||||
},
|
createShortUrlThunk: ReturnType<typeof createShortUrl>,
|
||||||
),
|
deleteShortUrlThunk: ReturnType<typeof deleteShortUrl>,
|
||||||
state,
|
) => createSlice({
|
||||||
),
|
name: REDUCER_PREFIX,
|
||||||
[`${CREATE_SHORT_URL}/fulfilled`]: pipe( // TODO Do not hardcode action type here
|
initialState,
|
||||||
// The only place where the list and the creation form coexist is the overview page.
|
reducers: {},
|
||||||
// There we can assume we are displaying page 1, and therefore, we can safely prepend the new short URL.
|
extraReducers: (builder) => {
|
||||||
// We can also remove the items above the amount that is displayed there.
|
builder.addCase(listShortUrlsThunk.pending, (state) => ({ ...state, loading: true, error: false }));
|
||||||
(state: ShortUrlsList, { payload }: CreateShortUrlAction) => (!state.shortUrls ? state : assocPath(
|
builder.addCase(listShortUrlsThunk.rejected, () => ({ loading: false, error: true }));
|
||||||
['shortUrls', 'data'],
|
builder.addCase(
|
||||||
[payload, ...state.shortUrls.data.slice(0, ITEMS_IN_OVERVIEW_PAGE - 1)],
|
listShortUrlsThunk.fulfilled,
|
||||||
state,
|
(_, { payload: shortUrls }) => ({ loading: false, error: false, shortUrls }),
|
||||||
)),
|
);
|
||||||
(state: ShortUrlsList) => (!state.shortUrls ? state : assocPath(
|
|
||||||
['shortUrls', 'pagination', 'totalItems'],
|
|
||||||
state.shortUrls.pagination.totalItems + 1,
|
|
||||||
state,
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
// TODO Do not hardcode action type here
|
|
||||||
[`${SHORT_URL_EDITED}/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;
|
builder.addCase(
|
||||||
}),
|
createShortUrlThunk.fulfilled,
|
||||||
state,
|
pipe(
|
||||||
)),
|
// The only place where the list and the creation form coexist is the overview page.
|
||||||
}, initialState);
|
// 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,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
export const listShortUrls = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
|
builder.addCase(
|
||||||
params: ShlinkShortUrlsListParams = {},
|
editShortUrlThunk.fulfilled,
|
||||||
) => async (dispatch: Dispatch, getState: GetState) => {
|
(state, { payload: editedShortUrl }) => (!state.shortUrls ? state : assocPath(
|
||||||
dispatch({ type: LIST_SHORT_URLS_START });
|
['shortUrls', 'data'],
|
||||||
const { listShortUrls: shlinkListShortUrls } = buildShlinkApiClient(getState);
|
state.shortUrls.data.map((shortUrl) => {
|
||||||
|
const { shortCode, domain } = editedShortUrl;
|
||||||
|
return shortUrlMatches(shortUrl, shortCode, domain) ? editedShortUrl : shortUrl;
|
||||||
|
}),
|
||||||
|
state,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
builder.addCase(
|
||||||
const shortUrls = await shlinkListShortUrls(params);
|
deleteShortUrlThunk.fulfilled,
|
||||||
|
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,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
dispatch<ListShortUrlsAction>({ type: LIST_SHORT_URLS, shortUrls });
|
builder.addCase(
|
||||||
} catch (e) {
|
createNewVisits,
|
||||||
dispatch({ type: LIST_SHORT_URLS_ERROR });
|
(state, { payload }) => assocPath(
|
||||||
}
|
['shortUrls', 'data'],
|
||||||
};
|
state.shortUrls?.data?.map(
|
||||||
|
(currentShortUrl) => {
|
||||||
|
// Find the last of the new visit for this short URL, and pick the amount of visits from it
|
||||||
|
const lastVisit = last(
|
||||||
|
payload.createdVisits.filter(
|
||||||
|
({ shortUrl }) => shortUrl && shortUrlMatches(currentShortUrl, shortUrl.shortCode, shortUrl.domain),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return lastVisit?.shortUrl
|
||||||
|
? assoc('visitsCount', lastVisit.shortUrl.visitsCount, currentShortUrl)
|
||||||
|
: currentShortUrl;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
state,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -7,10 +7,10 @@ import { ShortUrlsRowMenu } from '../helpers/ShortUrlsRowMenu';
|
||||||
import { CreateShortUrl } from '../CreateShortUrl';
|
import { CreateShortUrl } from '../CreateShortUrl';
|
||||||
import { DeleteShortUrlModal } from '../helpers/DeleteShortUrlModal';
|
import { DeleteShortUrlModal } from '../helpers/DeleteShortUrlModal';
|
||||||
import { CreateShortUrlResult } from '../helpers/CreateShortUrlResult';
|
import { CreateShortUrlResult } from '../helpers/CreateShortUrlResult';
|
||||||
import { listShortUrls } from '../reducers/shortUrlsList';
|
import { listShortUrls, shortUrlsListReducerCreator } from '../reducers/shortUrlsList';
|
||||||
import { shortUrlCreationReducerCreator } from '../reducers/shortUrlCreation';
|
import { shortUrlCreationReducerCreator, createShortUrl } from '../reducers/shortUrlCreation';
|
||||||
import { shortUrlDeletionReducerCreator } from '../reducers/shortUrlDeletion';
|
import { shortUrlDeletionReducerCreator, deleteShortUrl } from '../reducers/shortUrlDeletion';
|
||||||
import { shortUrlEditionReducerCreator } from '../reducers/shortUrlEdition';
|
import { editShortUrl, shortUrlEditionReducerCreator } from '../reducers/shortUrlEdition';
|
||||||
import { shortUrlDetailReducerCreator } from '../reducers/shortUrlDetail';
|
import { shortUrlDetailReducerCreator } from '../reducers/shortUrlDetail';
|
||||||
import { ConnectDecorator } from '../../container/types';
|
import { ConnectDecorator } from '../../container/types';
|
||||||
import { ShortUrlsTable } from '../ShortUrlsTable';
|
import { ShortUrlsTable } from '../ShortUrlsTable';
|
||||||
|
@ -57,13 +57,23 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
bottle.decorator('ExportShortUrlsBtn', connect(['selectedServer']));
|
bottle.decorator('ExportShortUrlsBtn', connect(['selectedServer']));
|
||||||
|
|
||||||
// Reducers
|
// Reducers
|
||||||
bottle.serviceFactory('shortUrlCreationReducerCreator', shortUrlCreationReducerCreator, 'buildShlinkApiClient');
|
bottle.serviceFactory(
|
||||||
|
'shortUrlsListReducerCreator',
|
||||||
|
shortUrlsListReducerCreator,
|
||||||
|
'listShortUrls',
|
||||||
|
'editShortUrl',
|
||||||
|
'createShortUrl',
|
||||||
|
'deleteShortUrl',
|
||||||
|
);
|
||||||
|
bottle.serviceFactory('shortUrlsListReducer', prop('reducer'), 'shortUrlsListReducerCreator');
|
||||||
|
|
||||||
|
bottle.serviceFactory('shortUrlCreationReducerCreator', shortUrlCreationReducerCreator, 'createShortUrl');
|
||||||
bottle.serviceFactory('shortUrlCreationReducer', prop('reducer'), 'shortUrlCreationReducerCreator');
|
bottle.serviceFactory('shortUrlCreationReducer', prop('reducer'), 'shortUrlCreationReducerCreator');
|
||||||
|
|
||||||
bottle.serviceFactory('shortUrlEditionReducerCreator', shortUrlEditionReducerCreator, 'buildShlinkApiClient');
|
bottle.serviceFactory('shortUrlEditionReducerCreator', shortUrlEditionReducerCreator, 'editShortUrl');
|
||||||
bottle.serviceFactory('shortUrlEditionReducer', prop('reducer'), 'shortUrlEditionReducerCreator');
|
bottle.serviceFactory('shortUrlEditionReducer', prop('reducer'), 'shortUrlEditionReducerCreator');
|
||||||
|
|
||||||
bottle.serviceFactory('shortUrlDeletionReducerCreator', shortUrlDeletionReducerCreator, 'buildShlinkApiClient');
|
bottle.serviceFactory('shortUrlDeletionReducerCreator', shortUrlDeletionReducerCreator, 'deleteShortUrl');
|
||||||
bottle.serviceFactory('shortUrlDeletionReducer', prop('reducer'), 'shortUrlDeletionReducerCreator');
|
bottle.serviceFactory('shortUrlDeletionReducer', prop('reducer'), 'shortUrlDeletionReducerCreator');
|
||||||
|
|
||||||
bottle.serviceFactory('shortUrlDetailReducerCreator', shortUrlDetailReducerCreator, 'buildShlinkApiClient');
|
bottle.serviceFactory('shortUrlDetailReducerCreator', shortUrlDetailReducerCreator, 'buildShlinkApiClient');
|
||||||
|
@ -72,15 +82,15 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
// Actions
|
// Actions
|
||||||
bottle.serviceFactory('listShortUrls', listShortUrls, 'buildShlinkApiClient');
|
bottle.serviceFactory('listShortUrls', listShortUrls, 'buildShlinkApiClient');
|
||||||
|
|
||||||
bottle.serviceFactory('createShortUrl', prop('createShortUrl'), 'shortUrlCreationReducerCreator');
|
bottle.serviceFactory('createShortUrl', createShortUrl, 'buildShlinkApiClient');
|
||||||
bottle.serviceFactory('resetCreateShortUrl', prop('resetCreateShortUrl'), 'shortUrlCreationReducerCreator');
|
bottle.serviceFactory('resetCreateShortUrl', prop('resetCreateShortUrl'), 'shortUrlCreationReducerCreator');
|
||||||
|
|
||||||
bottle.serviceFactory('deleteShortUrl', prop('deleteShortUrl'), 'shortUrlDeletionReducerCreator');
|
bottle.serviceFactory('deleteShortUrl', deleteShortUrl, 'buildShlinkApiClient');
|
||||||
bottle.serviceFactory('resetDeleteShortUrl', prop('resetDeleteShortUrl'), 'shortUrlDeletionReducerCreator');
|
bottle.serviceFactory('resetDeleteShortUrl', prop('resetDeleteShortUrl'), 'shortUrlDeletionReducerCreator');
|
||||||
|
|
||||||
bottle.serviceFactory('getShortUrlDetail', prop('getShortUrlDetail'), 'shortUrlDetailReducerCreator');
|
bottle.serviceFactory('getShortUrlDetail', prop('getShortUrlDetail'), 'shortUrlDetailReducerCreator');
|
||||||
|
|
||||||
bottle.serviceFactory('editShortUrl', prop('editShortUrl'), 'shortUrlEditionReducerCreator');
|
bottle.serviceFactory('editShortUrl', editShortUrl, 'buildShlinkApiClient');
|
||||||
};
|
};
|
||||||
|
|
||||||
export default provideServices;
|
export default provideServices;
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { createAction, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createAction, createSlice } from '@reduxjs/toolkit';
|
||||||
import { createAsyncThunk } from '../../utils/helpers/redux';
|
import { createAsyncThunk } from '../../utils/helpers/redux';
|
||||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||||
import { parseApiError } from '../../api/utils';
|
import { parseApiError } from '../../api/utils';
|
||||||
import { ProblemDetailsError } from '../../api/types/errors';
|
import { ProblemDetailsError } from '../../api/types/errors';
|
||||||
|
|
||||||
const DELETE_TAG = 'shlink/deleteTag/DELETE_TAG';
|
const REDUCER_PREFIX = 'shlink/tagDelete';
|
||||||
const TAG_DELETED = 'shlink/deleteTag/TAG_DELETED';
|
|
||||||
|
|
||||||
export interface TagDeletion {
|
export interface TagDeletion {
|
||||||
deleting: boolean;
|
deleting: boolean;
|
||||||
|
@ -14,24 +13,22 @@ export interface TagDeletion {
|
||||||
errorData?: ProblemDetailsError;
|
errorData?: ProblemDetailsError;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DeleteTagAction = PayloadAction<string>;
|
|
||||||
|
|
||||||
const initialState: TagDeletion = {
|
const initialState: TagDeletion = {
|
||||||
deleting: false,
|
deleting: false,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const tagDeleted = createAction<string>(TAG_DELETED);
|
export const tagDeleted = createAction<string>(`${REDUCER_PREFIX}/tagDeleted`);
|
||||||
|
|
||||||
export const tagDeleteReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder) => {
|
export const tagDeleteReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder) => {
|
||||||
const deleteTag = createAsyncThunk(DELETE_TAG, async (tag: string, { getState }): Promise<void> => {
|
const deleteTag = createAsyncThunk(`${REDUCER_PREFIX}/deleteTag`, async (tag: string, { getState }): Promise<void> => {
|
||||||
const { deleteTags } = buildShlinkApiClient(getState);
|
const { deleteTags } = buildShlinkApiClient(getState);
|
||||||
await deleteTags([tag]);
|
await deleteTags([tag]);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { reducer } = createSlice({
|
const { reducer } = createSlice({
|
||||||
name: 'tagDeleteReducer',
|
name: REDUCER_PREFIX,
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {},
|
reducers: {},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
|
|
|
@ -6,8 +6,7 @@ import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilde
|
||||||
import { parseApiError } from '../../api/utils';
|
import { parseApiError } from '../../api/utils';
|
||||||
import { ProblemDetailsError } from '../../api/types/errors';
|
import { ProblemDetailsError } from '../../api/types/errors';
|
||||||
|
|
||||||
const EDIT_TAG = 'shlink/editTag/EDIT_TAG';
|
const REDUCER_PREFIX = 'shlink/tagEdit';
|
||||||
const TAG_EDITED = 'shlink/editTag/TAG_EDITED';
|
|
||||||
|
|
||||||
export interface TagEdition {
|
export interface TagEdition {
|
||||||
oldName?: string;
|
oldName?: string;
|
||||||
|
@ -32,11 +31,11 @@ const initialState: TagEdition = {
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const tagEdited = createAction<EditTag>(TAG_EDITED);
|
export const tagEdited = createAction<EditTag>(`${REDUCER_PREFIX}/tagEdited`);
|
||||||
|
|
||||||
export const tagEditReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder, colorGenerator: ColorGenerator) => {
|
export const tagEditReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder, colorGenerator: ColorGenerator) => {
|
||||||
const editTag = createAsyncThunk(
|
const editTag = createAsyncThunk(
|
||||||
EDIT_TAG,
|
`${REDUCER_PREFIX}/editTag`,
|
||||||
async ({ oldName, newName, color }: EditTag, { getState }): Promise<EditTag> => {
|
async ({ oldName, newName, color }: EditTag, { getState }): Promise<EditTag> => {
|
||||||
await buildShlinkApiClient(getState).editTag(oldName, newName);
|
await buildShlinkApiClient(getState).editTag(oldName, newName);
|
||||||
colorGenerator.setColorForKey(newName, color);
|
colorGenerator.setColorForKey(newName, color);
|
||||||
|
@ -46,7 +45,7 @@ export const tagEditReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuild
|
||||||
);
|
);
|
||||||
|
|
||||||
const { reducer } = createSlice({
|
const { reducer } = createSlice({
|
||||||
name: 'tagEditReducer',
|
name: REDUCER_PREFIX,
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {},
|
reducers: {},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
|
|
|
@ -1,23 +1,18 @@
|
||||||
|
import { createAction, createSlice } from '@reduxjs/toolkit';
|
||||||
import { isEmpty, reject } from 'ramda';
|
import { isEmpty, reject } from 'ramda';
|
||||||
import { Action, Dispatch } from 'redux';
|
import { createNewVisits } from '../../visits/reducers/visitCreation';
|
||||||
import { createNewVisits, CreateVisitsAction } from '../../visits/reducers/visitCreation';
|
import { createAsyncThunk } from '../../utils/helpers/redux';
|
||||||
import { buildReducer } from '../../utils/helpers/redux';
|
|
||||||
import { ShlinkTags } from '../../api/types';
|
import { ShlinkTags } from '../../api/types';
|
||||||
import { GetState } from '../../container/types';
|
|
||||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||||
import { CreateVisit, Stats } from '../../visits/types';
|
import { CreateVisit, Stats } from '../../visits/types';
|
||||||
import { parseApiError } from '../../api/utils';
|
import { parseApiError } from '../../api/utils';
|
||||||
import { TagStats } from '../data';
|
import { TagStats } from '../data';
|
||||||
import { ApiErrorAction } from '../../api/types/actions';
|
import { createShortUrl } from '../../short-urls/reducers/shortUrlCreation';
|
||||||
import { CREATE_SHORT_URL, CreateShortUrlAction } from '../../short-urls/reducers/shortUrlCreation';
|
import { tagDeleted } from './tagDelete';
|
||||||
import { DeleteTagAction, tagDeleted } from './tagDelete';
|
import { tagEdited } from './tagEdit';
|
||||||
import { EditTagAction, tagEdited } from './tagEdit';
|
|
||||||
import { ProblemDetailsError } from '../../api/types/errors';
|
import { ProblemDetailsError } from '../../api/types/errors';
|
||||||
|
|
||||||
export const LIST_TAGS_START = 'shlink/tagsList/LIST_TAGS_START';
|
const REDUCER_PREFIX = 'shlink/tagsList';
|
||||||
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';
|
|
||||||
|
|
||||||
type TagsStatsMap = Record<string, TagStats>;
|
type TagsStatsMap = Record<string, TagStats>;
|
||||||
|
|
||||||
|
@ -30,24 +25,12 @@ export interface TagsList {
|
||||||
errorData?: ProblemDetailsError;
|
errorData?: ProblemDetailsError;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ListTagsAction extends Action<string> {
|
interface ListTags {
|
||||||
tags: string[];
|
tags: string[];
|
||||||
stats: TagsStatsMap;
|
stats: TagsStatsMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FilterTagsAction extends Action<string> {
|
const initialState: TagsList = {
|
||||||
searchTerm: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type TagsCombinedAction = ListTagsAction
|
|
||||||
& DeleteTagAction
|
|
||||||
& CreateVisitsAction
|
|
||||||
& CreateShortUrlAction
|
|
||||||
& EditTagAction
|
|
||||||
& FilterTagsAction
|
|
||||||
& ApiErrorAction;
|
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
tags: [],
|
tags: [],
|
||||||
filteredTags: [],
|
filteredTags: [],
|
||||||
stats: {},
|
stats: {},
|
||||||
|
@ -81,47 +64,15 @@ const calculateVisitsPerTag = (createdVisits: CreateVisit[]): TagIncrease[] => O
|
||||||
}, {}),
|
}, {}),
|
||||||
);
|
);
|
||||||
|
|
||||||
export default buildReducer<TagsList, TagsCombinedAction>({
|
export const listTags = (buildShlinkApiClient: ShlinkApiClientBuilder, force = true) => createAsyncThunk(
|
||||||
[LIST_TAGS_START]: () => ({ ...initialState, loading: true }),
|
`${REDUCER_PREFIX}/listTags`,
|
||||||
[LIST_TAGS_ERROR]: (_, { errorData }) => ({ ...initialState, error: true, errorData }),
|
async (_: void, { getState }): Promise<ListTags> => {
|
||||||
[LIST_TAGS]: (_, { tags, stats }) => ({ ...initialState, stats, tags, filteredTags: tags }),
|
const { tagsList } = getState();
|
||||||
[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, { 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) => () => async (
|
if (!force && !isEmpty(tagsList.tags)) {
|
||||||
dispatch: Dispatch,
|
return tagsList;
|
||||||
getState: GetState,
|
}
|
||||||
) => {
|
|
||||||
const { tagsList } = getState();
|
|
||||||
|
|
||||||
if (!force && (tagsList.loading || !isEmpty(tagsList.tags))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({ type: LIST_TAGS_START });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { listTags: shlinkListTags } = buildShlinkApiClient(getState);
|
const { listTags: shlinkListTags } = buildShlinkApiClient(getState);
|
||||||
const { tags, stats = [] }: ShlinkTags = await shlinkListTags();
|
const { tags, stats = [] }: ShlinkTags = await shlinkListTags();
|
||||||
const processedStats = stats.reduce<TagsStatsMap>((acc, { tag, shortUrlsCount, visitsCount }) => {
|
const processedStats = stats.reduce<TagsStatsMap>((acc, { tag, shortUrlsCount, visitsCount }) => {
|
||||||
|
@ -130,10 +81,51 @@ export const listTags = (buildShlinkApiClient: ShlinkApiClientBuilder, force = t
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
dispatch<ListTagsAction>({ tags, stats: processedStats, type: LIST_TAGS });
|
return { tags, stats: processedStats };
|
||||||
} catch (e: any) {
|
},
|
||||||
dispatch<ApiErrorAction>({ type: LIST_TAGS_ERROR, errorData: parseApiError(e) });
|
);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const filterTags = (searchTerm: string): FilterTagsAction => ({ type: FILTER_TAGS, searchTerm });
|
export const filterTags = createAction<string>(`${REDUCER_PREFIX}/filterTags`);
|
||||||
|
|
||||||
|
export const tagsListReducerCreator = (
|
||||||
|
listTagsThunk: ReturnType<typeof listTags>,
|
||||||
|
createShortUrlThunk: ReturnType<typeof createShortUrl>,
|
||||||
|
) => createSlice({
|
||||||
|
name: REDUCER_PREFIX,
|
||||||
|
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),
|
||||||
|
}));
|
||||||
|
|
||||||
|
builder.addCase(createShortUrlThunk.fulfilled, ({ tags: stateTags, ...rest }, { payload }) => ({
|
||||||
|
...rest,
|
||||||
|
tags: stateTags.concat(payload.tags.filter((tag: string) => !stateTags.includes(tag))), // More performant than [ ...new Set(...) ]
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { TagCard } from '../TagCard';
|
||||||
import { DeleteTagConfirmModal } from '../helpers/DeleteTagConfirmModal';
|
import { DeleteTagConfirmModal } from '../helpers/DeleteTagConfirmModal';
|
||||||
import { EditTagModal } from '../helpers/EditTagModal';
|
import { EditTagModal } from '../helpers/EditTagModal';
|
||||||
import { TagsList } from '../TagsList';
|
import { TagsList } from '../TagsList';
|
||||||
import { filterTags, listTags } from '../reducers/tagsList';
|
import { filterTags, listTags, tagsListReducerCreator } from '../reducers/tagsList';
|
||||||
import { tagDeleted, tagDeleteReducerCreator } from '../reducers/tagDelete';
|
import { tagDeleted, tagDeleteReducerCreator } from '../reducers/tagDelete';
|
||||||
import { tagEdited, tagEditReducerCreator } from '../reducers/tagEdit';
|
import { tagEdited, tagEditReducerCreator } from '../reducers/tagEdit';
|
||||||
import { ConnectDecorator } from '../../container/types';
|
import { ConnectDecorator } from '../../container/types';
|
||||||
|
@ -44,6 +44,9 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
bottle.serviceFactory('tagDeleteReducerCreator', tagDeleteReducerCreator, 'buildShlinkApiClient');
|
bottle.serviceFactory('tagDeleteReducerCreator', tagDeleteReducerCreator, 'buildShlinkApiClient');
|
||||||
bottle.serviceFactory('tagDeleteReducer', prop('reducer'), 'tagDeleteReducerCreator');
|
bottle.serviceFactory('tagDeleteReducer', prop('reducer'), 'tagDeleteReducerCreator');
|
||||||
|
|
||||||
|
bottle.serviceFactory('tagsListReducerCreator', tagsListReducerCreator, 'listTags', 'createShortUrl');
|
||||||
|
bottle.serviceFactory('tagsListReducer', prop('reducer'), 'tagsListReducerCreator');
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
const listTagsActionFactory = (force: boolean) =>
|
const listTagsActionFactory = (force: boolean) =>
|
||||||
({ buildShlinkApiClient }: IContainer) => listTags(buildShlinkApiClient, force);
|
({ buildShlinkApiClient }: IContainer) => listTags(buildShlinkApiClient, force);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@import '../utils/base';
|
@import '../base';
|
||||||
|
|
||||||
// Light theme colors
|
// Light theme colors
|
||||||
$lightPrimaryColor: #ffffff;
|
$lightPrimaryColor: #ffffff;
|
|
@ -2,6 +2,7 @@ import { Mock } from 'ts-mockery';
|
||||||
import {
|
import {
|
||||||
CreateShortUrlAction,
|
CreateShortUrlAction,
|
||||||
shortUrlCreationReducerCreator,
|
shortUrlCreationReducerCreator,
|
||||||
|
createShortUrl as createShortUrlCreator,
|
||||||
} from '../../../src/short-urls/reducers/shortUrlCreation';
|
} from '../../../src/short-urls/reducers/shortUrlCreation';
|
||||||
import { ShortUrl } from '../../../src/short-urls/data';
|
import { ShortUrl } from '../../../src/short-urls/data';
|
||||||
import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient';
|
import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient';
|
||||||
|
@ -11,7 +12,8 @@ describe('shortUrlCreationReducer', () => {
|
||||||
const shortUrl = Mock.of<ShortUrl>();
|
const shortUrl = Mock.of<ShortUrl>();
|
||||||
const createShortUrlCall = jest.fn();
|
const createShortUrlCall = jest.fn();
|
||||||
const buildShlinkApiClient = () => Mock.of<ShlinkApiClient>({ createShortUrl: createShortUrlCall });
|
const buildShlinkApiClient = () => Mock.of<ShlinkApiClient>({ createShortUrl: createShortUrlCall });
|
||||||
const { reducer, createShortUrl, resetCreateShortUrl } = shortUrlCreationReducerCreator(buildShlinkApiClient);
|
const createShortUrl = createShortUrlCreator(buildShlinkApiClient);
|
||||||
|
const { reducer, resetCreateShortUrl } = shortUrlCreationReducerCreator(createShortUrl);
|
||||||
|
|
||||||
afterEach(jest.resetAllMocks);
|
afterEach(jest.resetAllMocks);
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import { shortUrlDeletionReducerCreator } from '../../../src/short-urls/reducers/shortUrlDeletion';
|
import {
|
||||||
|
shortUrlDeletionReducerCreator,
|
||||||
|
deleteShortUrl as deleteShortUrlCretor,
|
||||||
|
} from '../../../src/short-urls/reducers/shortUrlDeletion';
|
||||||
import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient';
|
import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient';
|
||||||
import { ProblemDetailsError } from '../../../src/api/types/errors';
|
import { ProblemDetailsError } from '../../../src/api/types/errors';
|
||||||
|
|
||||||
describe('shortUrlDeletionReducer', () => {
|
describe('shortUrlDeletionReducer', () => {
|
||||||
const deleteShortUrlCall = jest.fn();
|
const deleteShortUrlCall = jest.fn();
|
||||||
const buildShlinkApiClient = () => Mock.of<ShlinkApiClient>({ deleteShortUrl: deleteShortUrlCall });
|
const buildShlinkApiClient = () => Mock.of<ShlinkApiClient>({ deleteShortUrl: deleteShortUrlCall });
|
||||||
const { reducer, resetDeleteShortUrl, deleteShortUrl } = shortUrlDeletionReducerCreator(buildShlinkApiClient);
|
const deleteShortUrl = deleteShortUrlCretor(buildShlinkApiClient);
|
||||||
|
const { reducer, resetDeleteShortUrl } = shortUrlDeletionReducerCreator(deleteShortUrl);
|
||||||
|
|
||||||
beforeEach(jest.clearAllMocks);
|
beforeEach(jest.clearAllMocks);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import { ShortUrlEditedAction, shortUrlEditionReducerCreator } from '../../../src/short-urls/reducers/shortUrlEdition';
|
import {
|
||||||
|
ShortUrlEditedAction,
|
||||||
|
shortUrlEditionReducerCreator,
|
||||||
|
editShortUrl as editShortUrlCreator,
|
||||||
|
} from '../../../src/short-urls/reducers/shortUrlEdition';
|
||||||
import { ShlinkState } from '../../../src/container/types';
|
import { ShlinkState } from '../../../src/container/types';
|
||||||
import { ShortUrl } from '../../../src/short-urls/data';
|
import { ShortUrl } from '../../../src/short-urls/data';
|
||||||
import { SelectedServer } from '../../../src/servers/data';
|
import { SelectedServer } from '../../../src/servers/data';
|
||||||
|
@ -10,7 +14,8 @@ describe('shortUrlEditionReducer', () => {
|
||||||
const shortUrl = Mock.of<ShortUrl>({ longUrl, shortCode });
|
const shortUrl = Mock.of<ShortUrl>({ longUrl, shortCode });
|
||||||
const updateShortUrl = jest.fn().mockResolvedValue(shortUrl);
|
const updateShortUrl = jest.fn().mockResolvedValue(shortUrl);
|
||||||
const buildShlinkApiClient = jest.fn().mockReturnValue({ updateShortUrl });
|
const buildShlinkApiClient = jest.fn().mockReturnValue({ updateShortUrl });
|
||||||
const { reducer, editShortUrl } = shortUrlEditionReducerCreator(buildShlinkApiClient);
|
const editShortUrl = editShortUrlCreator(buildShlinkApiClient);
|
||||||
|
const { reducer } = shortUrlEditionReducerCreator(editShortUrl);
|
||||||
|
|
||||||
afterEach(jest.clearAllMocks);
|
afterEach(jest.clearAllMocks);
|
||||||
|
|
||||||
|
|
|
@ -1,37 +1,44 @@
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import reducer, {
|
import {
|
||||||
LIST_SHORT_URLS,
|
listShortUrls as listShortUrlsCreator,
|
||||||
LIST_SHORT_URLS_ERROR,
|
shortUrlsListReducerCreator,
|
||||||
LIST_SHORT_URLS_START,
|
|
||||||
listShortUrls,
|
|
||||||
} from '../../../src/short-urls/reducers/shortUrlsList';
|
} from '../../../src/short-urls/reducers/shortUrlsList';
|
||||||
import { SHORT_URL_DELETED } from '../../../src/short-urls/reducers/shortUrlDeletion';
|
import { deleteShortUrl as deleteShortUrlCreator } from '../../../src/short-urls/reducers/shortUrlDeletion';
|
||||||
|
import { ShlinkPaginator, ShlinkShortUrlsResponse } from '../../../src/api/types';
|
||||||
|
import { createShortUrl as createShortUrlCreator } from '../../../src/short-urls/reducers/shortUrlCreation';
|
||||||
|
import { editShortUrl as editShortUrlCreator } from '../../../src/short-urls/reducers/shortUrlEdition';
|
||||||
|
import { createNewVisits } from '../../../src/visits/reducers/visitCreation';
|
||||||
import { ShortUrl } from '../../../src/short-urls/data';
|
import { ShortUrl } from '../../../src/short-urls/data';
|
||||||
import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient';
|
import { ShlinkApiClient } from '../../../src/api/services/ShlinkApiClient';
|
||||||
import { ShlinkPaginator, ShlinkShortUrlsResponse } from '../../../src/api/types';
|
|
||||||
import { CREATE_SHORT_URL } from '../../../src/short-urls/reducers/shortUrlCreation';
|
|
||||||
import { SHORT_URL_EDITED } from '../../../src/short-urls/reducers/shortUrlEdition';
|
|
||||||
import { createNewVisits } from '../../../src/visits/reducers/visitCreation';
|
|
||||||
|
|
||||||
describe('shortUrlsListReducer', () => {
|
describe('shortUrlsListReducer', () => {
|
||||||
const shortCode = 'abc123';
|
const shortCode = 'abc123';
|
||||||
|
const listShortUrlsMock = jest.fn();
|
||||||
|
const buildShlinkApiClient = () => Mock.of<ShlinkApiClient>({ listShortUrls: listShortUrlsMock });
|
||||||
|
const listShortUrls = listShortUrlsCreator(buildShlinkApiClient);
|
||||||
|
const editShortUrl = editShortUrlCreator(buildShlinkApiClient);
|
||||||
|
const createShortUrl = createShortUrlCreator(buildShlinkApiClient);
|
||||||
|
const deleteShortUrl = deleteShortUrlCreator(buildShlinkApiClient);
|
||||||
|
const { reducer } = shortUrlsListReducerCreator(listShortUrls, editShortUrl, createShortUrl, deleteShortUrl);
|
||||||
|
|
||||||
|
afterEach(jest.clearAllMocks);
|
||||||
|
|
||||||
describe('reducer', () => {
|
describe('reducer', () => {
|
||||||
it('returns loading on LIST_SHORT_URLS_START', () =>
|
it('returns loading on LIST_SHORT_URLS_START', () =>
|
||||||
expect(reducer(undefined, { type: LIST_SHORT_URLS_START } as any)).toEqual({
|
expect(reducer(undefined, { type: listShortUrls.pending.toString() })).toEqual({
|
||||||
loading: true,
|
loading: true,
|
||||||
error: false,
|
error: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('returns short URLs on LIST_SHORT_URLS', () =>
|
it('returns short URLs on LIST_SHORT_URLS', () =>
|
||||||
expect(reducer(undefined, { type: LIST_SHORT_URLS, shortUrls: { data: [] } } as any)).toEqual({
|
expect(reducer(undefined, { type: listShortUrls.fulfilled.toString(), payload: { data: [] } })).toEqual({
|
||||||
shortUrls: { data: [] },
|
shortUrls: { data: [] },
|
||||||
loading: false,
|
loading: false,
|
||||||
error: false,
|
error: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('returns error on LIST_SHORT_URLS_ERROR', () =>
|
it('returns error on LIST_SHORT_URLS_ERROR', () =>
|
||||||
expect(reducer(undefined, { type: LIST_SHORT_URLS_ERROR } as any)).toEqual({
|
expect(reducer(undefined, { type: listShortUrls.rejected.toString() })).toEqual({
|
||||||
loading: false,
|
loading: false,
|
||||||
error: true,
|
error: true,
|
||||||
}));
|
}));
|
||||||
|
@ -52,7 +59,7 @@ describe('shortUrlsListReducer', () => {
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(reducer(state, { type: `${SHORT_URL_DELETED}/fulfilled`, payload: { shortCode } } as any)).toEqual({
|
expect(reducer(state, { type: deleteShortUrl.fulfilled.toString(), payload: { shortCode } })).toEqual({
|
||||||
shortUrls: {
|
shortUrls: {
|
||||||
data: [{ shortCode, domain: 'example.com' }, { shortCode: 'foo' }],
|
data: [{ shortCode, domain: 'example.com' }, { shortCode: 'foo' }],
|
||||||
pagination: { totalItems: 9 },
|
pagination: { totalItems: 9 },
|
||||||
|
@ -85,7 +92,7 @@ describe('shortUrlsListReducer', () => {
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(reducer(state, { type: createNewVisits.toString(), payload: { createdVisits } } as any)).toEqual({
|
expect(reducer(state, { type: createNewVisits.toString(), payload: { createdVisits } })).toEqual({
|
||||||
shortUrls: {
|
shortUrls: {
|
||||||
data: [
|
data: [
|
||||||
{ shortCode, domain: 'example.com', visitsCount: 5 },
|
{ shortCode, domain: 'example.com', visitsCount: 5 },
|
||||||
|
@ -142,7 +149,7 @@ describe('shortUrlsListReducer', () => {
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(reducer(state, { type: `${CREATE_SHORT_URL}/fulfilled`, payload: newShortUrl } as any)).toEqual({
|
expect(reducer(state, { type: createShortUrl.fulfilled.toString(), payload: newShortUrl })).toEqual({
|
||||||
shortUrls: {
|
shortUrls: {
|
||||||
data: expectedData,
|
data: expectedData,
|
||||||
pagination: { totalItems: 16 },
|
pagination: { totalItems: 16 },
|
||||||
|
@ -181,7 +188,7 @@ describe('shortUrlsListReducer', () => {
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = reducer(state, { type: `${SHORT_URL_EDITED}/fulfilled`, payload: editedShortUrl } as any);
|
const result = reducer(state, { type: editShortUrl.fulfilled.toString(), payload: editedShortUrl });
|
||||||
|
|
||||||
expect(result.shortUrls?.data).toEqual(expectedList);
|
expect(result.shortUrls?.data).toEqual(expectedList);
|
||||||
});
|
});
|
||||||
|
@ -191,30 +198,29 @@ describe('shortUrlsListReducer', () => {
|
||||||
const dispatch = jest.fn();
|
const dispatch = jest.fn();
|
||||||
const getState = jest.fn().mockReturnValue({ selectedServer: {} });
|
const getState = jest.fn().mockReturnValue({ selectedServer: {} });
|
||||||
|
|
||||||
afterEach(jest.clearAllMocks);
|
|
||||||
|
|
||||||
it('dispatches proper actions if API client request succeeds', async () => {
|
it('dispatches proper actions if API client request succeeds', async () => {
|
||||||
const listShortUrlsMock = jest.fn().mockResolvedValue([]);
|
listShortUrlsMock.mockResolvedValue({});
|
||||||
const apiClientMock = Mock.of<ShlinkApiClient>({ listShortUrls: listShortUrlsMock });
|
|
||||||
|
|
||||||
await listShortUrls(() => apiClientMock)()(dispatch, getState);
|
await listShortUrls()(dispatch, getState, {});
|
||||||
|
|
||||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||||
expect(dispatch).toHaveBeenNthCalledWith(1, { type: LIST_SHORT_URLS_START });
|
expect(dispatch).toHaveBeenNthCalledWith(1, expect.objectContaining({ type: listShortUrls.pending.toString() }));
|
||||||
expect(dispatch).toHaveBeenNthCalledWith(2, { type: LIST_SHORT_URLS, shortUrls: [] });
|
expect(dispatch).toHaveBeenNthCalledWith(2, expect.objectContaining({
|
||||||
|
type: listShortUrls.fulfilled.toString(),
|
||||||
|
payload: {},
|
||||||
|
}));
|
||||||
|
|
||||||
expect(listShortUrlsMock).toHaveBeenCalledTimes(1);
|
expect(listShortUrlsMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dispatches proper actions if API client request fails', async () => {
|
it('dispatches proper actions if API client request fails', async () => {
|
||||||
const listShortUrlsMock = jest.fn().mockRejectedValue(undefined);
|
listShortUrlsMock.mockRejectedValue(undefined);
|
||||||
const apiClientMock = Mock.of<ShlinkApiClient>({ listShortUrls: listShortUrlsMock });
|
|
||||||
|
|
||||||
await listShortUrls(() => apiClientMock)()(dispatch, getState);
|
await listShortUrls()(dispatch, getState, {});
|
||||||
|
|
||||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||||
expect(dispatch).toHaveBeenNthCalledWith(1, { type: LIST_SHORT_URLS_START });
|
expect(dispatch).toHaveBeenNthCalledWith(1, expect.objectContaining({ type: listShortUrls.pending.toString() }));
|
||||||
expect(dispatch).toHaveBeenNthCalledWith(2, { type: LIST_SHORT_URLS_ERROR });
|
expect(dispatch).toHaveBeenNthCalledWith(2, expect.objectContaining({ type: listShortUrls.rejected.toString() }));
|
||||||
|
|
||||||
expect(listShortUrlsMock).toHaveBeenCalledTimes(1);
|
expect(listShortUrlsMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,32 +1,35 @@
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import reducer, {
|
import {
|
||||||
FILTER_TAGS,
|
|
||||||
filterTags,
|
|
||||||
LIST_TAGS,
|
|
||||||
LIST_TAGS_ERROR,
|
|
||||||
LIST_TAGS_START,
|
|
||||||
listTags,
|
|
||||||
TagsList,
|
TagsList,
|
||||||
|
filterTags,
|
||||||
|
listTags as listTagsCreator,
|
||||||
|
tagsListReducerCreator,
|
||||||
} from '../../../src/tags/reducers/tagsList';
|
} from '../../../src/tags/reducers/tagsList';
|
||||||
import { ShlinkState } from '../../../src/container/types';
|
import { ShlinkState } from '../../../src/container/types';
|
||||||
import { ShortUrl } from '../../../src/short-urls/data';
|
import { ShortUrl } from '../../../src/short-urls/data';
|
||||||
import { CREATE_SHORT_URL } from '../../../src/short-urls/reducers/shortUrlCreation';
|
import { createShortUrl as createShortUrlCreator } from '../../../src/short-urls/reducers/shortUrlCreation';
|
||||||
import { tagEdited } from '../../../src/tags/reducers/tagEdit';
|
import { tagEdited } from '../../../src/tags/reducers/tagEdit';
|
||||||
import { tagDeleted } from '../../../src/tags/reducers/tagDelete';
|
import { tagDeleted } from '../../../src/tags/reducers/tagDelete';
|
||||||
|
|
||||||
describe('tagsListReducer', () => {
|
describe('tagsListReducer', () => {
|
||||||
const state = (props: Partial<TagsList>) => Mock.of<TagsList>(props);
|
const state = (props: Partial<TagsList>) => Mock.of<TagsList>(props);
|
||||||
|
const buildShlinkApiClient = jest.fn();
|
||||||
|
const listTags = listTagsCreator(buildShlinkApiClient, true);
|
||||||
|
const createShortUrl = createShortUrlCreator(buildShlinkApiClient);
|
||||||
|
const { reducer } = tagsListReducerCreator(listTags, createShortUrl);
|
||||||
|
|
||||||
|
afterEach(jest.clearAllMocks);
|
||||||
|
|
||||||
describe('reducer', () => {
|
describe('reducer', () => {
|
||||||
it('returns loading on LIST_TAGS_START', () => {
|
it('returns loading on LIST_TAGS_START', () => {
|
||||||
expect(reducer(undefined, { type: LIST_TAGS_START } as any)).toEqual(expect.objectContaining({
|
expect(reducer(undefined, { type: listTags.pending.toString() })).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(undefined, { type: LIST_TAGS_ERROR } as any)).toEqual(expect.objectContaining({
|
expect(reducer(undefined, { type: listTags.rejected.toString() })).toEqual(expect.objectContaining({
|
||||||
loading: false,
|
loading: false,
|
||||||
error: true,
|
error: true,
|
||||||
}));
|
}));
|
||||||
|
@ -35,7 +38,10 @@ 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(undefined, { type: LIST_TAGS, tags } as any)).toEqual({
|
expect(reducer(undefined, {
|
||||||
|
type: listTags.fulfilled.toString(),
|
||||||
|
payload: { tags },
|
||||||
|
})).toEqual({
|
||||||
tags,
|
tags,
|
||||||
filteredTags: tags,
|
filteredTags: tags,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
@ -50,7 +56,7 @@ describe('tagsListReducer', () => {
|
||||||
|
|
||||||
expect(reducer(
|
expect(reducer(
|
||||||
state({ tags, filteredTags: tags }),
|
state({ tags, filteredTags: tags }),
|
||||||
{ type: tagDeleted.toString(), payload: tag } as any,
|
{ type: tagDeleted.toString(), payload: tag },
|
||||||
)).toEqual({
|
)).toEqual({
|
||||||
tags: expectedTags,
|
tags: expectedTags,
|
||||||
filteredTags: expectedTags,
|
filteredTags: expectedTags,
|
||||||
|
@ -68,7 +74,7 @@ describe('tagsListReducer', () => {
|
||||||
{
|
{
|
||||||
type: tagEdited.toString(),
|
type: tagEdited.toString(),
|
||||||
payload: { oldName, newName },
|
payload: { oldName, newName },
|
||||||
} as any,
|
},
|
||||||
)).toEqual({
|
)).toEqual({
|
||||||
tags: expectedTags,
|
tags: expectedTags,
|
||||||
filteredTags: expectedTags,
|
filteredTags: expectedTags,
|
||||||
|
@ -77,10 +83,10 @@ describe('tagsListReducer', () => {
|
||||||
|
|
||||||
it('filters original list of tags by provided search term on FILTER_TAGS', () => {
|
it('filters original list of tags by provided search term on FILTER_TAGS', () => {
|
||||||
const tags = ['foo', 'bar', 'baz', 'Foo2', 'fo'];
|
const tags = ['foo', 'bar', 'baz', 'Foo2', 'fo'];
|
||||||
const searchTerm = 'Fo';
|
const payload = 'Fo';
|
||||||
const filteredTags = ['foo', 'Foo2', 'fo'];
|
const filteredTags = ['foo', 'Foo2', 'fo'];
|
||||||
|
|
||||||
expect(reducer(state({ tags }), { type: FILTER_TAGS, searchTerm } as any)).toEqual({
|
expect(reducer(state({ tags }), { type: filterTags.toString(), payload })).toEqual({
|
||||||
tags,
|
tags,
|
||||||
filteredTags,
|
filteredTags,
|
||||||
});
|
});
|
||||||
|
@ -94,31 +100,28 @@ describe('tagsListReducer', () => {
|
||||||
const tags = ['foo', 'bar', 'baz', 'foo2', 'fo'];
|
const tags = ['foo', 'bar', 'baz', 'foo2', 'fo'];
|
||||||
const payload = Mock.of<ShortUrl>({ tags: shortUrlTags });
|
const payload = Mock.of<ShortUrl>({ tags: shortUrlTags });
|
||||||
|
|
||||||
expect(reducer(state({ tags }), { type: `${CREATE_SHORT_URL}/fulfilled`, payload } as any)).toEqual({
|
expect(reducer(state({ tags }), { type: createShortUrl.fulfilled.toString(), payload })).toEqual({
|
||||||
tags: expectedTags,
|
tags: expectedTags,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('filterTags', () => {
|
describe('filterTags', () => {
|
||||||
it('creates expected action', () => expect(filterTags('foo')).toEqual({ type: FILTER_TAGS, searchTerm: 'foo' }));
|
it('creates expected action', () => expect(filterTags('foo')).toEqual({ type: filterTags.toString(), payload: 'foo' }));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('listTags', () => {
|
describe('listTags', () => {
|
||||||
const dispatch = jest.fn();
|
const dispatch = jest.fn();
|
||||||
const getState = jest.fn(() => Mock.all<ShlinkState>());
|
const getState = jest.fn(() => Mock.all<ShlinkState>());
|
||||||
const buildShlinkApiClient = jest.fn();
|
|
||||||
const listTagsMock = jest.fn();
|
const listTagsMock = jest.fn();
|
||||||
|
|
||||||
afterEach(jest.clearAllMocks);
|
|
||||||
|
|
||||||
const assertNoAction = async (tagsList: TagsList) => {
|
const assertNoAction = async (tagsList: TagsList) => {
|
||||||
getState.mockReturnValue(Mock.of<ShlinkState>({ tagsList }));
|
getState.mockReturnValue(Mock.of<ShlinkState>({ tagsList }));
|
||||||
|
|
||||||
await listTags(buildShlinkApiClient, false)()(dispatch, getState);
|
await listTagsCreator(buildShlinkApiClient, false)()(dispatch, getState, {});
|
||||||
|
|
||||||
expect(buildShlinkApiClient).not.toHaveBeenCalled();
|
expect(buildShlinkApiClient).not.toHaveBeenCalled();
|
||||||
expect(dispatch).not.toHaveBeenCalled();
|
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||||
expect(getState).toHaveBeenCalledTimes(1);
|
expect(getState).toHaveBeenCalledTimes(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -134,23 +137,26 @@ describe('tagsListReducer', () => {
|
||||||
listTagsMock.mockResolvedValue({ tags, stats: [] });
|
listTagsMock.mockResolvedValue({ tags, stats: [] });
|
||||||
buildShlinkApiClient.mockReturnValue({ listTags: listTagsMock });
|
buildShlinkApiClient.mockReturnValue({ listTags: listTagsMock });
|
||||||
|
|
||||||
await listTags(buildShlinkApiClient, true)()(dispatch, getState);
|
await listTags()(dispatch, getState, {});
|
||||||
|
|
||||||
expect(buildShlinkApiClient).toHaveBeenCalledTimes(1);
|
expect(buildShlinkApiClient).toHaveBeenCalledTimes(1);
|
||||||
expect(getState).toHaveBeenCalledTimes(1);
|
expect(getState).toHaveBeenCalledTimes(1);
|
||||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||||
expect(dispatch).toHaveBeenNthCalledWith(1, { type: LIST_TAGS_START });
|
expect(dispatch).toHaveBeenNthCalledWith(1, expect.objectContaining({ type: listTags.pending.toString() }));
|
||||||
expect(dispatch).toHaveBeenNthCalledWith(2, { type: LIST_TAGS, tags, stats: {} });
|
expect(dispatch).toHaveBeenNthCalledWith(2, expect.objectContaining({
|
||||||
|
type: listTags.fulfilled.toString(),
|
||||||
|
payload: { tags, stats: {} },
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
const assertErrorResult = async () => {
|
const assertErrorResult = async () => {
|
||||||
await listTags(buildShlinkApiClient, true)()(dispatch, getState);
|
await listTags()(dispatch, getState, {});
|
||||||
|
|
||||||
expect(buildShlinkApiClient).toHaveBeenCalledTimes(1);
|
expect(buildShlinkApiClient).toHaveBeenCalledTimes(1);
|
||||||
expect(getState).toHaveBeenCalledTimes(1);
|
expect(getState).toHaveBeenCalledTimes(1);
|
||||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||||
expect(dispatch).toHaveBeenNthCalledWith(1, { type: LIST_TAGS_START });
|
expect(dispatch).toHaveBeenNthCalledWith(1, expect.objectContaining({ type: listTags.pending.toString() }));
|
||||||
expect(dispatch).toHaveBeenNthCalledWith(2, { type: LIST_TAGS_ERROR });
|
expect(dispatch).toHaveBeenNthCalledWith(2, expect.objectContaining({ type: listTags.rejected.toString() }));
|
||||||
};
|
};
|
||||||
|
|
||||||
it('dispatches error when error occurs on list call', async () => {
|
it('dispatches error when error occurs on list call', async () => {
|
||||||
|
@ -168,7 +174,6 @@ describe('tagsListReducer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
await assertErrorResult();
|
await assertErrorResult();
|
||||||
|
|
||||||
expect(listTagsMock).not.toHaveBeenCalled();
|
expect(listTagsMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue