mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-03 14:57:22 +03:00
Use apiClient factory to dynamically resolved different values at runtime
This commit is contained in:
parent
3a0cea1268
commit
d49da185d3
33 changed files with 146 additions and 80 deletions
|
@ -5,7 +5,6 @@ import { useEffect, useRef, useState } from 'react';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import type { SemVer } from '../src/utils/helpers/version';
|
import type { SemVer } from '../src/utils/helpers/version';
|
||||||
import type { ShlinkApiClient } from './api-contract';
|
import type { ShlinkApiClient } from './api-contract';
|
||||||
import { setUpStore } from './container/store';
|
|
||||||
import { FeaturesProvider, useFeatures } from './utils/features';
|
import { FeaturesProvider, useFeatures } from './utils/features';
|
||||||
import { RoutesPrefixProvider } from './utils/routesPrefix';
|
import { RoutesPrefixProvider } from './utils/routesPrefix';
|
||||||
import type { Settings } from './utils/settings';
|
import type { Settings } from './utils/settings';
|
||||||
|
@ -18,6 +17,11 @@ type ShlinkWebComponentProps = {
|
||||||
apiClient: ShlinkApiClient;
|
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 = (
|
export const createShlinkWebComponent = (
|
||||||
bottle: Bottle,
|
bottle: Bottle,
|
||||||
): FC<ShlinkWebComponentProps> => ({ routesPrefix = '', serverVersion, settings, apiClient }) => {
|
): FC<ShlinkWebComponentProps> => ({ routesPrefix = '', serverVersion, settings, apiClient }) => {
|
||||||
|
@ -25,15 +29,21 @@ export const createShlinkWebComponent = (
|
||||||
const mainContent = useRef<ReactNode>();
|
const mainContent = useRef<ReactNode>();
|
||||||
const [theStore, setStore] = useState<Store | undefined>();
|
const [theStore, setStore] = useState<Store | undefined>();
|
||||||
|
|
||||||
|
// Set client on every re-render
|
||||||
|
apiClientRef = apiClient;
|
||||||
|
|
||||||
useEffect(() => {
|
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
|
// It's important to not try to resolve services before the API client has been registered, as many other services
|
||||||
// depend on it
|
// depend on it
|
||||||
const { container } = bottle;
|
const { container } = bottle;
|
||||||
const { Main } = container;
|
const { Main, store, loadMercureInfo } = container;
|
||||||
mainContent.current = <Main />;
|
mainContent.current = <Main />;
|
||||||
setStore(setUpStore(container));
|
setStore(store);
|
||||||
|
|
||||||
|
// Load mercure info
|
||||||
|
store.dispatch(loadMercureInfo());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return !theStore ? <></> : (
|
return !theStore ? <></> : (
|
||||||
|
|
|
@ -15,8 +15,8 @@ import type {
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export type ShlinkApiClient = {
|
export type ShlinkApiClient = {
|
||||||
baseUrl: string;
|
readonly baseUrl: string;
|
||||||
apiKey: string;
|
readonly apiKey: string;
|
||||||
|
|
||||||
listShortUrls(params?: ShlinkShortUrlsListParams): Promise<ShlinkShortUrlsResponse>;
|
listShortUrls(params?: ShlinkShortUrlsListParams): Promise<ShlinkShortUrlsResponse>;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type Bottle from 'bottlejs';
|
import type Bottle from 'bottlejs';
|
||||||
import { Main } from '../Main';
|
import { Main } from '../Main';
|
||||||
import type { ConnectDecorator } from './index';
|
import type { ConnectDecorator } from './index';
|
||||||
|
import { setUpStore } from './store';
|
||||||
|
|
||||||
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
bottle.serviceFactory(
|
bottle.serviceFactory(
|
||||||
|
@ -18,6 +19,7 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
'EditShortUrl',
|
'EditShortUrl',
|
||||||
'ManageDomains',
|
'ManageDomains',
|
||||||
);
|
);
|
||||||
|
|
||||||
bottle.decorator('Main', connect(null, ['loadMercureInfo']));
|
bottle.decorator('Main', connect(null, ['loadMercureInfo']));
|
||||||
|
|
||||||
|
bottle.factory('store', setUpStore);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,20 @@
|
||||||
import { combineReducers, configureStore } from '@reduxjs/toolkit';
|
import { combineReducers, configureStore } from '@reduxjs/toolkit';
|
||||||
import type { IContainer } from 'bottlejs';
|
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';
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
|
@ -29,3 +44,22 @@ export const setUpStore = (container: IContainer) => configureStore({
|
||||||
serializableCheck: false,
|
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;
|
||||||
|
};
|
||||||
|
|
|
@ -19,8 +19,6 @@ interface DomainDropdownProps {
|
||||||
|
|
||||||
export const DomainDropdown: FC<DomainDropdownProps> = ({ domain, editDomainRedirects }) => {
|
export const DomainDropdown: FC<DomainDropdownProps> = ({ domain, editDomainRedirects }) => {
|
||||||
const [isModalOpen, toggleModal] = useToggle();
|
const [isModalOpen, toggleModal] = useToggle();
|
||||||
const { isDefault } = domain;
|
|
||||||
const canBeEdited = !isDefault;
|
|
||||||
const withVisits = useFeature('domainVisits');
|
const withVisits = useFeature('domainVisits');
|
||||||
const routesPrefix = useRoutesPrefix();
|
const routesPrefix = useRoutesPrefix();
|
||||||
|
|
||||||
|
@ -34,7 +32,7 @@ export const DomainDropdown: FC<DomainDropdownProps> = ({ domain, editDomainRedi
|
||||||
<FontAwesomeIcon icon={pieChartIcon} fixedWidth /> Visit stats
|
<FontAwesomeIcon icon={pieChartIcon} fixedWidth /> Visit stats
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
)}
|
)}
|
||||||
<DropdownItem disabled={!canBeEdited} onClick={!canBeEdited ? undefined : toggleModal}>
|
<DropdownItem onClick={toggleModal}>
|
||||||
<FontAwesomeIcon fixedWidth icon={editIcon} /> Edit redirects
|
<FontAwesomeIcon fixedWidth icon={editIcon} /> Edit redirects
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { createAsyncThunk } from '../../../src/utils/helpers/redux';
|
|
||||||
import type { ShlinkApiClient, ShlinkDomainRedirects } from '../../api-contract';
|
import type { ShlinkApiClient, ShlinkDomainRedirects } from '../../api-contract';
|
||||||
|
import { createAsyncThunk } from '../../utils/redux';
|
||||||
|
|
||||||
const EDIT_DOMAIN_REDIRECTS = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS';
|
const EDIT_DOMAIN_REDIRECTS = 'shlink/domainRedirects/EDIT_DOMAIN_REDIRECTS';
|
||||||
|
|
||||||
|
@ -9,10 +9,11 @@ export interface EditDomainRedirects {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const editDomainRedirects = (
|
export const editDomainRedirects = (
|
||||||
apiClient: ShlinkApiClient,
|
apiClientFactory: () => ShlinkApiClient,
|
||||||
) => createAsyncThunk(
|
) => createAsyncThunk(
|
||||||
EDIT_DOMAIN_REDIRECTS,
|
EDIT_DOMAIN_REDIRECTS,
|
||||||
async ({ domain, redirects: providedRedirects }: EditDomainRedirects): Promise<EditDomainRedirects> => {
|
async ({ domain, redirects: providedRedirects }: EditDomainRedirects): Promise<EditDomainRedirects> => {
|
||||||
|
const apiClient = apiClientFactory();
|
||||||
const redirects = await apiClient.editDomainRedirects({ domain, ...providedRedirects });
|
const redirects = await apiClient.editDomainRedirects({ domain, ...providedRedirects });
|
||||||
return { domain, redirects };
|
return { domain, redirects };
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import type { AsyncThunk, SliceCaseReducers } from '@reduxjs/toolkit';
|
import type { AsyncThunk, SliceCaseReducers } from '@reduxjs/toolkit';
|
||||||
import { createAction, createSlice } 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 type { ProblemDetailsError, ShlinkApiClient, ShlinkDomainRedirects } from '../../api-contract';
|
||||||
import { parseApiError } from '../../api-contract/utils';
|
import { parseApiError } from '../../api-contract/utils';
|
||||||
|
import { createAsyncThunk } from '../../utils/redux';
|
||||||
import type { Domain, DomainStatus } from '../data';
|
import type { Domain, DomainStatus } from '../data';
|
||||||
import type { EditDomainRedirects } from './domainRedirects';
|
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 });
|
(d: Domain): Domain => (d.domain !== domain ? d : { ...d, status });
|
||||||
|
|
||||||
export const domainsListReducerCreator = (
|
export const domainsListReducerCreator = (
|
||||||
apiClient: ShlinkApiClient,
|
apiClientFactory: () => ShlinkApiClient,
|
||||||
editDomainRedirects: AsyncThunk<EditDomainRedirects, any, any>,
|
editDomainRedirects: AsyncThunk<EditDomainRedirects, any, any>,
|
||||||
) => {
|
) => {
|
||||||
const listDomains = createAsyncThunk(`${REDUCER_PREFIX}/listDomains`, async (): Promise<ListDomains> => {
|
const listDomains = createAsyncThunk(`${REDUCER_PREFIX}/listDomains`, async (): Promise<ListDomains> => {
|
||||||
const { data, defaultRedirects } = await apiClient.listDomains();
|
const { data, defaultRedirects } = await apiClientFactory().listDomains();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
domains: data.map((domain): Domain => ({ ...domain, status: 'validating' })),
|
domains: data.map((domain): Domain => ({ ...domain, status: 'validating' })),
|
||||||
|
@ -57,7 +57,7 @@ export const domainsListReducerCreator = (
|
||||||
`${REDUCER_PREFIX}/checkDomainHealth`,
|
`${REDUCER_PREFIX}/checkDomainHealth`,
|
||||||
async (domain: string): Promise<ValidateDomain> => {
|
async (domain: string): Promise<ValidateDomain> => {
|
||||||
try {
|
try {
|
||||||
const { status } = await apiClient.health(domain);
|
const { status } = await apiClientFactory().health(domain);
|
||||||
return { domain, status: status === 'pass' ? 'valid' : 'invalid' };
|
return { domain, status: status === 'pass' ? 'valid' : 'invalid' };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { domain, status: 'invalid' };
|
return { domain, status: 'invalid' };
|
||||||
|
|
|
@ -21,7 +21,7 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
bottle.serviceFactory(
|
bottle.serviceFactory(
|
||||||
'domainsListReducerCreator',
|
'domainsListReducerCreator',
|
||||||
domainsListReducerCreator,
|
domainsListReducerCreator,
|
||||||
'apiClient',
|
'apiClientFactory',
|
||||||
'editDomainRedirects',
|
'editDomainRedirects',
|
||||||
);
|
);
|
||||||
bottle.serviceFactory('domainsListReducer', prop('reducer'), 'domainsListReducerCreator');
|
bottle.serviceFactory('domainsListReducer', prop('reducer'), 'domainsListReducerCreator');
|
||||||
|
@ -29,6 +29,6 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
// Actions
|
// Actions
|
||||||
bottle.serviceFactory('listDomains', prop('listDomains'), 'domainsListReducerCreator');
|
bottle.serviceFactory('listDomains', prop('listDomains'), 'domainsListReducerCreator');
|
||||||
bottle.serviceFactory('filterDomains', prop('filterDomains'), 'domainsListReducerCreator');
|
bottle.serviceFactory('filterDomains', prop('filterDomains'), 'domainsListReducerCreator');
|
||||||
bottle.serviceFactory('editDomainRedirects', editDomainRedirects, 'apiClient');
|
bottle.serviceFactory('editDomainRedirects', editDomainRedirects, 'apiClientFactory');
|
||||||
bottle.serviceFactory('checkDomainHealth', prop('checkDomainHealth'), 'domainsListReducerCreator');
|
bottle.serviceFactory('checkDomainHealth', prop('checkDomainHealth'), 'domainsListReducerCreator');
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,3 +2,5 @@ import { bottle } from './container';
|
||||||
import { createShlinkWebComponent } from './ShlinkWebComponent';
|
import { createShlinkWebComponent } from './ShlinkWebComponent';
|
||||||
|
|
||||||
export const ShlinkWebComponent = createShlinkWebComponent(bottle);
|
export const ShlinkWebComponent = createShlinkWebComponent(bottle);
|
||||||
|
|
||||||
|
export type { Settings } from './utils/settings';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import { createAsyncThunk } from '../../../src/utils/helpers/redux';
|
|
||||||
import type { ShlinkApiClient, ShlinkMercureInfo } from '../../api-contract';
|
import type { ShlinkApiClient, ShlinkMercureInfo } from '../../api-contract';
|
||||||
|
import { createAsyncThunk } from '../../utils/redux';
|
||||||
|
|
||||||
const REDUCER_PREFIX = 'shlink/mercure';
|
const REDUCER_PREFIX = 'shlink/mercure';
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ const initialState: MercureInfo = {
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mercureInfoReducerCreator = (apiClient: ShlinkApiClient) => {
|
export const mercureInfoReducerCreator = (apiClientFactory: () => ShlinkApiClient) => {
|
||||||
const loadMercureInfo = createAsyncThunk(
|
const loadMercureInfo = createAsyncThunk(
|
||||||
`${REDUCER_PREFIX}/loadMercureInfo`,
|
`${REDUCER_PREFIX}/loadMercureInfo`,
|
||||||
(): Promise<ShlinkMercureInfo> =>
|
(): Promise<ShlinkMercureInfo> =>
|
||||||
|
@ -25,7 +25,7 @@ export const mercureInfoReducerCreator = (apiClient: ShlinkApiClient) => {
|
||||||
// throw new Error('Real time updates not enabled');
|
// throw new Error('Real time updates not enabled');
|
||||||
// }
|
// }
|
||||||
|
|
||||||
apiClient.mercureInfo()
|
apiClientFactory().mercureInfo()
|
||||||
,
|
,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { mercureInfoReducerCreator } from '../reducers/mercureInfo';
|
||||||
|
|
||||||
export const provideServices = (bottle: Bottle) => {
|
export const provideServices = (bottle: Bottle) => {
|
||||||
// Reducer
|
// Reducer
|
||||||
bottle.serviceFactory('mercureInfoReducerCreator', mercureInfoReducerCreator, 'apiClient');
|
bottle.serviceFactory('mercureInfoReducerCreator', mercureInfoReducerCreator, 'apiClientFactory');
|
||||||
bottle.serviceFactory('mercureInfoReducer', prop('reducer'), 'mercureInfoReducerCreator');
|
bottle.serviceFactory('mercureInfoReducer', prop('reducer'), 'mercureInfoReducerCreator');
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
|
|
@ -14,7 +14,7 @@ export interface ExportShortUrlsBtnProps {
|
||||||
const itemsPerPage = 20;
|
const itemsPerPage = 20;
|
||||||
|
|
||||||
export const ExportShortUrlsBtn = (
|
export const ExportShortUrlsBtn = (
|
||||||
apiClient: ShlinkApiClient,
|
apiClientFactory: () => ShlinkApiClient,
|
||||||
{ exportShortUrls }: ReportExporter,
|
{ exportShortUrls }: ReportExporter,
|
||||||
): FC<ExportShortUrlsBtnProps> => ({ amount = 0 }) => {
|
): FC<ExportShortUrlsBtnProps> => ({ amount = 0 }) => {
|
||||||
const [{ tags, search, startDate, endDate, orderBy, tagsMode }] = useShortUrlsQuery();
|
const [{ tags, search, startDate, endDate, orderBy, tagsMode }] = useShortUrlsQuery();
|
||||||
|
@ -22,7 +22,7 @@ export const ExportShortUrlsBtn = (
|
||||||
const exportAllUrls = useCallback(async () => {
|
const exportAllUrls = useCallback(async () => {
|
||||||
const totalPages = amount / itemsPerPage;
|
const totalPages = amount / itemsPerPage;
|
||||||
const loadAllUrls = async (page = 1): Promise<ShortUrl[]> => {
|
const loadAllUrls = async (page = 1): Promise<ShortUrl[]> => {
|
||||||
const { data } = await apiClient.listShortUrls(
|
const { data } = await apiClientFactory().listShortUrls(
|
||||||
{ page: `${page}`, tags, searchTerm: search, startDate, endDate, orderBy, tagsMode, itemsPerPage },
|
{ page: `${page}`, tags, searchTerm: search, startDate, endDate, orderBy, tagsMode, itemsPerPage },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import { createAsyncThunk } from '../../../src/utils/helpers/redux';
|
|
||||||
import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract';
|
import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract';
|
||||||
import { parseApiError } from '../../api-contract/utils';
|
import { parseApiError } from '../../api-contract/utils';
|
||||||
|
import { createAsyncThunk } from '../../utils/redux';
|
||||||
import type { ShortUrl, ShortUrlData } from '../data';
|
import type { ShortUrl, ShortUrlData } from '../data';
|
||||||
|
|
||||||
const REDUCER_PREFIX = 'shlink/shortUrlCreation';
|
const REDUCER_PREFIX = 'shlink/shortUrlCreation';
|
||||||
|
@ -35,9 +35,9 @@ const initialState: ShortUrlCreation = {
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createShortUrl = (apiClient: ShlinkApiClient) => createAsyncThunk(
|
export const createShortUrl = (apiClientFactory: () => ShlinkApiClient) => createAsyncThunk(
|
||||||
`${REDUCER_PREFIX}/createShortUrl`,
|
`${REDUCER_PREFIX}/createShortUrl`,
|
||||||
(data: ShortUrlData): Promise<ShortUrl> => apiClient.createShortUrl(data),
|
(data: ShortUrlData): Promise<ShortUrl> => apiClientFactory().createShortUrl(data),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const shortUrlCreationReducerCreator = (createShortUrlThunk: ReturnType<typeof createShortUrl>) => {
|
export const shortUrlCreationReducerCreator = (createShortUrlThunk: ReturnType<typeof createShortUrl>) => {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { createAction, createSlice } from '@reduxjs/toolkit';
|
import { createAction, createSlice } from '@reduxjs/toolkit';
|
||||||
import { createAsyncThunk } from '../../../src/utils/helpers/redux';
|
|
||||||
import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract';
|
import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract';
|
||||||
import { parseApiError } from '../../api-contract/utils';
|
import { parseApiError } from '../../api-contract/utils';
|
||||||
|
import { createAsyncThunk } from '../../utils/redux';
|
||||||
import type { ShortUrl, ShortUrlIdentifier } from '../data';
|
import type { ShortUrl, ShortUrlIdentifier } from '../data';
|
||||||
|
|
||||||
const REDUCER_PREFIX = 'shlink/shortUrlDeletion';
|
const REDUCER_PREFIX = 'shlink/shortUrlDeletion';
|
||||||
|
@ -21,10 +21,10 @@ const initialState: ShortUrlDeletion = {
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteShortUrl = (apiClient: ShlinkApiClient) => createAsyncThunk(
|
export const deleteShortUrl = (apiClientFactory: () => ShlinkApiClient) => createAsyncThunk(
|
||||||
`${REDUCER_PREFIX}/deleteShortUrl`,
|
`${REDUCER_PREFIX}/deleteShortUrl`,
|
||||||
async ({ shortCode, domain }: ShortUrlIdentifier): Promise<ShortUrlIdentifier> => {
|
async ({ shortCode, domain }: ShortUrlIdentifier): Promise<ShortUrlIdentifier> => {
|
||||||
await apiClient.deleteShortUrl(shortCode, domain);
|
await apiClientFactory().deleteShortUrl(shortCode, domain);
|
||||||
return { shortCode, domain };
|
return { shortCode, domain };
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import { createAsyncThunk } from '../../../src/utils/helpers/redux';
|
|
||||||
import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract';
|
import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract';
|
||||||
import { parseApiError } from '../../api-contract/utils';
|
import { parseApiError } from '../../api-contract/utils';
|
||||||
|
import { createAsyncThunk } from '../../utils/redux';
|
||||||
import type { ShortUrl, ShortUrlIdentifier } from '../data';
|
import type { ShortUrl, ShortUrlIdentifier } from '../data';
|
||||||
import { shortUrlMatches } from '../helpers';
|
import { shortUrlMatches } from '../helpers';
|
||||||
|
|
||||||
|
@ -22,14 +22,14 @@ const initialState: ShortUrlDetail = {
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const shortUrlDetailReducerCreator = (apiClient: ShlinkApiClient) => {
|
export const shortUrlDetailReducerCreator = (apiClientFactory: () => ShlinkApiClient) => {
|
||||||
const getShortUrlDetail = createAsyncThunk(
|
const getShortUrlDetail = createAsyncThunk(
|
||||||
`${REDUCER_PREFIX}/getShortUrlDetail`,
|
`${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));
|
||||||
|
|
||||||
return alreadyLoaded ?? await apiClient.getShortUrl(shortCode, domain);
|
return alreadyLoaded ?? await apiClientFactory().getShortUrl(shortCode, domain);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import { createAsyncThunk } from '../../../src/utils/helpers/redux';
|
|
||||||
import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract';
|
import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract';
|
||||||
import { parseApiError } from '../../api-contract/utils';
|
import { parseApiError } from '../../api-contract/utils';
|
||||||
|
import { createAsyncThunk } from '../../utils/redux';
|
||||||
import type { EditShortUrlData, ShortUrl, ShortUrlIdentifier } from '../data';
|
import type { EditShortUrlData, ShortUrl, ShortUrlIdentifier } from '../data';
|
||||||
|
|
||||||
const REDUCER_PREFIX = 'shlink/shortUrlEdition';
|
const REDUCER_PREFIX = 'shlink/shortUrlEdition';
|
||||||
|
@ -27,10 +27,10 @@ const initialState: ShortUrlEdition = {
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const editShortUrl = (apiClient: ShlinkApiClient) => createAsyncThunk(
|
export const editShortUrl = (apiClientFactory: () => ShlinkApiClient) => createAsyncThunk(
|
||||||
`${REDUCER_PREFIX}/editShortUrl`,
|
`${REDUCER_PREFIX}/editShortUrl`,
|
||||||
({ shortCode, domain, data }: EditShortUrl): Promise<ShortUrl> =>
|
({ shortCode, domain, data }: EditShortUrl): Promise<ShortUrl> =>
|
||||||
apiClient.updateShortUrl(shortCode, domain, data as any) // TODO parse dates
|
apiClientFactory().updateShortUrl(shortCode, domain, data as any) // TODO parse dates
|
||||||
,
|
,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import { assocPath, last, pipe, reject } from 'ramda';
|
import { assocPath, last, pipe, reject } from 'ramda';
|
||||||
import { createAsyncThunk } from '../../../src/utils/helpers/redux';
|
|
||||||
import type { ShlinkApiClient, ShlinkShortUrlsListParams, ShlinkShortUrlsResponse } from '../../api-contract';
|
import type { ShlinkApiClient, ShlinkShortUrlsListParams, ShlinkShortUrlsResponse } from '../../api-contract';
|
||||||
|
import { createAsyncThunk } from '../../utils/redux';
|
||||||
import { createNewVisits } from '../../visits/reducers/visitCreation';
|
import { createNewVisits } from '../../visits/reducers/visitCreation';
|
||||||
import type { ShortUrl } from '../data';
|
import type { ShortUrl } from '../data';
|
||||||
import { shortUrlMatches } from '../helpers';
|
import { shortUrlMatches } from '../helpers';
|
||||||
|
@ -23,9 +23,11 @@ const initialState: ShortUrlsList = {
|
||||||
error: false,
|
error: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const listShortUrls = (apiClient: ShlinkApiClient) => createAsyncThunk(
|
export const listShortUrls = (apiClientFactory: () => ShlinkApiClient) => createAsyncThunk(
|
||||||
`${REDUCER_PREFIX}/listShortUrls`,
|
`${REDUCER_PREFIX}/listShortUrls`,
|
||||||
(params: ShlinkShortUrlsListParams | void): Promise<ShlinkShortUrlsResponse> => apiClient.listShortUrls(params ?? {}),
|
(params: ShlinkShortUrlsListParams | void): Promise<ShlinkShortUrlsResponse> => apiClientFactory().listShortUrls(
|
||||||
|
params ?? {},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const shortUrlsListReducerCreator = (
|
export const shortUrlsListReducerCreator = (
|
||||||
|
|
|
@ -54,7 +54,7 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
bottle.serviceFactory('QrCodeModal', QrCodeModal, 'ImageDownloader');
|
bottle.serviceFactory('QrCodeModal', QrCodeModal, 'ImageDownloader');
|
||||||
bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ExportShortUrlsBtn', 'TagsSelector');
|
bottle.serviceFactory('ShortUrlsFilteringBar', ShortUrlsFilteringBar, 'ExportShortUrlsBtn', 'TagsSelector');
|
||||||
|
|
||||||
bottle.serviceFactory('ExportShortUrlsBtn', ExportShortUrlsBtn, 'apiClient', 'ReportExporter');
|
bottle.serviceFactory('ExportShortUrlsBtn', ExportShortUrlsBtn, 'apiClientFactory', 'ReportExporter');
|
||||||
|
|
||||||
// Reducers
|
// Reducers
|
||||||
bottle.serviceFactory(
|
bottle.serviceFactory(
|
||||||
|
@ -75,20 +75,20 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
bottle.serviceFactory('shortUrlDeletionReducerCreator', shortUrlDeletionReducerCreator, 'deleteShortUrl');
|
bottle.serviceFactory('shortUrlDeletionReducerCreator', shortUrlDeletionReducerCreator, 'deleteShortUrl');
|
||||||
bottle.serviceFactory('shortUrlDeletionReducer', prop('reducer'), 'shortUrlDeletionReducerCreator');
|
bottle.serviceFactory('shortUrlDeletionReducer', prop('reducer'), 'shortUrlDeletionReducerCreator');
|
||||||
|
|
||||||
bottle.serviceFactory('shortUrlDetailReducerCreator', shortUrlDetailReducerCreator, 'apiClient');
|
bottle.serviceFactory('shortUrlDetailReducerCreator', shortUrlDetailReducerCreator, 'apiClientFactory');
|
||||||
bottle.serviceFactory('shortUrlDetailReducer', prop('reducer'), 'shortUrlDetailReducerCreator');
|
bottle.serviceFactory('shortUrlDetailReducer', prop('reducer'), 'shortUrlDetailReducerCreator');
|
||||||
|
|
||||||
// Actions
|
// 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('resetCreateShortUrl', prop('resetCreateShortUrl'), 'shortUrlCreationReducerCreator');
|
||||||
|
|
||||||
bottle.serviceFactory('deleteShortUrl', deleteShortUrl, 'apiClient');
|
bottle.serviceFactory('deleteShortUrl', deleteShortUrl, 'apiClientFactory');
|
||||||
bottle.serviceFactory('resetDeleteShortUrl', prop('resetDeleteShortUrl'), 'shortUrlDeletionReducerCreator');
|
bottle.serviceFactory('resetDeleteShortUrl', prop('resetDeleteShortUrl'), 'shortUrlDeletionReducerCreator');
|
||||||
bottle.serviceFactory('shortUrlDeleted', () => shortUrlDeleted);
|
bottle.serviceFactory('shortUrlDeleted', () => shortUrlDeleted);
|
||||||
|
|
||||||
bottle.serviceFactory('getShortUrlDetail', prop('getShortUrlDetail'), 'shortUrlDetailReducerCreator');
|
bottle.serviceFactory('getShortUrlDetail', prop('getShortUrlDetail'), 'shortUrlDetailReducerCreator');
|
||||||
|
|
||||||
bottle.serviceFactory('editShortUrl', editShortUrl, 'apiClient');
|
bottle.serviceFactory('editShortUrl', editShortUrl, 'apiClientFactory');
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { createAction, createSlice } from '@reduxjs/toolkit';
|
import { createAction, createSlice } from '@reduxjs/toolkit';
|
||||||
import { createAsyncThunk } from '../../../src/utils/helpers/redux';
|
|
||||||
import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract';
|
import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract';
|
||||||
import { parseApiError } from '../../api-contract/utils';
|
import { parseApiError } from '../../api-contract/utils';
|
||||||
|
import { createAsyncThunk } from '../../utils/redux';
|
||||||
|
|
||||||
const REDUCER_PREFIX = 'shlink/tagDelete';
|
const REDUCER_PREFIX = 'shlink/tagDelete';
|
||||||
|
|
||||||
|
@ -20,9 +20,9 @@ const initialState: TagDeletion = {
|
||||||
|
|
||||||
export const tagDeleted = createAction<string>(`${REDUCER_PREFIX}/tagDeleted`);
|
export const tagDeleted = createAction<string>(`${REDUCER_PREFIX}/tagDeleted`);
|
||||||
|
|
||||||
export const tagDeleteReducerCreator = (apiClient: ShlinkApiClient) => {
|
export const tagDeleteReducerCreator = (apiClientFactory: () => ShlinkApiClient) => {
|
||||||
const deleteTag = createAsyncThunk(`${REDUCER_PREFIX}/deleteTag`, async (tag: string): Promise<void> => {
|
const deleteTag = createAsyncThunk(`${REDUCER_PREFIX}/deleteTag`, async (tag: string): Promise<void> => {
|
||||||
await apiClient.deleteTags([tag]);
|
await apiClientFactory().deleteTags([tag]);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { reducer } = createSlice({
|
const { reducer } = createSlice({
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createAction, createSlice } from '@reduxjs/toolkit';
|
import { createAction, createSlice } from '@reduxjs/toolkit';
|
||||||
import { pick } from 'ramda';
|
import { pick } from 'ramda';
|
||||||
import { createAsyncThunk } from '../../../src/utils/helpers/redux';
|
|
||||||
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator';
|
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator';
|
||||||
import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract';
|
import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract';
|
||||||
import { parseApiError } from '../../api-contract/utils';
|
import { parseApiError } from '../../api-contract/utils';
|
||||||
|
import { createAsyncThunk } from '../../utils/redux';
|
||||||
|
|
||||||
const REDUCER_PREFIX = 'shlink/tagEdit';
|
const REDUCER_PREFIX = 'shlink/tagEdit';
|
||||||
|
|
||||||
|
@ -34,12 +34,12 @@ const initialState: TagEdition = {
|
||||||
export const tagEdited = createAction<EditTag>(`${REDUCER_PREFIX}/tagEdited`);
|
export const tagEdited = createAction<EditTag>(`${REDUCER_PREFIX}/tagEdited`);
|
||||||
|
|
||||||
export const editTag = (
|
export const editTag = (
|
||||||
apiClient: ShlinkApiClient,
|
apiClientFactory: () => ShlinkApiClient,
|
||||||
colorGenerator: ColorGenerator,
|
colorGenerator: ColorGenerator,
|
||||||
) => createAsyncThunk(
|
) => createAsyncThunk(
|
||||||
`${REDUCER_PREFIX}/editTag`,
|
`${REDUCER_PREFIX}/editTag`,
|
||||||
async ({ oldName, newName, color }: EditTag): Promise<EditTag> => {
|
async ({ oldName, newName, color }: EditTag): Promise<EditTag> => {
|
||||||
await apiClient.editTag(oldName, newName);
|
await apiClientFactory().editTag(oldName, newName);
|
||||||
colorGenerator.setColorForKey(newName, color);
|
colorGenerator.setColorForKey(newName, color);
|
||||||
|
|
||||||
return { oldName, newName, color };
|
return { oldName, newName, color };
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { createAction, createSlice } from '@reduxjs/toolkit';
|
import { createAction, createSlice } from '@reduxjs/toolkit';
|
||||||
import { isEmpty, reject } from 'ramda';
|
import { isEmpty, reject } from 'ramda';
|
||||||
import { createAsyncThunk } from '../../../src/utils/helpers/redux';
|
|
||||||
import type { ProblemDetailsError, ShlinkApiClient, ShlinkTags } from '../../api-contract';
|
import type { ProblemDetailsError, ShlinkApiClient, ShlinkTags } from '../../api-contract';
|
||||||
import { parseApiError } from '../../api-contract/utils';
|
import { parseApiError } from '../../api-contract/utils';
|
||||||
import type { createShortUrl } from '../../short-urls/reducers/shortUrlCreation';
|
import type { createShortUrl } from '../../short-urls/reducers/shortUrlCreation';
|
||||||
|
import { createAsyncThunk } from '../../utils/redux';
|
||||||
import { createNewVisits } from '../../visits/reducers/visitCreation';
|
import { createNewVisits } from '../../visits/reducers/visitCreation';
|
||||||
import type { CreateVisit } from '../../visits/types';
|
import type { CreateVisit } from '../../visits/types';
|
||||||
import type { TagStats } from '../data';
|
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`,
|
`${REDUCER_PREFIX}/listTags`,
|
||||||
async (_: void, { getState }): Promise<ListTags> => {
|
async (_: void, { getState }): Promise<ListTags> => {
|
||||||
const { tagsList } = getState();
|
const { tagsList } = getState();
|
||||||
|
@ -89,7 +89,7 @@ export const listTags = (apiClient: ShlinkApiClient, force = true) => createAsyn
|
||||||
return tagsList;
|
return tagsList;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { tags, stats }: ShlinkTags = await apiClient.tagsStats();
|
const { tags, stats }: ShlinkTags = await apiClientFactory().tagsStats();
|
||||||
const processedStats = stats.reduce<TagsStatsMap>((acc, { tag, ...rest }) => {
|
const processedStats = stats.reduce<TagsStatsMap>((acc, { tag, ...rest }) => {
|
||||||
acc[tag] = rest;
|
acc[tag] = rest;
|
||||||
return acc;
|
return acc;
|
||||||
|
|
|
@ -36,14 +36,17 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
bottle.serviceFactory('tagEditReducerCreator', tagEditReducerCreator, 'editTag');
|
bottle.serviceFactory('tagEditReducerCreator', tagEditReducerCreator, 'editTag');
|
||||||
bottle.serviceFactory('tagEditReducer', prop('reducer'), 'tagEditReducerCreator');
|
bottle.serviceFactory('tagEditReducer', prop('reducer'), 'tagEditReducerCreator');
|
||||||
|
|
||||||
bottle.serviceFactory('tagDeleteReducerCreator', tagDeleteReducerCreator, 'apiClient');
|
bottle.serviceFactory('tagDeleteReducerCreator', tagDeleteReducerCreator, 'apiClientFactory');
|
||||||
bottle.serviceFactory('tagDeleteReducer', prop('reducer'), 'tagDeleteReducerCreator');
|
bottle.serviceFactory('tagDeleteReducer', prop('reducer'), 'tagDeleteReducerCreator');
|
||||||
|
|
||||||
bottle.serviceFactory('tagsListReducerCreator', tagsListReducerCreator, 'listTags', 'createShortUrl');
|
bottle.serviceFactory('tagsListReducerCreator', tagsListReducerCreator, 'listTags', 'createShortUrl');
|
||||||
bottle.serviceFactory('tagsListReducer', prop('reducer'), 'tagsListReducerCreator');
|
bottle.serviceFactory('tagsListReducer', prop('reducer'), 'tagsListReducerCreator');
|
||||||
|
|
||||||
// Actions
|
// 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('listTags', listTagsActionFactory(false));
|
||||||
bottle.factory('forceListTags', listTagsActionFactory(true));
|
bottle.factory('forceListTags', listTagsActionFactory(true));
|
||||||
|
@ -52,6 +55,6 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
bottle.serviceFactory('deleteTag', prop('deleteTag'), 'tagDeleteReducerCreator');
|
bottle.serviceFactory('deleteTag', prop('deleteTag'), 'tagDeleteReducerCreator');
|
||||||
bottle.serviceFactory('tagDeleted', () => tagDeleted);
|
bottle.serviceFactory('tagDeleted', () => tagDeleted);
|
||||||
|
|
||||||
bottle.serviceFactory('editTag', editTag, 'apiClient', 'ColorGenerator');
|
bottle.serviceFactory('editTag', editTag, 'apiClientFactory', 'ColorGenerator');
|
||||||
bottle.serviceFactory('tagEdited', () => tagEdited);
|
bottle.serviceFactory('tagEdited', () => tagEdited);
|
||||||
};
|
};
|
||||||
|
|
13
shlink-web-component/utils/redux.ts
Normal file
13
shlink-web-component/utils/redux.ts
Normal file
|
@ -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 = <Returned, ThunkArg>(
|
||||||
|
typePrefix: string,
|
||||||
|
payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg, { state: RootState, serializedErrorType: any }>,
|
||||||
|
) => baseCreateAsyncThunk(
|
||||||
|
typePrefix,
|
||||||
|
payloadCreator,
|
||||||
|
{ serializeError: identity },
|
||||||
|
);
|
|
@ -1,11 +1,11 @@
|
||||||
import { createAction, createSlice } from '@reduxjs/toolkit';
|
import { createAction, createSlice } from '@reduxjs/toolkit';
|
||||||
import { flatten, prop, range, splitEvery } from 'ramda';
|
import { flatten, prop, range, splitEvery } from 'ramda';
|
||||||
import type { ShlinkState } from '../../../src/container/types';
|
|
||||||
import type { DateInterval } from '../../../src/utils/helpers/dateIntervals';
|
import type { DateInterval } from '../../../src/utils/helpers/dateIntervals';
|
||||||
import { dateToMatchingInterval } 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 type { ShlinkPaginator, ShlinkVisits, ShlinkVisitsParams } from '../../api-contract';
|
||||||
import { parseApiError } from '../../api-contract/utils';
|
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 { CreateVisit, Visit } from '../types';
|
||||||
import type { LoadVisits, VisitsInfo, VisitsLoaded } from './types';
|
import type { LoadVisits, VisitsInfo, VisitsLoaded } from './types';
|
||||||
import { createNewVisits } from './visitCreation';
|
import { createNewVisits } from './visitCreation';
|
||||||
|
@ -24,7 +24,7 @@ interface VisitsAsyncThunkOptions<T extends LoadVisits = LoadVisits, R extends V
|
||||||
typePrefix: string;
|
typePrefix: string;
|
||||||
createLoaders: (params: T) => [VisitsLoader, LastVisitLoader];
|
createLoaders: (params: T) => [VisitsLoader, LastVisitLoader];
|
||||||
getExtraFulfilledPayload: (params: T) => Partial<R>;
|
getExtraFulfilledPayload: (params: T) => Partial<R>;
|
||||||
shouldCancel: (getState: () => ShlinkState) => boolean;
|
shouldCancel: (getState: () => RootState) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createVisitsAsyncThunk = <T extends LoadVisits = LoadVisits, R extends VisitsLoaded = VisitsLoaded>(
|
export const createVisitsAsyncThunk = <T extends LoadVisits = LoadVisits, R extends VisitsLoaded = VisitsLoaded>(
|
||||||
|
|
|
@ -26,10 +26,10 @@ const initialState: DomainVisits = {
|
||||||
progress: 0,
|
progress: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDomainVisits = (apiClient: ShlinkApiClient) => createVisitsAsyncThunk({
|
export const getDomainVisits = (apiClientFactory: () => ShlinkApiClient) => createVisitsAsyncThunk({
|
||||||
typePrefix: `${REDUCER_PREFIX}/getDomainVisits`,
|
typePrefix: `${REDUCER_PREFIX}/getDomainVisits`,
|
||||||
createLoaders: ({ domain, query = {}, doIntervalFallback = false }: LoadDomainVisits) => {
|
createLoaders: ({ domain, query = {}, doIntervalFallback = false }: LoadDomainVisits) => {
|
||||||
const { getDomainVisits: getVisits } = apiClient;
|
const { getDomainVisits: getVisits } = apiClientFactory();
|
||||||
const visitsLoader = async (page: number, itemsPerPage: number) => getVisits(
|
const visitsLoader = async (page: number, itemsPerPage: number) => getVisits(
|
||||||
domain,
|
domain,
|
||||||
{ ...query, page, itemsPerPage },
|
{ ...query, page, itemsPerPage },
|
||||||
|
|
|
@ -14,10 +14,10 @@ const initialState: VisitsInfo = {
|
||||||
progress: 0,
|
progress: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getNonOrphanVisits = (apiClient: ShlinkApiClient) => createVisitsAsyncThunk({
|
export const getNonOrphanVisits = (apiClientFactory: () => ShlinkApiClient) => createVisitsAsyncThunk({
|
||||||
typePrefix: `${REDUCER_PREFIX}/getNonOrphanVisits`,
|
typePrefix: `${REDUCER_PREFIX}/getNonOrphanVisits`,
|
||||||
createLoaders: ({ query = {}, doIntervalFallback = false }) => {
|
createLoaders: ({ query = {}, doIntervalFallback = false }) => {
|
||||||
const { getNonOrphanVisits: shlinkGetNonOrphanVisits } = apiClient;
|
const { getNonOrphanVisits: shlinkGetNonOrphanVisits } = apiClientFactory();
|
||||||
const visitsLoader = async (page: number, itemsPerPage: number) =>
|
const visitsLoader = async (page: number, itemsPerPage: number) =>
|
||||||
shlinkGetNonOrphanVisits({ ...query, page, itemsPerPage });
|
shlinkGetNonOrphanVisits({ ...query, page, itemsPerPage });
|
||||||
const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, shlinkGetNonOrphanVisits);
|
const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, shlinkGetNonOrphanVisits);
|
||||||
|
|
|
@ -23,10 +23,10 @@ const initialState: VisitsInfo = {
|
||||||
const matchesType = (visit: OrphanVisit, orphanVisitsType?: OrphanVisitType) =>
|
const matchesType = (visit: OrphanVisit, orphanVisitsType?: OrphanVisitType) =>
|
||||||
!orphanVisitsType || orphanVisitsType === visit.type;
|
!orphanVisitsType || orphanVisitsType === visit.type;
|
||||||
|
|
||||||
export const getOrphanVisits = (apiClient: ShlinkApiClient) => createVisitsAsyncThunk({
|
export const getOrphanVisits = (apiClientFactory: () => ShlinkApiClient) => createVisitsAsyncThunk({
|
||||||
typePrefix: `${REDUCER_PREFIX}/getOrphanVisits`,
|
typePrefix: `${REDUCER_PREFIX}/getOrphanVisits`,
|
||||||
createLoaders: ({ orphanVisitsType, query = {}, doIntervalFallback = false }: LoadOrphanVisits) => {
|
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 })
|
const visitsLoader = async (page: number, itemsPerPage: number) => getVisits({ ...query, page, itemsPerPage })
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
const visits = result.data.filter((visit) => isOrphanVisit(visit) && matchesType(visit, orphanVisitsType));
|
const visits = result.data.filter((visit) => isOrphanVisit(visit) && matchesType(visit, orphanVisitsType));
|
||||||
|
|
|
@ -24,10 +24,10 @@ const initialState: ShortUrlVisits = {
|
||||||
progress: 0,
|
progress: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getShortUrlVisits = (apiClient: ShlinkApiClient) => createVisitsAsyncThunk({
|
export const getShortUrlVisits = (apiClientFactory: () => ShlinkApiClient) => createVisitsAsyncThunk({
|
||||||
typePrefix: `${REDUCER_PREFIX}/getShortUrlVisits`,
|
typePrefix: `${REDUCER_PREFIX}/getShortUrlVisits`,
|
||||||
createLoaders: ({ shortCode, query = {}, doIntervalFallback = false }: LoadShortUrlVisits) => {
|
createLoaders: ({ shortCode, query = {}, doIntervalFallback = false }: LoadShortUrlVisits) => {
|
||||||
const { getShortUrlVisits: shlinkGetShortUrlVisits } = apiClient;
|
const { getShortUrlVisits: shlinkGetShortUrlVisits } = apiClientFactory();
|
||||||
const visitsLoader = async (page: number, itemsPerPage: number) => shlinkGetShortUrlVisits(
|
const visitsLoader = async (page: number, itemsPerPage: number) => shlinkGetShortUrlVisits(
|
||||||
shortCode,
|
shortCode,
|
||||||
{ ...query, page, itemsPerPage },
|
{ ...query, page, itemsPerPage },
|
||||||
|
|
|
@ -23,10 +23,10 @@ const initialState: TagVisits = {
|
||||||
progress: 0,
|
progress: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTagVisits = (apiClient: ShlinkApiClient) => createVisitsAsyncThunk({
|
export const getTagVisits = (apiClientFactory: () => ShlinkApiClient) => createVisitsAsyncThunk({
|
||||||
typePrefix: `${REDUCER_PREFIX}/getTagVisits`,
|
typePrefix: `${REDUCER_PREFIX}/getTagVisits`,
|
||||||
createLoaders: ({ tag, query = {}, doIntervalFallback = false }: LoadTagVisits) => {
|
createLoaders: ({ tag, query = {}, doIntervalFallback = false }: LoadTagVisits) => {
|
||||||
const { getTagVisits: getVisits } = apiClient;
|
const { getTagVisits: getVisits } = apiClientFactory();
|
||||||
const visitsLoader = async (page: number, itemsPerPage: number) => getVisits(
|
const visitsLoader = async (page: number, itemsPerPage: number) => getVisits(
|
||||||
tag,
|
tag,
|
||||||
{ ...query, page, itemsPerPage },
|
{ ...query, page, itemsPerPage },
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import { createAsyncThunk } from '../../../src/utils/helpers/redux';
|
|
||||||
import type { ShlinkApiClient, ShlinkVisitsOverview } from '../../api-contract';
|
import type { ShlinkApiClient, ShlinkVisitsOverview } from '../../api-contract';
|
||||||
|
import { createAsyncThunk } from '../../utils/redux';
|
||||||
import type { CreateVisit } from '../types';
|
import type { CreateVisit } from '../types';
|
||||||
import { groupNewVisitsByType } from '../types/helpers';
|
import { groupNewVisitsByType } from '../types/helpers';
|
||||||
import { createNewVisits } from './visitCreation';
|
import { createNewVisits } from './visitCreation';
|
||||||
|
@ -39,9 +39,9 @@ const initialState: VisitsOverview = {
|
||||||
|
|
||||||
const countBots = (visits: CreateVisit[]) => visits.filter(({ visit }) => visit.potentialBot).length;
|
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`,
|
`${REDUCER_PREFIX}/loadVisitsOverview`,
|
||||||
(): Promise<ParsedVisitsOverview> => apiClient.getVisitsOverview().then(
|
(): Promise<ParsedVisitsOverview> => apiClientFactory().getVisitsOverview().then(
|
||||||
({ nonOrphanVisits, visitsCount, orphanVisits, orphanVisitsCount }) => ({
|
({ nonOrphanVisits, visitsCount, orphanVisits, orphanVisitsCount }) => ({
|
||||||
nonOrphanVisits: {
|
nonOrphanVisits: {
|
||||||
total: nonOrphanVisits?.total ?? visitsCount,
|
total: nonOrphanVisits?.total ?? visitsCount,
|
||||||
|
|
|
@ -54,23 +54,23 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
bottle.serviceFactory('VisitsParser', () => visitsParser);
|
bottle.serviceFactory('VisitsParser', () => visitsParser);
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
bottle.serviceFactory('getShortUrlVisits', getShortUrlVisits, 'apiClient');
|
bottle.serviceFactory('getShortUrlVisits', getShortUrlVisits, 'apiClientFactory');
|
||||||
bottle.serviceFactory('cancelGetShortUrlVisits', prop('cancelGetVisits'), 'shortUrlVisitsReducerCreator');
|
bottle.serviceFactory('cancelGetShortUrlVisits', prop('cancelGetVisits'), 'shortUrlVisitsReducerCreator');
|
||||||
|
|
||||||
bottle.serviceFactory('getTagVisits', getTagVisits, 'apiClient');
|
bottle.serviceFactory('getTagVisits', getTagVisits, 'apiClientFactory');
|
||||||
bottle.serviceFactory('cancelGetTagVisits', prop('cancelGetVisits'), 'tagVisitsReducerCreator');
|
bottle.serviceFactory('cancelGetTagVisits', prop('cancelGetVisits'), 'tagVisitsReducerCreator');
|
||||||
|
|
||||||
bottle.serviceFactory('getDomainVisits', getDomainVisits, 'apiClient');
|
bottle.serviceFactory('getDomainVisits', getDomainVisits, 'apiClientFactory');
|
||||||
bottle.serviceFactory('cancelGetDomainVisits', prop('cancelGetVisits'), 'domainVisitsReducerCreator');
|
bottle.serviceFactory('cancelGetDomainVisits', prop('cancelGetVisits'), 'domainVisitsReducerCreator');
|
||||||
|
|
||||||
bottle.serviceFactory('getOrphanVisits', getOrphanVisits, 'apiClient');
|
bottle.serviceFactory('getOrphanVisits', getOrphanVisits, 'apiClientFactory');
|
||||||
bottle.serviceFactory('cancelGetOrphanVisits', prop('cancelGetVisits'), 'orphanVisitsReducerCreator');
|
bottle.serviceFactory('cancelGetOrphanVisits', prop('cancelGetVisits'), 'orphanVisitsReducerCreator');
|
||||||
|
|
||||||
bottle.serviceFactory('getNonOrphanVisits', getNonOrphanVisits, 'apiClient');
|
bottle.serviceFactory('getNonOrphanVisits', getNonOrphanVisits, 'apiClientFactory');
|
||||||
bottle.serviceFactory('cancelGetNonOrphanVisits', prop('cancelGetVisits'), 'nonOrphanVisitsReducerCreator');
|
bottle.serviceFactory('cancelGetNonOrphanVisits', prop('cancelGetVisits'), 'nonOrphanVisitsReducerCreator');
|
||||||
|
|
||||||
bottle.serviceFactory('createNewVisits', () => createNewVisits);
|
bottle.serviceFactory('createNewVisits', () => createNewVisits);
|
||||||
bottle.serviceFactory('loadVisitsOverview', loadVisitsOverview, 'apiClient');
|
bottle.serviceFactory('loadVisitsOverview', loadVisitsOverview, 'apiClientFactory');
|
||||||
|
|
||||||
// Reducers
|
// Reducers
|
||||||
bottle.serviceFactory('visitsOverviewReducerCreator', visitsOverviewReducerCreator, 'loadVisitsOverview');
|
bottle.serviceFactory('visitsOverviewReducerCreator', visitsOverviewReducerCreator, 'loadVisitsOverview');
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
import type { Settings } from '../../shlink-web-component';
|
||||||
import type { Sidebar } from '../common/reducers/sidebar';
|
import type { Sidebar } from '../common/reducers/sidebar';
|
||||||
import type { SelectedServer, ServersMap } from '../servers/data';
|
import type { SelectedServer, ServersMap } from '../servers/data';
|
||||||
import type { Settings } from '../settings/reducers/settings';
|
|
||||||
|
|
||||||
export interface ShlinkState {
|
export interface ShlinkState {
|
||||||
servers: ServersMap;
|
servers: ServersMap;
|
||||||
|
|
|
@ -35,6 +35,7 @@ export default defineConfig({
|
||||||
reporter: ['text', 'text-summary', 'html', 'clover'],
|
reporter: ['text', 'text-summary', 'html', 'clover'],
|
||||||
include: [
|
include: [
|
||||||
'src/**/*.{ts,tsx}',
|
'src/**/*.{ts,tsx}',
|
||||||
|
'shlink-web-component/**/*.{ts,tsx}',
|
||||||
'!src/*.{ts,tsx}',
|
'!src/*.{ts,tsx}',
|
||||||
'!src/reducers/index.ts',
|
'!src/reducers/index.ts',
|
||||||
'!src/**/provideServices.ts',
|
'!src/**/provideServices.ts',
|
||||||
|
|
Loading…
Reference in a new issue