Fixed DeleteShortUrlModal being removed from the DOM before CSS transition finished

This commit is contained in:
Alejandro Celaya 2022-11-22 19:39:07 +01:00
parent bc2c945fee
commit d21758c410
6 changed files with 36 additions and 23 deletions

View file

@ -10,23 +10,30 @@ import { ShlinkApiError } from '../../api/ShlinkApiError';
interface DeleteShortUrlModalConnectProps extends ShortUrlModalProps { interface DeleteShortUrlModalConnectProps extends ShortUrlModalProps {
shortUrlDeletion: ShortUrlDeletion; shortUrlDeletion: ShortUrlDeletion;
deleteShortUrl: (shortUrl: ShortUrlIdentifier) => void; deleteShortUrl: (shortUrl: ShortUrlIdentifier) => Promise<void>;
shortUrlDeleted: (shortUrl: ShortUrlIdentifier) => void;
resetDeleteShortUrl: () => void; resetDeleteShortUrl: () => void;
} }
export const DeleteShortUrlModal = ( export const DeleteShortUrlModal = ({
{ shortUrl, toggle, isOpen, shortUrlDeletion, resetDeleteShortUrl, deleteShortUrl }: DeleteShortUrlModalConnectProps, shortUrl,
) => { toggle,
isOpen,
shortUrlDeletion,
resetDeleteShortUrl,
deleteShortUrl,
shortUrlDeleted,
}: DeleteShortUrlModalConnectProps) => {
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
useEffect(() => resetDeleteShortUrl, []); useEffect(() => resetDeleteShortUrl, []);
const { loading, error, errorData } = shortUrlDeletion; const { loading, error, deleted, errorData } = shortUrlDeletion;
const close = pipe(resetDeleteShortUrl, toggle); const close = pipe(resetDeleteShortUrl, toggle);
const handleDeleteUrl = handleEventPreventingDefault(() => deleteShortUrl(shortUrl)); const handleDeleteUrl = handleEventPreventingDefault(() => deleteShortUrl(shortUrl).then(toggle));
return ( return (
<Modal isOpen={isOpen} toggle={close} centered> <Modal isOpen={isOpen} toggle={close} centered onClosed={() => deleted && shortUrlDeleted(shortUrl)}>
<form onSubmit={handleDeleteUrl}> <form onSubmit={handleDeleteUrl}>
<ModalHeader toggle={close}> <ModalHeader toggle={close}>
<span className="text-danger">Delete short URL</span> <span className="text-danger">Delete short URL</span>

View file

@ -1,9 +1,9 @@
import { createSlice } from '@reduxjs/toolkit'; import { createAction, createSlice } from '@reduxjs/toolkit';
import { createAsyncThunk } from '../../utils/helpers/redux'; import { createAsyncThunk } from '../../utils/helpers/redux';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import { parseApiError } from '../../api/utils'; import { parseApiError } from '../../api/utils';
import { ProblemDetailsError } from '../../api/types/errors'; import { ProblemDetailsError } from '../../api/types/errors';
import { ShortUrlIdentifier } from '../data'; import { ShortUrl, ShortUrlIdentifier } from '../data';
const REDUCER_PREFIX = 'shlink/shortUrlDeletion'; const REDUCER_PREFIX = 'shlink/shortUrlDeletion';
@ -31,6 +31,8 @@ export const deleteShortUrl = (buildShlinkApiClient: ShlinkApiClientBuilder) =>
}, },
); );
export const shortUrlDeleted = createAction<ShortUrl>(`${REDUCER_PREFIX}/shortUrlDeleted`);
export const shortUrlDeletionReducerCreator = (deleteShortUrlThunk: ReturnType<typeof deleteShortUrl>) => { export const shortUrlDeletionReducerCreator = (deleteShortUrlThunk: ReturnType<typeof deleteShortUrl>) => {
const { actions, reducer } = createSlice({ const { actions, reducer } = createSlice({
name: REDUCER_PREFIX, name: REDUCER_PREFIX,

View file

@ -5,7 +5,7 @@ import { createNewVisits } from '../../visits/reducers/visitCreation';
import { createAsyncThunk } from '../../utils/helpers/redux'; import { createAsyncThunk } from '../../utils/helpers/redux';
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder'; import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
import { ShlinkShortUrlsListParams, ShlinkShortUrlsResponse } from '../../api/types'; import { ShlinkShortUrlsListParams, ShlinkShortUrlsResponse } from '../../api/types';
import { deleteShortUrl } from './shortUrlDeletion'; import { shortUrlDeleted } from './shortUrlDeletion';
import { createShortUrl } from './shortUrlCreation'; import { createShortUrl } from './shortUrlCreation';
import { editShortUrl } from './shortUrlEdition'; import { editShortUrl } from './shortUrlEdition';
import { ShortUrl } from '../data'; import { ShortUrl } from '../data';
@ -36,7 +36,6 @@ export const shortUrlsListReducerCreator = (
listShortUrlsThunk: ReturnType<typeof listShortUrls>, listShortUrlsThunk: ReturnType<typeof listShortUrls>,
editShortUrlThunk: ReturnType<typeof editShortUrl>, editShortUrlThunk: ReturnType<typeof editShortUrl>,
createShortUrlThunk: ReturnType<typeof createShortUrl>, createShortUrlThunk: ReturnType<typeof createShortUrl>,
deleteShortUrlThunk: ReturnType<typeof deleteShortUrl>,
) => createSlice({ ) => createSlice({
name: REDUCER_PREFIX, name: REDUCER_PREFIX,
initialState, initialState,
@ -81,7 +80,7 @@ export const shortUrlsListReducerCreator = (
); );
builder.addCase( builder.addCase(
deleteShortUrlThunk.fulfilled, shortUrlDeleted,
pipe( pipe(
(state, { payload }) => (!state.shortUrls ? state : assocPath( (state, { payload }) => (!state.shortUrls ? state : assocPath(
['shortUrls', 'data'], ['shortUrls', 'data'],

View file

@ -9,7 +9,7 @@ import { DeleteShortUrlModal } from '../helpers/DeleteShortUrlModal';
import { CreateShortUrlResult } from '../helpers/CreateShortUrlResult'; import { CreateShortUrlResult } from '../helpers/CreateShortUrlResult';
import { listShortUrls, shortUrlsListReducerCreator } from '../reducers/shortUrlsList'; import { listShortUrls, shortUrlsListReducerCreator } from '../reducers/shortUrlsList';
import { shortUrlCreationReducerCreator, createShortUrl } from '../reducers/shortUrlCreation'; import { shortUrlCreationReducerCreator, createShortUrl } from '../reducers/shortUrlCreation';
import { shortUrlDeletionReducerCreator, deleteShortUrl } from '../reducers/shortUrlDeletion'; import { shortUrlDeletionReducerCreator, deleteShortUrl, shortUrlDeleted } from '../reducers/shortUrlDeletion';
import { editShortUrl, shortUrlEditionReducerCreator } from '../reducers/shortUrlEdition'; import { editShortUrl, shortUrlEditionReducerCreator } from '../reducers/shortUrlEdition';
import { shortUrlDetailReducerCreator } from '../reducers/shortUrlDetail'; import { shortUrlDetailReducerCreator } from '../reducers/shortUrlDetail';
import { ConnectDecorator } from '../../container/types'; import { ConnectDecorator } from '../../container/types';
@ -46,7 +46,10 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
)); ));
bottle.serviceFactory('DeleteShortUrlModal', () => DeleteShortUrlModal); bottle.serviceFactory('DeleteShortUrlModal', () => DeleteShortUrlModal);
bottle.decorator('DeleteShortUrlModal', connect(['shortUrlDeletion'], ['deleteShortUrl', 'resetDeleteShortUrl'])); bottle.decorator('DeleteShortUrlModal', connect(
['shortUrlDeletion'],
['deleteShortUrl', 'shortUrlDeleted', 'resetDeleteShortUrl'],
));
bottle.serviceFactory('QrCodeModal', QrCodeModal, 'ImageDownloader'); bottle.serviceFactory('QrCodeModal', QrCodeModal, 'ImageDownloader');
bottle.decorator('QrCodeModal', connect(['selectedServer'])); bottle.decorator('QrCodeModal', connect(['selectedServer']));
@ -63,7 +66,6 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
'listShortUrls', 'listShortUrls',
'editShortUrl', 'editShortUrl',
'createShortUrl', 'createShortUrl',
'deleteShortUrl',
); );
bottle.serviceFactory('shortUrlsListReducer', prop('reducer'), 'shortUrlsListReducerCreator'); bottle.serviceFactory('shortUrlsListReducer', prop('reducer'), 'shortUrlsListReducerCreator');
@ -87,6 +89,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
bottle.serviceFactory('deleteShortUrl', deleteShortUrl, 'buildShlinkApiClient'); bottle.serviceFactory('deleteShortUrl', deleteShortUrl, 'buildShlinkApiClient');
bottle.serviceFactory('resetDeleteShortUrl', prop('resetDeleteShortUrl'), 'shortUrlDeletionReducerCreator'); bottle.serviceFactory('resetDeleteShortUrl', prop('resetDeleteShortUrl'), 'shortUrlDeletionReducerCreator');
bottle.serviceFactory('shortUrlDeleted', () => shortUrlDeleted);
bottle.serviceFactory('getShortUrlDetail', prop('getShortUrlDetail'), 'shortUrlDetailReducerCreator'); bottle.serviceFactory('getShortUrlDetail', prop('getShortUrlDetail'), 'shortUrlDetailReducerCreator');

View file

@ -1,4 +1,4 @@
import { screen } from '@testing-library/react'; import { screen, waitFor } from '@testing-library/react';
import { Mock } from 'ts-mockery'; import { Mock } from 'ts-mockery';
import { DeleteShortUrlModal } from '../../../src/short-urls/helpers/DeleteShortUrlModal'; import { DeleteShortUrlModal } from '../../../src/short-urls/helpers/DeleteShortUrlModal';
import { ShortUrl } from '../../../src/short-urls/data'; import { ShortUrl } from '../../../src/short-urls/data';
@ -12,15 +12,17 @@ describe('<DeleteShortUrlModal />', () => {
shortCode: 'abc123', shortCode: 'abc123',
longUrl: 'https://long-domain.com/foo/bar', longUrl: 'https://long-domain.com/foo/bar',
}); });
const deleteShortUrl = jest.fn(); const deleteShortUrl = jest.fn().mockResolvedValue(undefined);
const toggle = jest.fn();
const setUp = (shortUrlDeletion: Partial<ShortUrlDeletion>) => renderWithEvents( const setUp = (shortUrlDeletion: Partial<ShortUrlDeletion>) => renderWithEvents(
<DeleteShortUrlModal <DeleteShortUrlModal
isOpen isOpen
shortUrl={shortUrl} shortUrl={shortUrl}
shortUrlDeletion={Mock.of<ShortUrlDeletion>(shortUrlDeletion)} shortUrlDeletion={Mock.of<ShortUrlDeletion>(shortUrlDeletion)}
deleteShortUrl={deleteShortUrl} deleteShortUrl={deleteShortUrl}
toggle={() => {}} shortUrlDeleted={jest.fn()}
resetDeleteShortUrl={() => {}} toggle={toggle}
resetDeleteShortUrl={jest.fn()}
/>, />,
); );
@ -81,5 +83,6 @@ describe('<DeleteShortUrlModal />', () => {
await user.type(screen.getByPlaceholderText(/^Insert the short code/), shortCode); await user.type(screen.getByPlaceholderText(/^Insert the short code/), shortCode);
await user.click(screen.getByRole('button', { name: 'Delete' })); await user.click(screen.getByRole('button', { name: 'Delete' }));
expect(deleteShortUrl).toHaveBeenCalledTimes(1); expect(deleteShortUrl).toHaveBeenCalledTimes(1);
await waitFor(() => expect(toggle).toHaveBeenCalledTimes(1));
}); });
}); });

View file

@ -3,7 +3,7 @@ import {
listShortUrls as listShortUrlsCreator, listShortUrls as listShortUrlsCreator,
shortUrlsListReducerCreator, shortUrlsListReducerCreator,
} from '../../../src/short-urls/reducers/shortUrlsList'; } from '../../../src/short-urls/reducers/shortUrlsList';
import { deleteShortUrl as deleteShortUrlCreator } from '../../../src/short-urls/reducers/shortUrlDeletion'; import { shortUrlDeleted } from '../../../src/short-urls/reducers/shortUrlDeletion';
import { ShlinkPaginator, ShlinkShortUrlsResponse } from '../../../src/api/types'; import { ShlinkPaginator, ShlinkShortUrlsResponse } from '../../../src/api/types';
import { createShortUrl as createShortUrlCreator } from '../../../src/short-urls/reducers/shortUrlCreation'; import { createShortUrl as createShortUrlCreator } from '../../../src/short-urls/reducers/shortUrlCreation';
import { editShortUrl as editShortUrlCreator } from '../../../src/short-urls/reducers/shortUrlEdition'; import { editShortUrl as editShortUrlCreator } from '../../../src/short-urls/reducers/shortUrlEdition';
@ -18,8 +18,7 @@ describe('shortUrlsListReducer', () => {
const listShortUrls = listShortUrlsCreator(buildShlinkApiClient); const listShortUrls = listShortUrlsCreator(buildShlinkApiClient);
const editShortUrl = editShortUrlCreator(buildShlinkApiClient); const editShortUrl = editShortUrlCreator(buildShlinkApiClient);
const createShortUrl = createShortUrlCreator(buildShlinkApiClient); const createShortUrl = createShortUrlCreator(buildShlinkApiClient);
const deleteShortUrl = deleteShortUrlCreator(buildShlinkApiClient); const { reducer } = shortUrlsListReducerCreator(listShortUrls, editShortUrl, createShortUrl);
const { reducer } = shortUrlsListReducerCreator(listShortUrls, editShortUrl, createShortUrl, deleteShortUrl);
afterEach(jest.clearAllMocks); afterEach(jest.clearAllMocks);
@ -59,7 +58,7 @@ describe('shortUrlsListReducer', () => {
error: false, error: false,
}; };
expect(reducer(state, { type: deleteShortUrl.fulfilled.toString(), payload: { shortCode } })).toEqual({ expect(reducer(state, { type: shortUrlDeleted.toString(), payload: { shortCode } })).toEqual({
shortUrls: { shortUrls: {
data: [{ shortCode, domain: 'example.com' }, { shortCode: 'foo' }], data: [{ shortCode, domain: 'example.com' }, { shortCode: 'foo' }],
pagination: { totalItems: 9 }, pagination: { totalItems: 9 },