diff --git a/shlink-web-component/ShlinkWebComponent.tsx b/shlink-web-component/ShlinkWebComponent.tsx index 9537b0fc..e166418f 100644 --- a/shlink-web-component/ShlinkWebComponent.tsx +++ b/shlink-web-component/ShlinkWebComponent.tsx @@ -5,7 +5,6 @@ import { useEffect, useRef, useState } from 'react'; import { Provider } from 'react-redux'; import type { SemVer } from '../src/utils/helpers/version'; import type { ShlinkApiClient } from './api-contract'; -import { setUpStore } from './container/store'; import { FeaturesProvider, useFeatures } from './utils/features'; import { RoutesPrefixProvider } from './utils/routesPrefix'; import type { Settings } from './utils/settings'; @@ -18,6 +17,11 @@ type ShlinkWebComponentProps = { apiClient: ShlinkApiClient; }; +// FIXME +// This allows to track the reference to be resolved by the container, but it's hacky and relies on not more than one +// ShlinkWebComponent rendered at the same time +let apiClientRef: ShlinkApiClient; + export const createShlinkWebComponent = ( bottle: Bottle, ): FC => ({ routesPrefix = '', serverVersion, settings, apiClient }) => { @@ -25,15 +29,21 @@ export const createShlinkWebComponent = ( const mainContent = useRef(); const [theStore, setStore] = useState(); + // Set client on every re-render + apiClientRef = apiClient; + useEffect(() => { - bottle.constant('apiClient', apiClient); + bottle.value('apiClientFactory', () => apiClientRef); // It's important to not try to resolve services before the API client has been registered, as many other services // depend on it const { container } = bottle; - const { Main } = container; + const { Main, store, loadMercureInfo } = container; mainContent.current =
; - setStore(setUpStore(container)); + setStore(store); + + // Load mercure info + store.dispatch(loadMercureInfo()); }, []); return !theStore ? <> : ( diff --git a/shlink-web-component/api-contract/ShlinkApiClient.ts b/shlink-web-component/api-contract/ShlinkApiClient.ts index 02b8acca..20c9ce5c 100644 --- a/shlink-web-component/api-contract/ShlinkApiClient.ts +++ b/shlink-web-component/api-contract/ShlinkApiClient.ts @@ -15,8 +15,8 @@ import type { } from './types'; export type ShlinkApiClient = { - baseUrl: string; - apiKey: string; + readonly baseUrl: string; + readonly apiKey: string; listShortUrls(params?: ShlinkShortUrlsListParams): Promise; diff --git a/shlink-web-component/container/provideServices.ts b/shlink-web-component/container/provideServices.ts index 59b81daa..4b3489ed 100644 --- a/shlink-web-component/container/provideServices.ts +++ b/shlink-web-component/container/provideServices.ts @@ -1,6 +1,7 @@ import type Bottle from 'bottlejs'; import { Main } from '../Main'; import type { ConnectDecorator } from './index'; +import { setUpStore } from './store'; export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory( @@ -18,6 +19,7 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { 'EditShortUrl', 'ManageDomains', ); - bottle.decorator('Main', connect(null, ['loadMercureInfo'])); + + bottle.factory('store', setUpStore); }; diff --git a/shlink-web-component/container/store.ts b/shlink-web-component/container/store.ts index 4af1309d..db777abe 100644 --- a/shlink-web-component/container/store.ts +++ b/shlink-web-component/container/store.ts @@ -1,5 +1,20 @@ import { combineReducers, configureStore } from '@reduxjs/toolkit'; import type { IContainer } from 'bottlejs'; +import type { DomainsList } from '../domains/reducers/domainsList'; +import type { MercureInfo } from '../mercure/reducers/mercureInfo'; +import type { ShortUrlCreation } from '../short-urls/reducers/shortUrlCreation'; +import type { ShortUrlDeletion } from '../short-urls/reducers/shortUrlDeletion'; +import type { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail'; +import type { ShortUrlEdition } from '../short-urls/reducers/shortUrlEdition'; +import type { ShortUrlsList } from '../short-urls/reducers/shortUrlsList'; +import type { TagDeletion } from '../tags/reducers/tagDelete'; +import type { TagEdition } from '../tags/reducers/tagEdit'; +import type { TagsList } from '../tags/reducers/tagsList'; +import type { DomainVisits } from '../visits/reducers/domainVisits'; +import type { ShortUrlVisits } from '../visits/reducers/shortUrlVisits'; +import type { TagVisits } from '../visits/reducers/tagVisits'; +import type { VisitsInfo } from '../visits/reducers/types'; +import type { VisitsOverview } from '../visits/reducers/visitsOverview'; const isProduction = process.env.NODE_ENV === 'production'; @@ -29,3 +44,22 @@ export const setUpStore = (container: IContainer) => configureStore({ serializableCheck: false, }), }); + +export type RootState = { + shortUrlsList: ShortUrlsList; + shortUrlCreation: ShortUrlCreation; + shortUrlDeletion: ShortUrlDeletion; + shortUrlEdition: ShortUrlEdition; + shortUrlVisits: ShortUrlVisits; + tagVisits: TagVisits; + domainVisits: DomainVisits; + orphanVisits: VisitsInfo; + nonOrphanVisits: VisitsInfo; + shortUrlDetail: ShortUrlDetail; + tagsList: TagsList; + tagDelete: TagDeletion; + tagEdit: TagEdition; + mercureInfo: MercureInfo; + domainsList: DomainsList; + visitsOverview: VisitsOverview; +}; diff --git a/shlink-web-component/domains/helpers/DomainDropdown.tsx b/shlink-web-component/domains/helpers/DomainDropdown.tsx index e8b24116..d36b2cbe 100644 --- a/shlink-web-component/domains/helpers/DomainDropdown.tsx +++ b/shlink-web-component/domains/helpers/DomainDropdown.tsx @@ -19,8 +19,6 @@ interface DomainDropdownProps { export const DomainDropdown: FC = ({ domain, editDomainRedirects }) => { const [isModalOpen, toggleModal] = useToggle(); - const { isDefault } = domain; - const canBeEdited = !isDefault; const withVisits = useFeature('domainVisits'); const routesPrefix = useRoutesPrefix(); @@ -34,7 +32,7 @@ export const DomainDropdown: FC = ({ domain, editDomainRedi Visit stats )} - + Edit redirects diff --git a/shlink-web-component/domains/reducers/domainRedirects.ts b/shlink-web-component/domains/reducers/domainRedirects.ts index dea1e8c6..e8ae3cf2 100644 --- a/shlink-web-component/domains/reducers/domainRedirects.ts +++ b/shlink-web-component/domains/reducers/domainRedirects.ts @@ -1,5 +1,5 @@ -import { createAsyncThunk } from '../../../src/utils/helpers/redux'; import type { ShlinkApiClient, ShlinkDomainRedirects } from '../../api-contract'; +import { createAsyncThunk } from '../../utils/redux'; const EDIT_DOMAIN_REDIRECTS = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS'; @@ -9,10 +9,11 @@ export interface EditDomainRedirects { } export const editDomainRedirects = ( - apiClient: ShlinkApiClient, + apiClientFactory: () => ShlinkApiClient, ) => createAsyncThunk( EDIT_DOMAIN_REDIRECTS, async ({ domain, redirects: providedRedirects }: EditDomainRedirects): Promise => { + const apiClient = apiClientFactory(); const redirects = await apiClient.editDomainRedirects({ domain, ...providedRedirects }); return { domain, redirects }; }, diff --git a/shlink-web-component/domains/reducers/domainsList.ts b/shlink-web-component/domains/reducers/domainsList.ts index 39030f22..4a6f3311 100644 --- a/shlink-web-component/domains/reducers/domainsList.ts +++ b/shlink-web-component/domains/reducers/domainsList.ts @@ -1,8 +1,8 @@ import type { AsyncThunk, SliceCaseReducers } from '@reduxjs/toolkit'; import { createAction, createSlice } from '@reduxjs/toolkit'; -import { createAsyncThunk } from '../../../src/utils/helpers/redux'; import type { ProblemDetailsError, ShlinkApiClient, ShlinkDomainRedirects } from '../../api-contract'; import { parseApiError } from '../../api-contract/utils'; +import { createAsyncThunk } from '../../utils/redux'; import type { Domain, DomainStatus } from '../data'; import type { EditDomainRedirects } from './domainRedirects'; @@ -41,11 +41,11 @@ export const replaceStatusOnDomain = (domain: string, status: DomainStatus) => (d: Domain): Domain => (d.domain !== domain ? d : { ...d, status }); export const domainsListReducerCreator = ( - apiClient: ShlinkApiClient, + apiClientFactory: () => ShlinkApiClient, editDomainRedirects: AsyncThunk, ) => { const listDomains = createAsyncThunk(`${REDUCER_PREFIX}/listDomains`, async (): Promise => { - const { data, defaultRedirects } = await apiClient.listDomains(); + const { data, defaultRedirects } = await apiClientFactory().listDomains(); return { domains: data.map((domain): Domain => ({ ...domain, status: 'validating' })), @@ -57,7 +57,7 @@ export const domainsListReducerCreator = ( `${REDUCER_PREFIX}/checkDomainHealth`, async (domain: string): Promise => { try { - const { status } = await apiClient.health(domain); + const { status } = await apiClientFactory().health(domain); return { domain, status: status === 'pass' ? 'valid' : 'invalid' }; } catch (e) { return { domain, status: 'invalid' }; diff --git a/shlink-web-component/domains/services/provideServices.ts b/shlink-web-component/domains/services/provideServices.ts index 1b2a944c..3731a81e 100644 --- a/shlink-web-component/domains/services/provideServices.ts +++ b/shlink-web-component/domains/services/provideServices.ts @@ -21,7 +21,7 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory( 'domainsListReducerCreator', domainsListReducerCreator, - 'apiClient', + 'apiClientFactory', 'editDomainRedirects', ); bottle.serviceFactory('domainsListReducer', prop('reducer'), 'domainsListReducerCreator'); @@ -29,6 +29,6 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { // Actions bottle.serviceFactory('listDomains', prop('listDomains'), 'domainsListReducerCreator'); bottle.serviceFactory('filterDomains', prop('filterDomains'), 'domainsListReducerCreator'); - bottle.serviceFactory('editDomainRedirects', editDomainRedirects, 'apiClient'); + bottle.serviceFactory('editDomainRedirects', editDomainRedirects, 'apiClientFactory'); bottle.serviceFactory('checkDomainHealth', prop('checkDomainHealth'), 'domainsListReducerCreator'); }; diff --git a/shlink-web-component/index.ts b/shlink-web-component/index.ts index 74fcebee..875ec5d2 100644 --- a/shlink-web-component/index.ts +++ b/shlink-web-component/index.ts @@ -2,3 +2,5 @@ import { bottle } from './container'; import { createShlinkWebComponent } from './ShlinkWebComponent'; export const ShlinkWebComponent = createShlinkWebComponent(bottle); + +export type { Settings } from './utils/settings'; diff --git a/shlink-web-component/mercure/reducers/mercureInfo.ts b/shlink-web-component/mercure/reducers/mercureInfo.ts index ff48bd11..16c2e311 100644 --- a/shlink-web-component/mercure/reducers/mercureInfo.ts +++ b/shlink-web-component/mercure/reducers/mercureInfo.ts @@ -1,6 +1,6 @@ import { createSlice } from '@reduxjs/toolkit'; -import { createAsyncThunk } from '../../../src/utils/helpers/redux'; import type { ShlinkApiClient, ShlinkMercureInfo } from '../../api-contract'; +import { createAsyncThunk } from '../../utils/redux'; const REDUCER_PREFIX = 'shlink/mercure'; @@ -15,7 +15,7 @@ const initialState: MercureInfo = { error: false, }; -export const mercureInfoReducerCreator = (apiClient: ShlinkApiClient) => { +export const mercureInfoReducerCreator = (apiClientFactory: () => ShlinkApiClient) => { const loadMercureInfo = createAsyncThunk( `${REDUCER_PREFIX}/loadMercureInfo`, (): Promise => @@ -25,7 +25,7 @@ export const mercureInfoReducerCreator = (apiClient: ShlinkApiClient) => { // throw new Error('Real time updates not enabled'); // } - apiClient.mercureInfo() + apiClientFactory().mercureInfo() , ); diff --git a/shlink-web-component/mercure/services/provideServices.ts b/shlink-web-component/mercure/services/provideServices.ts index 7dcc952d..2eaabf4a 100644 --- a/shlink-web-component/mercure/services/provideServices.ts +++ b/shlink-web-component/mercure/services/provideServices.ts @@ -4,7 +4,7 @@ import { mercureInfoReducerCreator } from '../reducers/mercureInfo'; export const provideServices = (bottle: Bottle) => { // Reducer - bottle.serviceFactory('mercureInfoReducerCreator', mercureInfoReducerCreator, 'apiClient'); + bottle.serviceFactory('mercureInfoReducerCreator', mercureInfoReducerCreator, 'apiClientFactory'); bottle.serviceFactory('mercureInfoReducer', prop('reducer'), 'mercureInfoReducerCreator'); // Actions diff --git a/shlink-web-component/short-urls/helpers/ExportShortUrlsBtn.tsx b/shlink-web-component/short-urls/helpers/ExportShortUrlsBtn.tsx index 96497676..e2d06332 100644 --- a/shlink-web-component/short-urls/helpers/ExportShortUrlsBtn.tsx +++ b/shlink-web-component/short-urls/helpers/ExportShortUrlsBtn.tsx @@ -14,7 +14,7 @@ export interface ExportShortUrlsBtnProps { const itemsPerPage = 20; export const ExportShortUrlsBtn = ( - apiClient: ShlinkApiClient, + apiClientFactory: () => ShlinkApiClient, { exportShortUrls }: ReportExporter, ): FC => ({ amount = 0 }) => { const [{ tags, search, startDate, endDate, orderBy, tagsMode }] = useShortUrlsQuery(); @@ -22,7 +22,7 @@ export const ExportShortUrlsBtn = ( const exportAllUrls = useCallback(async () => { const totalPages = amount / itemsPerPage; const loadAllUrls = async (page = 1): Promise => { - const { data } = await apiClient.listShortUrls( + const { data } = await apiClientFactory().listShortUrls( { page: `${page}`, tags, searchTerm: search, startDate, endDate, orderBy, tagsMode, itemsPerPage }, ); diff --git a/shlink-web-component/short-urls/reducers/shortUrlCreation.ts b/shlink-web-component/short-urls/reducers/shortUrlCreation.ts index 5987b457..2fc62b9c 100644 --- a/shlink-web-component/short-urls/reducers/shortUrlCreation.ts +++ b/shlink-web-component/short-urls/reducers/shortUrlCreation.ts @@ -1,8 +1,8 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; -import { createAsyncThunk } from '../../../src/utils/helpers/redux'; import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract'; import { parseApiError } from '../../api-contract/utils'; +import { createAsyncThunk } from '../../utils/redux'; import type { ShortUrl, ShortUrlData } from '../data'; const REDUCER_PREFIX = 'shlink/shortUrlCreation'; @@ -35,9 +35,9 @@ const initialState: ShortUrlCreation = { error: false, }; -export const createShortUrl = (apiClient: ShlinkApiClient) => createAsyncThunk( +export const createShortUrl = (apiClientFactory: () => ShlinkApiClient) => createAsyncThunk( `${REDUCER_PREFIX}/createShortUrl`, - (data: ShortUrlData): Promise => apiClient.createShortUrl(data), + (data: ShortUrlData): Promise => apiClientFactory().createShortUrl(data), ); export const shortUrlCreationReducerCreator = (createShortUrlThunk: ReturnType) => { diff --git a/shlink-web-component/short-urls/reducers/shortUrlDeletion.ts b/shlink-web-component/short-urls/reducers/shortUrlDeletion.ts index d4999a3b..2b9f82b3 100644 --- a/shlink-web-component/short-urls/reducers/shortUrlDeletion.ts +++ b/shlink-web-component/short-urls/reducers/shortUrlDeletion.ts @@ -1,7 +1,7 @@ import { createAction, createSlice } from '@reduxjs/toolkit'; -import { createAsyncThunk } from '../../../src/utils/helpers/redux'; import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract'; import { parseApiError } from '../../api-contract/utils'; +import { createAsyncThunk } from '../../utils/redux'; import type { ShortUrl, ShortUrlIdentifier } from '../data'; const REDUCER_PREFIX = 'shlink/shortUrlDeletion'; @@ -21,10 +21,10 @@ const initialState: ShortUrlDeletion = { error: false, }; -export const deleteShortUrl = (apiClient: ShlinkApiClient) => createAsyncThunk( +export const deleteShortUrl = (apiClientFactory: () => ShlinkApiClient) => createAsyncThunk( `${REDUCER_PREFIX}/deleteShortUrl`, async ({ shortCode, domain }: ShortUrlIdentifier): Promise => { - await apiClient.deleteShortUrl(shortCode, domain); + await apiClientFactory().deleteShortUrl(shortCode, domain); return { shortCode, domain }; }, ); diff --git a/shlink-web-component/short-urls/reducers/shortUrlDetail.ts b/shlink-web-component/short-urls/reducers/shortUrlDetail.ts index 86b80e2a..9791974f 100644 --- a/shlink-web-component/short-urls/reducers/shortUrlDetail.ts +++ b/shlink-web-component/short-urls/reducers/shortUrlDetail.ts @@ -1,8 +1,8 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; -import { createAsyncThunk } from '../../../src/utils/helpers/redux'; import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract'; import { parseApiError } from '../../api-contract/utils'; +import { createAsyncThunk } from '../../utils/redux'; import type { ShortUrl, ShortUrlIdentifier } from '../data'; import { shortUrlMatches } from '../helpers'; @@ -22,14 +22,14 @@ const initialState: ShortUrlDetail = { error: false, }; -export const shortUrlDetailReducerCreator = (apiClient: ShlinkApiClient) => { +export const shortUrlDetailReducerCreator = (apiClientFactory: () => ShlinkApiClient) => { const getShortUrlDetail = createAsyncThunk( `${REDUCER_PREFIX}/getShortUrlDetail`, async ({ shortCode, domain }: ShortUrlIdentifier, { getState }): Promise => { const { shortUrlsList } = getState(); const alreadyLoaded = shortUrlsList?.shortUrls?.data.find((url) => shortUrlMatches(url, shortCode, domain)); - return alreadyLoaded ?? await apiClient.getShortUrl(shortCode, domain); + return alreadyLoaded ?? await apiClientFactory().getShortUrl(shortCode, domain); }, ); diff --git a/shlink-web-component/short-urls/reducers/shortUrlEdition.ts b/shlink-web-component/short-urls/reducers/shortUrlEdition.ts index f75b92b0..93a0987e 100644 --- a/shlink-web-component/short-urls/reducers/shortUrlEdition.ts +++ b/shlink-web-component/short-urls/reducers/shortUrlEdition.ts @@ -1,8 +1,8 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; -import { createAsyncThunk } from '../../../src/utils/helpers/redux'; import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract'; import { parseApiError } from '../../api-contract/utils'; +import { createAsyncThunk } from '../../utils/redux'; import type { EditShortUrlData, ShortUrl, ShortUrlIdentifier } from '../data'; const REDUCER_PREFIX = 'shlink/shortUrlEdition'; @@ -27,10 +27,10 @@ const initialState: ShortUrlEdition = { error: false, }; -export const editShortUrl = (apiClient: ShlinkApiClient) => createAsyncThunk( +export const editShortUrl = (apiClientFactory: () => ShlinkApiClient) => createAsyncThunk( `${REDUCER_PREFIX}/editShortUrl`, ({ shortCode, domain, data }: EditShortUrl): Promise => - apiClient.updateShortUrl(shortCode, domain, data as any) // TODO parse dates + apiClientFactory().updateShortUrl(shortCode, domain, data as any) // TODO parse dates , ); diff --git a/shlink-web-component/short-urls/reducers/shortUrlsList.ts b/shlink-web-component/short-urls/reducers/shortUrlsList.ts index 05506d37..3bbfb5c8 100644 --- a/shlink-web-component/short-urls/reducers/shortUrlsList.ts +++ b/shlink-web-component/short-urls/reducers/shortUrlsList.ts @@ -1,7 +1,7 @@ import { createSlice } from '@reduxjs/toolkit'; import { assocPath, last, pipe, reject } from 'ramda'; -import { createAsyncThunk } from '../../../src/utils/helpers/redux'; 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'; @@ -23,9 +23,11 @@ const initialState: ShortUrlsList = { error: false, }; -export const listShortUrls = (apiClient: ShlinkApiClient) => createAsyncThunk( +export const listShortUrls = (apiClientFactory: () => ShlinkApiClient) => createAsyncThunk( `${REDUCER_PREFIX}/listShortUrls`, - (params: ShlinkShortUrlsListParams | void): Promise => apiClient.listShortUrls(params ?? {}), + (params: ShlinkShortUrlsListParams | void): Promise => apiClientFactory().listShortUrls( + params ?? {}, + ), ); export const shortUrlsListReducerCreator = ( diff --git a/shlink-web-component/short-urls/services/provideServices.ts b/shlink-web-component/short-urls/services/provideServices.ts index 3c79a376..9f90c675 100644 --- a/shlink-web-component/short-urls/services/provideServices.ts +++ b/shlink-web-component/short-urls/services/provideServices.ts @@ -54,7 +54,7 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('QrCodeModal', QrCodeModal, 'ImageDownloader'); bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ExportShortUrlsBtn', 'TagsSelector'); - bottle.serviceFactory('ExportShortUrlsBtn', ExportShortUrlsBtn, 'apiClient', 'ReportExporter'); + bottle.serviceFactory('ExportShortUrlsBtn', ExportShortUrlsBtn, 'apiClientFactory', 'ReportExporter'); // Reducers bottle.serviceFactory( @@ -75,20 +75,20 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('shortUrlDeletionReducerCreator', shortUrlDeletionReducerCreator, 'deleteShortUrl'); bottle.serviceFactory('shortUrlDeletionReducer', prop('reducer'), 'shortUrlDeletionReducerCreator'); - bottle.serviceFactory('shortUrlDetailReducerCreator', shortUrlDetailReducerCreator, 'apiClient'); + bottle.serviceFactory('shortUrlDetailReducerCreator', shortUrlDetailReducerCreator, 'apiClientFactory'); bottle.serviceFactory('shortUrlDetailReducer', prop('reducer'), 'shortUrlDetailReducerCreator'); // Actions - bottle.serviceFactory('listShortUrls', listShortUrls, 'apiClient'); + bottle.serviceFactory('listShortUrls', listShortUrls, 'apiClientFactory'); - bottle.serviceFactory('createShortUrl', createShortUrl, 'apiClient'); + bottle.serviceFactory('createShortUrl', createShortUrl, 'apiClientFactory'); bottle.serviceFactory('resetCreateShortUrl', prop('resetCreateShortUrl'), 'shortUrlCreationReducerCreator'); - bottle.serviceFactory('deleteShortUrl', deleteShortUrl, 'apiClient'); + bottle.serviceFactory('deleteShortUrl', deleteShortUrl, 'apiClientFactory'); bottle.serviceFactory('resetDeleteShortUrl', prop('resetDeleteShortUrl'), 'shortUrlDeletionReducerCreator'); bottle.serviceFactory('shortUrlDeleted', () => shortUrlDeleted); bottle.serviceFactory('getShortUrlDetail', prop('getShortUrlDetail'), 'shortUrlDetailReducerCreator'); - bottle.serviceFactory('editShortUrl', editShortUrl, 'apiClient'); + bottle.serviceFactory('editShortUrl', editShortUrl, 'apiClientFactory'); }; diff --git a/shlink-web-component/tags/reducers/tagDelete.ts b/shlink-web-component/tags/reducers/tagDelete.ts index a2d81440..861f177e 100644 --- a/shlink-web-component/tags/reducers/tagDelete.ts +++ b/shlink-web-component/tags/reducers/tagDelete.ts @@ -1,7 +1,7 @@ import { createAction, createSlice } from '@reduxjs/toolkit'; -import { createAsyncThunk } from '../../../src/utils/helpers/redux'; import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract'; import { parseApiError } from '../../api-contract/utils'; +import { createAsyncThunk } from '../../utils/redux'; const REDUCER_PREFIX = 'shlink/tagDelete'; @@ -20,9 +20,9 @@ const initialState: TagDeletion = { export const tagDeleted = createAction(`${REDUCER_PREFIX}/tagDeleted`); -export const tagDeleteReducerCreator = (apiClient: ShlinkApiClient) => { +export const tagDeleteReducerCreator = (apiClientFactory: () => ShlinkApiClient) => { const deleteTag = createAsyncThunk(`${REDUCER_PREFIX}/deleteTag`, async (tag: string): Promise => { - await apiClient.deleteTags([tag]); + await apiClientFactory().deleteTags([tag]); }); const { reducer } = createSlice({ diff --git a/shlink-web-component/tags/reducers/tagEdit.ts b/shlink-web-component/tags/reducers/tagEdit.ts index 7b6235a1..3a6f69c2 100644 --- a/shlink-web-component/tags/reducers/tagEdit.ts +++ b/shlink-web-component/tags/reducers/tagEdit.ts @@ -1,10 +1,10 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createAction, createSlice } from '@reduxjs/toolkit'; import { pick } from 'ramda'; -import { createAsyncThunk } from '../../../src/utils/helpers/redux'; import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator'; import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract'; import { parseApiError } from '../../api-contract/utils'; +import { createAsyncThunk } from '../../utils/redux'; const REDUCER_PREFIX = 'shlink/tagEdit'; @@ -34,12 +34,12 @@ const initialState: TagEdition = { export const tagEdited = createAction(`${REDUCER_PREFIX}/tagEdited`); export const editTag = ( - apiClient: ShlinkApiClient, + apiClientFactory: () => ShlinkApiClient, colorGenerator: ColorGenerator, ) => createAsyncThunk( `${REDUCER_PREFIX}/editTag`, async ({ oldName, newName, color }: EditTag): Promise => { - await apiClient.editTag(oldName, newName); + await apiClientFactory().editTag(oldName, newName); colorGenerator.setColorForKey(newName, color); return { oldName, newName, color }; diff --git a/shlink-web-component/tags/reducers/tagsList.ts b/shlink-web-component/tags/reducers/tagsList.ts index 50577da3..a7c46cbf 100644 --- a/shlink-web-component/tags/reducers/tagsList.ts +++ b/shlink-web-component/tags/reducers/tagsList.ts @@ -1,9 +1,9 @@ import { createAction, createSlice } from '@reduxjs/toolkit'; import { isEmpty, reject } from 'ramda'; -import { createAsyncThunk } from '../../../src/utils/helpers/redux'; import type { ProblemDetailsError, ShlinkApiClient, ShlinkTags } from '../../api-contract'; import { parseApiError } from '../../api-contract/utils'; import type { createShortUrl } from '../../short-urls/reducers/shortUrlCreation'; +import { createAsyncThunk } from '../../utils/redux'; import { createNewVisits } from '../../visits/reducers/visitCreation'; import type { CreateVisit } from '../../visits/types'; import type { TagStats } from '../data'; @@ -80,7 +80,7 @@ const calculateVisitsPerTag = (createdVisits: CreateVisit[]): TagIncrease[] => O }, {}), ); -export const listTags = (apiClient: ShlinkApiClient, force = true) => createAsyncThunk( +export const listTags = (apiClientFactory: () => ShlinkApiClient, force = true) => createAsyncThunk( `${REDUCER_PREFIX}/listTags`, async (_: void, { getState }): Promise => { const { tagsList } = getState(); @@ -89,7 +89,7 @@ export const listTags = (apiClient: ShlinkApiClient, force = true) => createAsyn return tagsList; } - const { tags, stats }: ShlinkTags = await apiClient.tagsStats(); + const { tags, stats }: ShlinkTags = await apiClientFactory().tagsStats(); const processedStats = stats.reduce((acc, { tag, ...rest }) => { acc[tag] = rest; return acc; diff --git a/shlink-web-component/tags/services/provideServices.ts b/shlink-web-component/tags/services/provideServices.ts index e6b16322..01279e88 100644 --- a/shlink-web-component/tags/services/provideServices.ts +++ b/shlink-web-component/tags/services/provideServices.ts @@ -36,14 +36,17 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('tagEditReducerCreator', tagEditReducerCreator, 'editTag'); bottle.serviceFactory('tagEditReducer', prop('reducer'), 'tagEditReducerCreator'); - bottle.serviceFactory('tagDeleteReducerCreator', tagDeleteReducerCreator, 'apiClient'); + bottle.serviceFactory('tagDeleteReducerCreator', tagDeleteReducerCreator, 'apiClientFactory'); bottle.serviceFactory('tagDeleteReducer', prop('reducer'), 'tagDeleteReducerCreator'); bottle.serviceFactory('tagsListReducerCreator', tagsListReducerCreator, 'listTags', 'createShortUrl'); bottle.serviceFactory('tagsListReducer', prop('reducer'), 'tagsListReducerCreator'); // Actions - const listTagsActionFactory = (force: boolean) => ({ apiClient }: IContainer) => listTags(apiClient, force); + const listTagsActionFactory = (force: boolean) => ({ apiClientFactory }: IContainer) => listTags( + apiClientFactory, + force, + ); bottle.factory('listTags', listTagsActionFactory(false)); bottle.factory('forceListTags', listTagsActionFactory(true)); @@ -52,6 +55,6 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('deleteTag', prop('deleteTag'), 'tagDeleteReducerCreator'); bottle.serviceFactory('tagDeleted', () => tagDeleted); - bottle.serviceFactory('editTag', editTag, 'apiClient', 'ColorGenerator'); + bottle.serviceFactory('editTag', editTag, 'apiClientFactory', 'ColorGenerator'); bottle.serviceFactory('tagEdited', () => tagEdited); }; diff --git a/shlink-web-component/utils/redux.ts b/shlink-web-component/utils/redux.ts new file mode 100644 index 00000000..2c7f252a --- /dev/null +++ b/shlink-web-component/utils/redux.ts @@ -0,0 +1,13 @@ +import type { AsyncThunkPayloadCreator } from '@reduxjs/toolkit'; +import { createAsyncThunk as baseCreateAsyncThunk } from '@reduxjs/toolkit'; +import { identity } from 'ramda'; +import type { RootState } from '../container/store'; + +export const createAsyncThunk = ( + typePrefix: string, + payloadCreator: AsyncThunkPayloadCreator, +) => baseCreateAsyncThunk( + typePrefix, + payloadCreator, + { serializeError: identity }, + ); diff --git a/shlink-web-component/visits/reducers/common.ts b/shlink-web-component/visits/reducers/common.ts index 15112f9c..93c214e9 100644 --- a/shlink-web-component/visits/reducers/common.ts +++ b/shlink-web-component/visits/reducers/common.ts @@ -1,11 +1,11 @@ import { createAction, createSlice } from '@reduxjs/toolkit'; import { flatten, prop, range, splitEvery } from 'ramda'; -import type { ShlinkState } from '../../../src/container/types'; import type { DateInterval } from '../../../src/utils/helpers/dateIntervals'; import { dateToMatchingInterval } from '../../../src/utils/helpers/dateIntervals'; -import { createAsyncThunk } from '../../../src/utils/helpers/redux'; import type { ShlinkPaginator, ShlinkVisits, ShlinkVisitsParams } from '../../api-contract'; import { parseApiError } from '../../api-contract/utils'; +import type { RootState } from '../../container/store'; +import { createAsyncThunk } from '../../utils/redux'; import type { CreateVisit, Visit } from '../types'; import type { LoadVisits, VisitsInfo, VisitsLoaded } from './types'; import { createNewVisits } from './visitCreation'; @@ -24,7 +24,7 @@ interface VisitsAsyncThunkOptions [VisitsLoader, LastVisitLoader]; getExtraFulfilledPayload: (params: T) => Partial; - shouldCancel: (getState: () => ShlinkState) => boolean; + shouldCancel: (getState: () => RootState) => boolean; } export const createVisitsAsyncThunk = ( diff --git a/shlink-web-component/visits/reducers/domainVisits.ts b/shlink-web-component/visits/reducers/domainVisits.ts index c5d503b9..db767039 100644 --- a/shlink-web-component/visits/reducers/domainVisits.ts +++ b/shlink-web-component/visits/reducers/domainVisits.ts @@ -26,10 +26,10 @@ const initialState: DomainVisits = { progress: 0, }; -export const getDomainVisits = (apiClient: ShlinkApiClient) => createVisitsAsyncThunk({ +export const getDomainVisits = (apiClientFactory: () => ShlinkApiClient) => createVisitsAsyncThunk({ typePrefix: `${REDUCER_PREFIX}/getDomainVisits`, createLoaders: ({ domain, query = {}, doIntervalFallback = false }: LoadDomainVisits) => { - const { getDomainVisits: getVisits } = apiClient; + const { getDomainVisits: getVisits } = apiClientFactory(); const visitsLoader = async (page: number, itemsPerPage: number) => getVisits( domain, { ...query, page, itemsPerPage }, diff --git a/shlink-web-component/visits/reducers/nonOrphanVisits.ts b/shlink-web-component/visits/reducers/nonOrphanVisits.ts index 3bed1262..1b257661 100644 --- a/shlink-web-component/visits/reducers/nonOrphanVisits.ts +++ b/shlink-web-component/visits/reducers/nonOrphanVisits.ts @@ -14,10 +14,10 @@ const initialState: VisitsInfo = { progress: 0, }; -export const getNonOrphanVisits = (apiClient: ShlinkApiClient) => createVisitsAsyncThunk({ +export const getNonOrphanVisits = (apiClientFactory: () => ShlinkApiClient) => createVisitsAsyncThunk({ typePrefix: `${REDUCER_PREFIX}/getNonOrphanVisits`, createLoaders: ({ query = {}, doIntervalFallback = false }) => { - const { getNonOrphanVisits: shlinkGetNonOrphanVisits } = apiClient; + const { getNonOrphanVisits: shlinkGetNonOrphanVisits } = apiClientFactory(); const visitsLoader = async (page: number, itemsPerPage: number) => shlinkGetNonOrphanVisits({ ...query, page, itemsPerPage }); const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, shlinkGetNonOrphanVisits); diff --git a/shlink-web-component/visits/reducers/orphanVisits.ts b/shlink-web-component/visits/reducers/orphanVisits.ts index 219dea20..ce1664b2 100644 --- a/shlink-web-component/visits/reducers/orphanVisits.ts +++ b/shlink-web-component/visits/reducers/orphanVisits.ts @@ -23,10 +23,10 @@ const initialState: VisitsInfo = { const matchesType = (visit: OrphanVisit, orphanVisitsType?: OrphanVisitType) => !orphanVisitsType || orphanVisitsType === visit.type; -export const getOrphanVisits = (apiClient: ShlinkApiClient) => createVisitsAsyncThunk({ +export const getOrphanVisits = (apiClientFactory: () => ShlinkApiClient) => createVisitsAsyncThunk({ typePrefix: `${REDUCER_PREFIX}/getOrphanVisits`, createLoaders: ({ orphanVisitsType, query = {}, doIntervalFallback = false }: LoadOrphanVisits) => { - const { getOrphanVisits: getVisits } = apiClient; + const { getOrphanVisits: getVisits } = apiClientFactory(); const visitsLoader = async (page: number, itemsPerPage: number) => getVisits({ ...query, page, itemsPerPage }) .then((result) => { const visits = result.data.filter((visit) => isOrphanVisit(visit) && matchesType(visit, orphanVisitsType)); diff --git a/shlink-web-component/visits/reducers/shortUrlVisits.ts b/shlink-web-component/visits/reducers/shortUrlVisits.ts index ca6ef50b..57f1a0b5 100644 --- a/shlink-web-component/visits/reducers/shortUrlVisits.ts +++ b/shlink-web-component/visits/reducers/shortUrlVisits.ts @@ -24,10 +24,10 @@ const initialState: ShortUrlVisits = { progress: 0, }; -export const getShortUrlVisits = (apiClient: ShlinkApiClient) => createVisitsAsyncThunk({ +export const getShortUrlVisits = (apiClientFactory: () => ShlinkApiClient) => createVisitsAsyncThunk({ typePrefix: `${REDUCER_PREFIX}/getShortUrlVisits`, createLoaders: ({ shortCode, query = {}, doIntervalFallback = false }: LoadShortUrlVisits) => { - const { getShortUrlVisits: shlinkGetShortUrlVisits } = apiClient; + const { getShortUrlVisits: shlinkGetShortUrlVisits } = apiClientFactory(); const visitsLoader = async (page: number, itemsPerPage: number) => shlinkGetShortUrlVisits( shortCode, { ...query, page, itemsPerPage }, diff --git a/shlink-web-component/visits/reducers/tagVisits.ts b/shlink-web-component/visits/reducers/tagVisits.ts index 3bb4e994..5507956c 100644 --- a/shlink-web-component/visits/reducers/tagVisits.ts +++ b/shlink-web-component/visits/reducers/tagVisits.ts @@ -23,10 +23,10 @@ const initialState: TagVisits = { progress: 0, }; -export const getTagVisits = (apiClient: ShlinkApiClient) => createVisitsAsyncThunk({ +export const getTagVisits = (apiClientFactory: () => ShlinkApiClient) => createVisitsAsyncThunk({ typePrefix: `${REDUCER_PREFIX}/getTagVisits`, createLoaders: ({ tag, query = {}, doIntervalFallback = false }: LoadTagVisits) => { - const { getTagVisits: getVisits } = apiClient; + const { getTagVisits: getVisits } = apiClientFactory(); const visitsLoader = async (page: number, itemsPerPage: number) => getVisits( tag, { ...query, page, itemsPerPage }, diff --git a/shlink-web-component/visits/reducers/visitsOverview.ts b/shlink-web-component/visits/reducers/visitsOverview.ts index d15dcc22..259a4492 100644 --- a/shlink-web-component/visits/reducers/visitsOverview.ts +++ b/shlink-web-component/visits/reducers/visitsOverview.ts @@ -1,7 +1,7 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; -import { createAsyncThunk } from '../../../src/utils/helpers/redux'; import type { ShlinkApiClient, ShlinkVisitsOverview } from '../../api-contract'; +import { createAsyncThunk } from '../../utils/redux'; import type { CreateVisit } from '../types'; import { groupNewVisitsByType } from '../types/helpers'; import { createNewVisits } from './visitCreation'; @@ -39,9 +39,9 @@ const initialState: VisitsOverview = { const countBots = (visits: CreateVisit[]) => visits.filter(({ visit }) => visit.potentialBot).length; -export const loadVisitsOverview = (apiClient: ShlinkApiClient) => createAsyncThunk( +export const loadVisitsOverview = (apiClientFactory: () => ShlinkApiClient) => createAsyncThunk( `${REDUCER_PREFIX}/loadVisitsOverview`, - (): Promise => apiClient.getVisitsOverview().then( + (): Promise => apiClientFactory().getVisitsOverview().then( ({ nonOrphanVisits, visitsCount, orphanVisits, orphanVisitsCount }) => ({ nonOrphanVisits: { total: nonOrphanVisits?.total ?? visitsCount, diff --git a/shlink-web-component/visits/services/provideServices.ts b/shlink-web-component/visits/services/provideServices.ts index c1626596..59cf5964 100644 --- a/shlink-web-component/visits/services/provideServices.ts +++ b/shlink-web-component/visits/services/provideServices.ts @@ -54,23 +54,23 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('VisitsParser', () => visitsParser); // Actions - bottle.serviceFactory('getShortUrlVisits', getShortUrlVisits, 'apiClient'); + bottle.serviceFactory('getShortUrlVisits', getShortUrlVisits, 'apiClientFactory'); bottle.serviceFactory('cancelGetShortUrlVisits', prop('cancelGetVisits'), 'shortUrlVisitsReducerCreator'); - bottle.serviceFactory('getTagVisits', getTagVisits, 'apiClient'); + bottle.serviceFactory('getTagVisits', getTagVisits, 'apiClientFactory'); bottle.serviceFactory('cancelGetTagVisits', prop('cancelGetVisits'), 'tagVisitsReducerCreator'); - bottle.serviceFactory('getDomainVisits', getDomainVisits, 'apiClient'); + bottle.serviceFactory('getDomainVisits', getDomainVisits, 'apiClientFactory'); bottle.serviceFactory('cancelGetDomainVisits', prop('cancelGetVisits'), 'domainVisitsReducerCreator'); - bottle.serviceFactory('getOrphanVisits', getOrphanVisits, 'apiClient'); + bottle.serviceFactory('getOrphanVisits', getOrphanVisits, 'apiClientFactory'); bottle.serviceFactory('cancelGetOrphanVisits', prop('cancelGetVisits'), 'orphanVisitsReducerCreator'); - bottle.serviceFactory('getNonOrphanVisits', getNonOrphanVisits, 'apiClient'); + bottle.serviceFactory('getNonOrphanVisits', getNonOrphanVisits, 'apiClientFactory'); bottle.serviceFactory('cancelGetNonOrphanVisits', prop('cancelGetVisits'), 'nonOrphanVisitsReducerCreator'); bottle.serviceFactory('createNewVisits', () => createNewVisits); - bottle.serviceFactory('loadVisitsOverview', loadVisitsOverview, 'apiClient'); + bottle.serviceFactory('loadVisitsOverview', loadVisitsOverview, 'apiClientFactory'); // Reducers bottle.serviceFactory('visitsOverviewReducerCreator', visitsOverviewReducerCreator, 'loadVisitsOverview'); diff --git a/src/container/types.ts b/src/container/types.ts index 98b28151..995c258a 100644 --- a/src/container/types.ts +++ b/src/container/types.ts @@ -1,6 +1,6 @@ +import type { Settings } from '../../shlink-web-component'; import type { Sidebar } from '../common/reducers/sidebar'; import type { SelectedServer, ServersMap } from '../servers/data'; -import type { Settings } from '../settings/reducers/settings'; export interface ShlinkState { servers: ServersMap; diff --git a/vite.config.ts b/vite.config.ts index 6057fac6..0dd5f21a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -35,6 +35,7 @@ export default defineConfig({ reporter: ['text', 'text-summary', 'html', 'clover'], include: [ 'src/**/*.{ts,tsx}', + 'shlink-web-component/**/*.{ts,tsx}', '!src/*.{ts,tsx}', '!src/reducers/index.ts', '!src/**/provideServices.ts',