Ensured JSON decodedoes not happen for endpoints returning empty body

This commit is contained in:
Alejandro Celaya 2022-11-19 09:29:29 +01:00
parent bd8ea17c84
commit dd9ee044eb
6 changed files with 53 additions and 51 deletions

View file

@ -79,7 +79,7 @@ export class ShlinkApiClient {
this.performRequest<ShortUrl>(`/short-urls/${shortCode}`, 'GET', { domain });
public readonly deleteShortUrl = async (shortCode: string, domain?: OptionalString): Promise<void> =>
this.performRequest(`/short-urls/${shortCode}`, 'DELETE', { domain })
this.performRequest<void>(`/short-urls/${shortCode}`, 'DELETE', { domain })
.then(() => {});
public readonly updateShortUrl = async (
@ -95,11 +95,11 @@ export class ShlinkApiClient {
.then(({ data, stats }) => ({ tags: data, stats }));
public readonly deleteTags = async (tags: string[]): Promise<{ tags: string[] }> =>
this.performRequest('/tags', 'DELETE', { tags })
this.performRequest<void>('/tags', 'DELETE', { tags })
.then(() => ({ tags }));
public readonly editTag = async (oldName: string, newName: string): Promise<{ oldName: string; newName: string }> =>
this.performRequest('/tags', 'PUT', {}, { oldName, newName })
this.performRequest<void>('/tags', 'PUT', {}, { oldName, newName })
.then(() => ({ oldName, newName }));
public readonly health = async (): Promise<ShlinkHealth> => this.performRequest<ShlinkHealth>('/health', 'GET');

View file

@ -5,13 +5,15 @@ export class HttpClient {
public fetchJson<T>(url: string, options?: RequestInit): Promise<T> {
return this.fetch(url, options).then(async (resp) => {
const parsed = await resp.json();
if (!resp.ok) {
throw parsed;
throw await resp.json();
}
return parsed as T;
try {
return (await resp.json()) as T;
} catch (e) {
return undefined as T;
}
});
}

View file

@ -27,9 +27,10 @@ export const EditTagModal = ({ getColorForKey }: ColorGenerator) => (
const [showColorPicker, toggleColorPicker, , hideColorPicker] = useToggle();
const { editing, error, edited, errorData } = tagEdit;
const saveTag = handleEventPreventingDefault(
async () => editTag({ oldName: tag, newName: newTagName, color })
.then(toggle)
.catch(() => {}),
async () => {
await editTag({ oldName: tag, newName: newTagName, color });
toggle();
},
);
const onClosed = pipe(hideColorPicker, () => edited && tagEdited({ oldName: tag, newName: newTagName, color }));

View file

@ -33,35 +33,34 @@ const initialState: TagEdition = {
export const tagEdited = createAction<EditTag>(`${REDUCER_PREFIX}/tagEdited`);
export const tagEditReducerCreator = (buildShlinkApiClient: ShlinkApiClientBuilder, colorGenerator: ColorGenerator) => {
const editTag = createAsyncThunk(
`${REDUCER_PREFIX}/editTag`,
async ({ oldName, newName, color }: EditTag, { getState }): Promise<EditTag> => {
await buildShlinkApiClient(getState).editTag(oldName, newName);
colorGenerator.setColorForKey(newName, color);
export const editTag = (
buildShlinkApiClient: ShlinkApiClientBuilder,
colorGenerator: ColorGenerator,
) => createAsyncThunk(
`${REDUCER_PREFIX}/editTag`,
async ({ oldName, newName, color }: EditTag, { getState }): Promise<EditTag> => {
await buildShlinkApiClient(getState).editTag(oldName, newName);
colorGenerator.setColorForKey(newName, color);
return { oldName, newName, color };
},
);
return { oldName, newName, color };
},
);
const { reducer } = createSlice({
name: REDUCER_PREFIX,
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(editTag.pending, () => ({ editing: true, edited: false, error: false }));
builder.addCase(
editTag.rejected,
(_, { error }) => ({ editing: false, edited: false, error: true, errorData: parseApiError(error) }),
);
builder.addCase(editTag.fulfilled, (_, { payload }) => ({
...pick(['oldName', 'newName'], payload),
editing: false,
edited: true,
error: false,
}));
},
});
return { reducer, editTag };
};
export const tagEditReducerCreator = (editTagThunk: ReturnType<typeof editTag>) => createSlice({
name: REDUCER_PREFIX,
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(editTagThunk.pending, () => ({ editing: true, edited: false, error: false }));
builder.addCase(
editTagThunk.rejected,
(_, { error }) => ({ editing: false, edited: false, error: true, errorData: parseApiError(error) }),
);
builder.addCase(editTagThunk.fulfilled, (_, { payload }) => ({
...pick(['oldName', 'newName'], payload),
editing: false,
edited: true,
error: false,
}));
},
});

View file

@ -111,15 +111,15 @@ export const tagsListReducerCreator = (
{ ...initialState, stats: payload.stats, tags: payload.tags, filteredTags: payload.tags }
));
builder.addCase(tagDeleted, (state, { payload: tag }) => ({
...state,
tags: rejectTag(state.tags, tag),
filteredTags: rejectTag(state.filteredTags, tag),
builder.addCase(tagDeleted, ({ tags, filteredTags, ...rest }, { payload: tag }) => ({
...rest,
tags: rejectTag(tags, tag),
filteredTags: rejectTag(filteredTags, tag),
}));
builder.addCase(tagEdited, (state, { payload }) => ({
...state,
tags: state.tags.map(renameTag(payload.oldName, payload.newName)).sort(),
filteredTags: state.filteredTags.map(renameTag(payload.oldName, payload.newName)).sort(),
builder.addCase(tagEdited, ({ tags, filteredTags, ...rest }, { payload }) => ({
...rest,
tags: tags.map(renameTag(payload.oldName, payload.newName)).sort(),
filteredTags: filteredTags.map(renameTag(payload.oldName, payload.newName)).sort(),
}));
builder.addCase(createNewVisits, (state, { payload }) => ({
...state,

View file

@ -7,7 +7,7 @@ import { EditTagModal } from '../helpers/EditTagModal';
import { TagsList } from '../TagsList';
import { filterTags, listTags, tagsListReducerCreator } from '../reducers/tagsList';
import { tagDeleted, tagDeleteReducerCreator } from '../reducers/tagDelete';
import { tagEdited, tagEditReducerCreator } from '../reducers/tagEdit';
import { editTag, tagEdited, tagEditReducerCreator } from '../reducers/tagEdit';
import { ConnectDecorator } from '../../container/types';
import { TagsCards } from '../TagsCards';
import { TagsTable } from '../TagsTable';
@ -38,7 +38,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
));
// Reducers
bottle.serviceFactory('tagEditReducerCreator', tagEditReducerCreator, 'buildShlinkApiClient', 'ColorGenerator');
bottle.serviceFactory('tagEditReducerCreator', tagEditReducerCreator, 'editTag');
bottle.serviceFactory('tagEditReducer', prop('reducer'), 'tagEditReducerCreator');
bottle.serviceFactory('tagDeleteReducerCreator', tagDeleteReducerCreator, 'buildShlinkApiClient');
@ -58,7 +58,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('deleteTag', prop('deleteTag'), 'tagDeleteReducerCreator');
bottle.serviceFactory('tagDeleted', () => tagDeleted);
bottle.serviceFactory('editTag', prop('editTag'), 'tagEditReducerCreator');
bottle.serviceFactory('editTag', editTag, 'buildShlinkApiClient', 'ColorGenerator');
bottle.serviceFactory('tagEdited', () => tagEdited);
};