mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-03-18 05:23:49 +00:00
Create src folder for shlink-web-component
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
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';
|
||||
|
||||
export type ShortUrlCreation = {
|
||||
saving: false;
|
||||
saved: false;
|
||||
error: false;
|
||||
} | {
|
||||
saving: true;
|
||||
saved: false;
|
||||
error: false;
|
||||
} | {
|
||||
saving: false;
|
||||
saved: false;
|
||||
error: true;
|
||||
errorData?: ProblemDetailsError;
|
||||
} | {
|
||||
result: ShortUrl;
|
||||
saving: false;
|
||||
saved: true;
|
||||
error: false;
|
||||
};
|
||||
|
||||
export type CreateShortUrlAction = PayloadAction<ShortUrl>;
|
||||
|
||||
const initialState: ShortUrlCreation = {
|
||||
saving: false,
|
||||
saved: false,
|
||||
error: false,
|
||||
};
|
||||
|
||||
export const createShortUrl = (apiClientFactory: () => ShlinkApiClient) => createAsyncThunk(
|
||||
`${REDUCER_PREFIX}/createShortUrl`,
|
||||
(data: ShortUrlData): Promise<ShortUrl> => apiClientFactory().createShortUrl(data),
|
||||
);
|
||||
|
||||
export const shortUrlCreationReducerCreator = (createShortUrlThunk: ReturnType<typeof createShortUrl>) => {
|
||||
const { reducer, actions } = createSlice({
|
||||
name: REDUCER_PREFIX,
|
||||
initialState: initialState as ShortUrlCreation, // Without this casting it infers type ShortUrlCreationWaiting
|
||||
reducers: {
|
||||
resetCreateShortUrl: () => initialState,
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(createShortUrlThunk.pending, () => ({ saving: true, saved: false, error: false }));
|
||||
builder.addCase(
|
||||
createShortUrlThunk.rejected,
|
||||
(_, { error }) => ({ saving: false, saved: false, error: true, errorData: parseApiError(error) }),
|
||||
);
|
||||
builder.addCase(
|
||||
createShortUrlThunk.fulfilled,
|
||||
(_, { payload: result }) => ({ result, saving: false, saved: true, error: false }),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const { resetCreateShortUrl } = actions;
|
||||
|
||||
return {
|
||||
reducer,
|
||||
resetCreateShortUrl,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { createAction, createSlice } from '@reduxjs/toolkit';
|
||||
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';
|
||||
|
||||
export interface ShortUrlDeletion {
|
||||
shortCode: string;
|
||||
loading: boolean;
|
||||
deleted: boolean;
|
||||
error: boolean;
|
||||
errorData?: ProblemDetailsError;
|
||||
}
|
||||
|
||||
const initialState: ShortUrlDeletion = {
|
||||
shortCode: '',
|
||||
loading: false,
|
||||
deleted: false,
|
||||
error: false,
|
||||
};
|
||||
|
||||
export const deleteShortUrl = (apiClientFactory: () => ShlinkApiClient) => createAsyncThunk(
|
||||
`${REDUCER_PREFIX}/deleteShortUrl`,
|
||||
async ({ shortCode, domain }: ShortUrlIdentifier): Promise<ShortUrlIdentifier> => {
|
||||
await apiClientFactory().deleteShortUrl(shortCode, domain);
|
||||
return { shortCode, domain };
|
||||
},
|
||||
);
|
||||
|
||||
export const shortUrlDeleted = createAction<ShortUrl>(`${REDUCER_PREFIX}/shortUrlDeleted`);
|
||||
|
||||
export const shortUrlDeletionReducerCreator = (deleteShortUrlThunk: ReturnType<typeof deleteShortUrl>) => {
|
||||
const { actions, reducer } = createSlice({
|
||||
name: REDUCER_PREFIX,
|
||||
initialState,
|
||||
reducers: {
|
||||
resetDeleteShortUrl: () => initialState,
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(
|
||||
deleteShortUrlThunk.pending,
|
||||
(state) => ({ ...state, loading: true, error: false, deleted: false }),
|
||||
);
|
||||
builder.addCase(deleteShortUrlThunk.rejected, (state, { error }) => (
|
||||
{ ...state, errorData: parseApiError(error), loading: false, error: true, deleted: false }
|
||||
));
|
||||
builder.addCase(deleteShortUrlThunk.fulfilled, (state, { payload }) => (
|
||||
{ ...state, shortCode: payload.shortCode, loading: false, error: false, deleted: true }
|
||||
));
|
||||
},
|
||||
});
|
||||
|
||||
const { resetDeleteShortUrl } = actions;
|
||||
|
||||
return { reducer, resetDeleteShortUrl };
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
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';
|
||||
|
||||
const REDUCER_PREFIX = 'shlink/shortUrlDetail';
|
||||
|
||||
export interface ShortUrlDetail {
|
||||
shortUrl?: ShortUrl;
|
||||
loading: boolean;
|
||||
error: boolean;
|
||||
errorData?: ProblemDetailsError;
|
||||
}
|
||||
|
||||
export type ShortUrlDetailAction = PayloadAction<ShortUrl>;
|
||||
|
||||
const initialState: ShortUrlDetail = {
|
||||
loading: false,
|
||||
error: false,
|
||||
};
|
||||
|
||||
export const shortUrlDetailReducerCreator = (apiClientFactory: () => ShlinkApiClient) => {
|
||||
const getShortUrlDetail = createAsyncThunk(
|
||||
`${REDUCER_PREFIX}/getShortUrlDetail`,
|
||||
async ({ shortCode, domain }: ShortUrlIdentifier, { getState }): Promise<ShortUrl> => {
|
||||
const { shortUrlsList } = getState();
|
||||
const alreadyLoaded = shortUrlsList?.shortUrls?.data.find((url) => shortUrlMatches(url, shortCode, domain));
|
||||
|
||||
return alreadyLoaded ?? await apiClientFactory().getShortUrl(shortCode, domain);
|
||||
},
|
||||
);
|
||||
|
||||
const { reducer } = createSlice({
|
||||
name: REDUCER_PREFIX,
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(getShortUrlDetail.pending, () => ({ loading: true, error: false }));
|
||||
builder.addCase(getShortUrlDetail.rejected, (_, { error }) => (
|
||||
{ loading: false, error: true, errorData: parseApiError(error) }
|
||||
));
|
||||
builder.addCase(getShortUrlDetail.fulfilled, (_, { payload: shortUrl }) => ({ ...initialState, shortUrl }));
|
||||
},
|
||||
});
|
||||
|
||||
return { reducer, getShortUrlDetail };
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
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';
|
||||
|
||||
export interface ShortUrlEdition {
|
||||
shortUrl?: ShortUrl;
|
||||
saving: boolean;
|
||||
saved: boolean;
|
||||
error: boolean;
|
||||
errorData?: ProblemDetailsError;
|
||||
}
|
||||
|
||||
export interface EditShortUrl extends ShortUrlIdentifier {
|
||||
data: EditShortUrlData;
|
||||
}
|
||||
|
||||
export type ShortUrlEditedAction = PayloadAction<ShortUrl>;
|
||||
|
||||
const initialState: ShortUrlEdition = {
|
||||
saving: false,
|
||||
saved: false,
|
||||
error: false,
|
||||
};
|
||||
|
||||
export const editShortUrl = (apiClientFactory: () => ShlinkApiClient) => createAsyncThunk(
|
||||
`${REDUCER_PREFIX}/editShortUrl`,
|
||||
({ shortCode, domain, data }: EditShortUrl): Promise<ShortUrl> =>
|
||||
apiClientFactory().updateShortUrl(shortCode, domain, data as any) // TODO parse dates
|
||||
,
|
||||
);
|
||||
|
||||
export const shortUrlEditionReducerCreator = (editShortUrlThunk: ReturnType<typeof editShortUrl>) => createSlice({
|
||||
name: REDUCER_PREFIX,
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(editShortUrlThunk.pending, (state) => ({ ...state, saving: true, error: false, saved: false }));
|
||||
builder.addCase(
|
||||
editShortUrlThunk.rejected,
|
||||
(state, { error }) => ({ ...state, saving: false, error: true, saved: false, errorData: parseApiError(error) }),
|
||||
);
|
||||
builder.addCase(
|
||||
editShortUrlThunk.fulfilled,
|
||||
(_, { payload: shortUrl }) => ({ shortUrl, saving: false, error: false, saved: true }),
|
||||
);
|
||||
},
|
||||
});
|
||||
113
shlink-web-component/src/short-urls/reducers/shortUrlsList.ts
Normal file
113
shlink-web-component/src/short-urls/reducers/shortUrlsList.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { assocPath, last, pipe, reject } from 'ramda';
|
||||
import type { ShlinkApiClient, ShlinkShortUrlsListParams, ShlinkShortUrlsResponse } from '../../api-contract';
|
||||
import { createAsyncThunk } from '../../utils/redux';
|
||||
import { createNewVisits } from '../../visits/reducers/visitCreation';
|
||||
import type { ShortUrl } from '../data';
|
||||
import { shortUrlMatches } from '../helpers';
|
||||
import type { createShortUrl } from './shortUrlCreation';
|
||||
import { shortUrlDeleted } from './shortUrlDeletion';
|
||||
import type { editShortUrl } from './shortUrlEdition';
|
||||
|
||||
const REDUCER_PREFIX = 'shlink/shortUrlsList';
|
||||
export const ITEMS_IN_OVERVIEW_PAGE = 5;
|
||||
|
||||
export interface ShortUrlsList {
|
||||
shortUrls?: ShlinkShortUrlsResponse;
|
||||
loading: boolean;
|
||||
error: boolean;
|
||||
}
|
||||
|
||||
const initialState: ShortUrlsList = {
|
||||
loading: true,
|
||||
error: false,
|
||||
};
|
||||
|
||||
export const listShortUrls = (apiClientFactory: () => ShlinkApiClient) => createAsyncThunk(
|
||||
`${REDUCER_PREFIX}/listShortUrls`,
|
||||
(params: ShlinkShortUrlsListParams | void): Promise<ShlinkShortUrlsResponse> => apiClientFactory().listShortUrls(
|
||||
params ?? {},
|
||||
),
|
||||
);
|
||||
|
||||
export const shortUrlsListReducerCreator = (
|
||||
listShortUrlsThunk: ReturnType<typeof listShortUrls>,
|
||||
editShortUrlThunk: ReturnType<typeof editShortUrl>,
|
||||
createShortUrlThunk: ReturnType<typeof createShortUrl>,
|
||||
) => createSlice({
|
||||
name: REDUCER_PREFIX,
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(listShortUrlsThunk.pending, (state) => ({ ...state, loading: true, error: false }));
|
||||
builder.addCase(listShortUrlsThunk.rejected, () => ({ loading: false, error: true }));
|
||||
builder.addCase(
|
||||
listShortUrlsThunk.fulfilled,
|
||||
(_, { payload: shortUrls }) => ({ loading: false, error: false, shortUrls }),
|
||||
);
|
||||
|
||||
builder.addCase(
|
||||
createShortUrlThunk.fulfilled,
|
||||
pipe(
|
||||
// The only place where the list and the creation form coexist is the overview page.
|
||||
// There we can assume we are displaying page 1, and therefore, we can safely prepend the new short URL.
|
||||
// We can also remove the items above the amount that is displayed there.
|
||||
(state, { payload }) => (!state.shortUrls ? state : assocPath(
|
||||
['shortUrls', 'data'],
|
||||
[payload, ...state.shortUrls.data.slice(0, ITEMS_IN_OVERVIEW_PAGE - 1)],
|
||||
state,
|
||||
)),
|
||||
(state: ShortUrlsList) => (!state.shortUrls ? state : assocPath(
|
||||
['shortUrls', 'pagination', 'totalItems'],
|
||||
state.shortUrls.pagination.totalItems + 1,
|
||||
state,
|
||||
)),
|
||||
),
|
||||
);
|
||||
|
||||
builder.addCase(
|
||||
editShortUrlThunk.fulfilled,
|
||||
(state, { payload: editedShortUrl }) => (!state.shortUrls ? state : assocPath(
|
||||
['shortUrls', 'data'],
|
||||
state.shortUrls.data.map((shortUrl) => {
|
||||
const { shortCode, domain } = editedShortUrl;
|
||||
return shortUrlMatches(shortUrl, shortCode, domain) ? editedShortUrl : shortUrl;
|
||||
}),
|
||||
state,
|
||||
)),
|
||||
);
|
||||
|
||||
builder.addCase(
|
||||
shortUrlDeleted,
|
||||
pipe(
|
||||
(state, { payload }) => (!state.shortUrls ? state : assocPath(
|
||||
['shortUrls', 'data'],
|
||||
reject<ShortUrl, ShortUrl[]>((shortUrl) =>
|
||||
shortUrlMatches(shortUrl, payload.shortCode, payload.domain), state.shortUrls.data),
|
||||
state,
|
||||
)),
|
||||
(state) => (!state.shortUrls ? state : assocPath(
|
||||
['shortUrls', 'pagination', 'totalItems'],
|
||||
state.shortUrls.pagination.totalItems - 1,
|
||||
state,
|
||||
)),
|
||||
),
|
||||
);
|
||||
|
||||
builder.addCase(
|
||||
createNewVisits,
|
||||
(state, { payload }) => assocPath(
|
||||
['shortUrls', 'data'],
|
||||
state.shortUrls?.data?.map(
|
||||
// Find the last of the new visit for this short URL, and pick its short URL. It will have an up-to-date amount of visits.
|
||||
(currentShortUrl) => last(
|
||||
payload.createdVisits.filter(
|
||||
({ shortUrl }) => shortUrl && shortUrlMatches(currentShortUrl, shortUrl.shortCode, shortUrl.domain),
|
||||
),
|
||||
)?.shortUrl ?? currentShortUrl,
|
||||
),
|
||||
state,
|
||||
),
|
||||
);
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user