Create src folder for shlink-web-component

This commit is contained in:
Alejandro Celaya
2023-08-02 08:23:48 +02:00
parent b7d57a53f2
commit c48facc863
294 changed files with 347 additions and 347 deletions

View File

@@ -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,
};
};

View File

@@ -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 };
};

View File

@@ -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 };
};

View File

@@ -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 }),
);
},
});

View 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,
),
);
},
});