diff --git a/src/api/Api.test.ts b/src/api/Api.test.ts index 9ab6d99..8063296 100644 --- a/src/api/Api.test.ts +++ b/src/api/Api.test.ts @@ -1,4 +1,4 @@ -import { apiClient, apiHeaders } from './Api' +import { apiClient, apiHeaders, ApiResponse, handleApiCall } from './Api' describe('apiClient', () => { beforeEach(() => { @@ -31,3 +31,39 @@ describe('apiClient', () => { expect(fetchMock).toHaveBeenCalledWith('/admin/features', { headers: apiHeaders('token') }) }) }) + +describe('handleApiCall', async () => { + const onSuccess = vi.fn() + const onError = vi.fn() + const onFailure = vi.fn() + + beforeEach(() => { + onSuccess.mockClear() + onError.mockClear() + onFailure.mockClear() + }) + + it('should call onSuccess on success', async () => { + const apiCall = Promise.resolve({ status: 'success' } as ApiResponse) + await handleApiCall(apiCall, { onSuccess, onError, onFailure }) + expect(onSuccess).toHaveBeenCalledTimes(1) + expect(onError).not.toHaveBeenCalled() + expect(onFailure).not.toHaveBeenCalled() + }) + + it('should call onError on error', async () => { + const apiCall = Promise.resolve({ status: 'error' } as ApiResponse) + await handleApiCall(apiCall, { onSuccess, onError, onFailure }) + expect(onSuccess).not.toHaveBeenCalled() + expect(onError).toHaveBeenCalledTimes(1) + expect(onFailure).not.toHaveBeenCalled() + }) + + it('should call onFailure on failure', async () => { + const apiCall = Promise.reject(new Error('Network error')) + await handleApiCall(apiCall, { onSuccess, onError, onFailure }) + expect(onSuccess).not.toHaveBeenCalled() + expect(onError).not.toHaveBeenCalled() + expect(onFailure).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/api/Api.ts b/src/api/Api.ts index 57d4734..a270844 100644 --- a/src/api/Api.ts +++ b/src/api/Api.ts @@ -47,3 +47,24 @@ export function apiHeaders(token: string): HeadersInit { 'Content-Type': 'application/json', } } + +interface ApiCallParams { + onSuccess: (response: ApiResponse) => void + onError: (response: ApiResponse) => void + onFailure: (error: unknown) => void +} + +export async function handleApiCall(apiCall: Promise>, params: ApiCallParams): Promise { + const { onSuccess, onError, onFailure } = params + + try { + const response = await apiCall + if (response.status === 'error') { + onError(response) + } else { + onSuccess(response) + } + } catch (error) { + onFailure(error) + } +} diff --git a/src/components/ticker/BlueskyCard.tsx b/src/components/ticker/BlueskyCard.tsx index eb2e937..cda435e 100644 --- a/src/components/ticker/BlueskyCard.tsx +++ b/src/components/ticker/BlueskyCard.tsx @@ -3,9 +3,11 @@ import { faGear, faPause, faPlay, faTrash } from '@fortawesome/free-solid-svg-ic import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { Box, Button, Card, CardActions, CardContent, Divider, Link, Stack, Typography } from '@mui/material' import { useQueryClient } from '@tanstack/react-query' -import { FC, useCallback, useState } from 'react' +import { FC, useState } from 'react' +import { handleApiCall } from '../../api/Api' import { Ticker, deleteTickerBlueskyApi, putTickerBlueskyApi } from '../../api/Ticker' import useAuth from '../../contexts/useAuth' +import useNotification from '../../contexts/useNotification' import BlueskyModalForm from './BlueskyModalForm' interface Props { @@ -13,6 +15,7 @@ interface Props { } const BlueskyCard: FC = ({ ticker }) => { + const { createNotification } = useNotification() const { token } = useAuth() const [open, setOpen] = useState(false) @@ -20,17 +23,35 @@ const BlueskyCard: FC = ({ ticker }) => { const bluesky = ticker.bluesky - const handleDelete = useCallback(() => { - deleteTickerBlueskyApi(token, ticker).finally(() => { - queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) + const handleDelete = () => { + handleApiCall(deleteTickerBlueskyApi(token, ticker), { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) + createNotification({ content: 'Bluesky integration successfully deleted', severity: 'success' }) + }, + onError: () => { + createNotification({ content: 'Failed to delete Bluesky integration', severity: 'error' }) + }, + onFailure: error => { + createNotification({ content: error as string, severity: 'error' }) + }, }) - }, [token, queryClient, ticker]) + } - const handleToggle = useCallback(() => { - putTickerBlueskyApi(token, { active: !bluesky.active }, ticker).finally(() => { - queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) + const handleToggle = () => { + handleApiCall(putTickerBlueskyApi(token, { active: !bluesky.active }, ticker), { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) + createNotification({ content: `Bluesky integration ${bluesky.active ? 'disabled' : 'enabled'} successfully`, severity: 'success' }) + }, + onError: () => { + createNotification({ content: 'Failed to update Bluesky integration', severity: 'error' }) + }, + onFailure: error => { + createNotification({ content: error as string, severity: 'error' }) + }, }) - }, [bluesky.active, token, queryClient, ticker]) + } const profileLink = ( diff --git a/src/components/ticker/BlueskyForm.tsx b/src/components/ticker/BlueskyForm.tsx index 59824a6..1ea31dd 100644 --- a/src/components/ticker/BlueskyForm.tsx +++ b/src/components/ticker/BlueskyForm.tsx @@ -3,6 +3,7 @@ import Grid from '@mui/material/Grid2' import { useQueryClient } from '@tanstack/react-query' import { FC } from 'react' import { useForm } from 'react-hook-form' +import { handleApiCall } from '../../api/Api' import { Ticker, TickerBlueskyFormData, putTickerBlueskyApi } from '../../api/Ticker' import useAuth from '../../contexts/useAuth' import useNotification from '../../contexts/useNotification' @@ -31,15 +32,19 @@ const BlueskyForm: FC = ({ callback, ticker }) => { const queryClient = useQueryClient() const onSubmit = handleSubmit(data => { - putTickerBlueskyApi(token, data, ticker).then(response => { - if (response.status == 'error') { - setError('root.authenticationFailed', { message: 'Authentication failed' }) - createNotification({ content: 'Bluesky integration failed to update', severity: 'error' }) - } else { + handleApiCall(putTickerBlueskyApi(token, data, ticker), { + onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) createNotification({ content: 'Bluesky integration was successfully updated', severity: 'success' }) callback() - } + }, + onError: () => { + setError('root.authenticationFailed', { message: 'Authentication failed' }) + createNotification({ content: 'Failed to update Bluesky integration', severity: 'error' }) + }, + onFailure: error => { + createNotification({ content: error as string, severity: 'error' }) + }, }) }) diff --git a/src/components/ticker/MastodonCard.tsx b/src/components/ticker/MastodonCard.tsx index d24c34d..e4548d7 100644 --- a/src/components/ticker/MastodonCard.tsx +++ b/src/components/ticker/MastodonCard.tsx @@ -3,9 +3,11 @@ import { faGear, faPause, faPlay, faTrash } from '@fortawesome/free-solid-svg-ic import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { Box, Button, Card, CardActions, CardContent, Divider, Link, Stack, Typography } from '@mui/material' import { useQueryClient } from '@tanstack/react-query' -import { FC, useCallback, useState } from 'react' +import { FC, useState } from 'react' +import { handleApiCall } from '../../api/Api' import { Ticker, deleteTickerMastodonApi, putTickerMastodonApi } from '../../api/Ticker' import useAuth from '../../contexts/useAuth' +import useNotification from '../../contexts/useNotification' import MastodonModalForm from './MastodonModalForm' interface Props { @@ -13,6 +15,7 @@ interface Props { } const MastodonCard: FC = ({ ticker }) => { + const { createNotification } = useNotification() const { token } = useAuth() const [open, setOpen] = useState(false) @@ -20,17 +23,35 @@ const MastodonCard: FC = ({ ticker }) => { const mastodon = ticker.mastodon - const handleDelete = useCallback(() => { - deleteTickerMastodonApi(token, ticker).finally(() => { - queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) + const handleDelete = () => { + handleApiCall(deleteTickerMastodonApi(token, ticker), { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) + createNotification({ content: 'Mastodon integration successfully deleted', severity: 'success' }) + }, + onError: () => { + createNotification({ content: 'Failed to delete Mastodon integration', severity: 'error' }) + }, + onFailure: error => { + createNotification({ content: error as string, severity: 'error' }) + }, }) - }, [token, queryClient, ticker]) + } - const handleToggle = useCallback(() => { - putTickerMastodonApi(token, { active: !mastodon.active }, ticker).finally(() => { - queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) + const handleToggle = () => { + handleApiCall(putTickerMastodonApi(token, { active: !mastodon.active }, ticker), { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) + createNotification({ content: `Mastodon integration ${mastodon.active ? 'disabled' : 'enabled'} successfully`, severity: 'success' }) + }, + onError: () => { + createNotification({ content: 'Failed to update Mastodon integration', severity: 'error' }) + }, + onFailure: error => { + createNotification({ content: error as string, severity: 'error' }) + }, }) - }, [mastodon.active, token, queryClient, ticker]) + } const profileLink = ( diff --git a/src/components/ticker/MastodonForm.tsx b/src/components/ticker/MastodonForm.tsx index 538e85f..0ef6306 100644 --- a/src/components/ticker/MastodonForm.tsx +++ b/src/components/ticker/MastodonForm.tsx @@ -3,6 +3,7 @@ import Grid from '@mui/material/Grid2' import { useQueryClient } from '@tanstack/react-query' import { FC } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' +import { handleApiCall } from '../../api/Api' import { Ticker, TickerMastodonFormData, putTickerMastodonApi } from '../../api/Ticker' import useAuth from '../../contexts/useAuth' import useNotification from '../../contexts/useNotification' @@ -25,10 +26,18 @@ const MastodonForm: FC = ({ callback, ticker }) => { const queryClient = useQueryClient() const onSubmit: SubmitHandler = data => { - putTickerMastodonApi(token, data, ticker).finally(() => { - queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) - createNotification({ content: 'Mastodon integration was successfully updated', severity: 'success' }) - callback() + handleApiCall(putTickerMastodonApi(token, data, ticker), { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) + createNotification({ content: 'Mastodon integration was successfully updated', severity: 'success' }) + callback() + }, + onError: () => { + createNotification({ content: 'Failed to update Mastodon integration', severity: 'error' }) + }, + onFailure: error => { + createNotification({ content: error as string, severity: 'error' }) + }, }) } diff --git a/src/components/ticker/SignalGroupAdminForm.tsx b/src/components/ticker/SignalGroupAdminForm.tsx index d8f2fc3..f0db685 100644 --- a/src/components/ticker/SignalGroupAdminForm.tsx +++ b/src/components/ticker/SignalGroupAdminForm.tsx @@ -4,6 +4,7 @@ import { Alert, FormGroup, InputAdornment, TextField } from '@mui/material' import Grid from '@mui/material/Grid2' import { FC } from 'react' import { useForm } from 'react-hook-form' +import { handleApiCall } from '../../api/Api' import { Ticker, TickerSignalGroupAdminFormData, putTickerSignalGroupAdminApi } from '../../api/Ticker' import useAuth from '../../contexts/useAuth' import useNotification from '../../contexts/useNotification' @@ -26,19 +27,22 @@ const SignalGroupAdminForm: FC = ({ callback, ticker, setSubmitting }) => const onSubmit = handleSubmit(data => { setSubmitting(true) - putTickerSignalGroupAdminApi(token, data, ticker) - .then(response => { - if (response.status == 'error') { - createNotification({ content: 'Failed to add number to Signal group', severity: 'error' }) - setError('number', { message: 'Failed to add number to Signal group' }) - } else { - createNotification({ content: 'Number successfully added to Signal group', severity: 'success' }) - callback() - } - }) - .finally(() => { - setSubmitting(false) - }) + + handleApiCall(putTickerSignalGroupAdminApi(token, data, ticker), { + onSuccess: () => { + createNotification({ content: 'Number successfully added to Signal group', severity: 'success' }) + callback() + }, + onError: () => { + createNotification({ content: 'Failed to add number to Signal group', severity: 'error' }) + setError('number', { message: 'Failed to add number to Signal group' }) + }, + onFailure: error => { + createNotification({ content: error as string, severity: 'error' }) + }, + }) + + setSubmitting(false) }) return ( diff --git a/src/components/ticker/SignalGroupCard.tsx b/src/components/ticker/SignalGroupCard.tsx index 9450e29..0a4a640 100644 --- a/src/components/ticker/SignalGroupCard.tsx +++ b/src/components/ticker/SignalGroupCard.tsx @@ -19,7 +19,8 @@ import { Typography, } from '@mui/material' import { useQueryClient } from '@tanstack/react-query' -import { FC, useCallback, useState } from 'react' +import { FC, useState } from 'react' +import { handleApiCall } from '../../api/Api' import { Ticker, deleteTickerSignalGroupApi, putTickerSignalGroupApi } from '../../api/Ticker' import useAuth from '../../contexts/useAuth' import useNotification from '../../contexts/useNotification' @@ -44,24 +45,41 @@ const SignalGroupCard: FC = ({ ticker }) => { const handleAdd = () => { setSubmittingAdd(true) - putTickerSignalGroupApi(token, { active: true }, ticker) - .finally(() => { + + handleApiCall(putTickerSignalGroupApi(token, { active: true }, ticker), { + onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) createNotification({ content: 'Signal Group enabled successfully', severity: 'success' }) - setSubmittingAdd(false) - }) - .catch(() => { + }, + onError: () => { createNotification({ content: 'Failed to configure Signal group', severity: 'error' }) - }) + }, + onFailure: error => { + createNotification({ content: error as string, severity: 'error' }) + }, + }) + + setSubmittingAdd(false) } - const handleToggle = useCallback(() => { + const handleToggle = () => { setSubmittingToggle(true) - putTickerSignalGroupApi(token, { active: !signalGroup.active }, ticker).finally(() => { - queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) - setSubmittingToggle(false) + + handleApiCall(putTickerSignalGroupApi(token, { active: !signalGroup.active }, ticker), { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) + createNotification({ content: `Signal group ${signalGroup.active ? 'disabled' : 'enabled'} successfully`, severity: 'success' }) + }, + onError: () => { + createNotification({ content: 'Failed to update Signal group', severity: 'error' }) + }, + onFailure: error => { + createNotification({ content: error as string, severity: 'error' }) + }, }) - }, [token, queryClient, signalGroup.active, ticker]) + + setSubmittingToggle(false) + } const handleDelete = () => { setSubmittingDelete(true) diff --git a/src/components/ticker/TelegramCard.tsx b/src/components/ticker/TelegramCard.tsx index 91d1751..abd33e5 100644 --- a/src/components/ticker/TelegramCard.tsx +++ b/src/components/ticker/TelegramCard.tsx @@ -3,7 +3,8 @@ import { faGear, faPause, faPlay, faTrash } from '@fortawesome/free-solid-svg-ic import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { Box, Button, Card, CardActions, CardContent, Divider, Link, Stack, Typography } from '@mui/material' import { useQueryClient } from '@tanstack/react-query' -import { FC, useCallback, useState } from 'react' +import { FC, useState } from 'react' +import { handleApiCall } from '../../api/Api' import { Ticker, deleteTickerTelegramApi, putTickerTelegramApi } from '../../api/Ticker' import useAuth from '../../contexts/useAuth' import useNotification from '../../contexts/useNotification' @@ -21,19 +22,35 @@ const TelegramCard: FC = ({ ticker }) => { const telegram = ticker.telegram - const handleToggle = useCallback(() => { - putTickerTelegramApi(token, { active: !telegram.active }, ticker).finally(() => { - queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) - createNotification({ content: `Telegram integration ${telegram.active ? 'disabled' : 'enabled'} successfully`, severity: 'success' }) + const handleToggle = () => { + handleApiCall(putTickerTelegramApi(token, { active: !telegram.active }, ticker), { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) + createNotification({ content: `Telegram integration ${telegram.active ? 'disabled' : 'enabled'} successfully`, severity: 'success' }) + }, + onError: () => { + createNotification({ content: 'Failed to update Telegram integration', severity: 'error' }) + }, + onFailure: error => { + createNotification({ content: error as string, severity: 'error' }) + }, }) - }, [token, telegram.active, ticker, queryClient, createNotification]) + } - const handleDelete = useCallback(() => { - deleteTickerTelegramApi(token, ticker).finally(() => { - queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) - createNotification({ content: 'Telegram integration successfully deleted', severity: 'success' }) + const handleDelete = () => { + handleApiCall(deleteTickerTelegramApi(token, ticker), { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) + createNotification({ content: 'Telegram integration successfully deleted', severity: 'success' }) + }, + onError: () => { + createNotification({ content: 'Failed to delete Telegram integration', severity: 'error' }) + }, + onFailure: error => { + createNotification({ content: error as string, severity: 'error' }) + }, }) - }, [token, ticker, queryClient, createNotification]) + } const channelLink = ( diff --git a/src/components/ticker/TelegramForm.tsx b/src/components/ticker/TelegramForm.tsx index 3e34e57..580b8d7 100644 --- a/src/components/ticker/TelegramForm.tsx +++ b/src/components/ticker/TelegramForm.tsx @@ -3,6 +3,7 @@ import Grid from '@mui/material/Grid2' import { useQueryClient } from '@tanstack/react-query' import { FC } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' +import { handleApiCall } from '../../api/Api' import { Ticker, putTickerTelegramApi } from '../../api/Ticker' import useAuth from '../../contexts/useAuth' import useNotification from '../../contexts/useNotification' @@ -30,10 +31,18 @@ const TelegramForm: FC = ({ callback, ticker }) => { const queryClient = useQueryClient() const onSubmit: SubmitHandler = data => { - putTickerTelegramApi(token, data, ticker).finally(() => { - queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) - createNotification({ content: 'Telegram integration was successfully updated', severity: 'success' }) - callback() + handleApiCall(putTickerTelegramApi(token, data, ticker), { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) + createNotification({ content: 'Telegram integration was successfully updated', severity: 'success' }) + callback() + }, + onError: () => { + createNotification({ content: 'Failed to update Telegram integration', severity: 'error' }) + }, + onFailure: error => { + createNotification({ content: error as string, severity: 'error' }) + }, }) } diff --git a/src/components/ticker/TickerModalDelete.tsx b/src/components/ticker/TickerModalDelete.tsx index 3f4fc17..cde26a5 100644 --- a/src/components/ticker/TickerModalDelete.tsx +++ b/src/components/ticker/TickerModalDelete.tsx @@ -1,5 +1,6 @@ import { useQueryClient } from '@tanstack/react-query' import { FC, useCallback } from 'react' +import { handleApiCall } from '../../api/Api' import { Ticker, deleteTickerApi } from '../../api/Ticker' import useAuth from '../../contexts/useAuth' import useNotification from '../../contexts/useNotification' @@ -17,9 +18,17 @@ const TickerModalDelete: FC = ({ open, onClose, ticker }) => { const queryClient = useQueryClient() const handleDelete = useCallback(() => { - deleteTickerApi(token, ticker).finally(() => { - queryClient.invalidateQueries({ queryKey: ['tickers'] }) - createNotification({ content: 'Ticker was successfully deleted', severity: 'success' }) + handleApiCall(deleteTickerApi(token, ticker), { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['tickers'] }) + createNotification({ content: 'Ticker was successfully deleted', severity: 'success' }) + }, + onError: () => { + createNotification({ content: 'Failed to delete ticker', severity: 'error' }) + }, + onFailure: error => { + createNotification({ content: error as string, severity: 'error' }) + }, }) }, [token, ticker, queryClient, createNotification]) diff --git a/src/components/ticker/TickerResetModal.tsx b/src/components/ticker/TickerResetModal.tsx index 913a42e..0d37aef 100644 --- a/src/components/ticker/TickerResetModal.tsx +++ b/src/components/ticker/TickerResetModal.tsx @@ -1,5 +1,6 @@ import { useQueryClient } from '@tanstack/react-query' import { FC, useCallback } from 'react' +import { handleApiCall } from '../../api/Api' import { Ticker, putTickerResetApi } from '../../api/Ticker' import useAuth from '../../contexts/useAuth' import useNotification from '../../contexts/useNotification' @@ -17,16 +18,21 @@ const TickerResetModal: FC = ({ onClose, open, ticker }) => { const queryClient = useQueryClient() const handleReset = useCallback(() => { - putTickerResetApi(token, ticker) - .then(() => { + handleApiCall(putTickerResetApi(token, ticker), { + onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['messages', ticker.id] }) queryClient.invalidateQueries({ queryKey: ['tickerUsers', ticker.id] }) queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) - }) - .finally(() => { createNotification({ content: 'Ticker has been successfully reset', severity: 'success' }) onClose() - }) + }, + onError: () => { + createNotification({ content: 'Failed to reset ticker', severity: 'error' }) + }, + onFailure: error => { + createNotification({ content: error as string, severity: 'error' }) + }, + }) }, [token, ticker, queryClient, createNotification, onClose]) return ( diff --git a/src/components/ticker/TickerUserModalDelete.tsx b/src/components/ticker/TickerUserModalDelete.tsx index a60ce86..e8dbcac 100644 --- a/src/components/ticker/TickerUserModalDelete.tsx +++ b/src/components/ticker/TickerUserModalDelete.tsx @@ -1,5 +1,6 @@ import { useQueryClient } from '@tanstack/react-query' import { FC, useCallback } from 'react' +import { handleApiCall } from '../../api/Api' import { Ticker, deleteTickerUserApi } from '../../api/Ticker' import { User } from '../../api/User' import useAuth from '../../contexts/useAuth' @@ -19,10 +20,18 @@ const TickerUserModalDelete: FC = ({ open, onClose, ticker, user }) => { const queryClient = useQueryClient() const handleDelete = useCallback(() => { - deleteTickerUserApi(token, ticker, user).finally(() => { - queryClient.invalidateQueries({ queryKey: ['tickerUsers', ticker.id] }) - createNotification({ content: 'User is successfully deleted from ticker', severity: 'success' }) - onClose() + handleApiCall(deleteTickerUserApi(token, ticker, user), { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['tickerUsers', ticker.id] }) + createNotification({ content: 'User is successfully deleted from ticker', severity: 'success' }) + onClose() + }, + onError: () => { + createNotification({ content: 'Failed to delete user from ticker', severity: 'error' }) + }, + onFailure: error => { + createNotification({ content: error as string, severity: 'error' }) + }, }) }, [token, ticker, user, queryClient, createNotification, onClose]) diff --git a/src/components/ticker/TickerUsersForm.tsx b/src/components/ticker/TickerUsersForm.tsx index acbc15f..98feeda 100644 --- a/src/components/ticker/TickerUsersForm.tsx +++ b/src/components/ticker/TickerUsersForm.tsx @@ -2,6 +2,7 @@ import { Box, Chip, FormControl, InputLabel, MenuItem, OutlinedInput, Select, Se import { useQueryClient } from '@tanstack/react-query' import { FC, useEffect, useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' +import { handleApiCall } from '../../api/Api' import { Ticker, putTickerUsersApi } from '../../api/Ticker' import { User, fetchUsersApi } from '../../api/User' import useAuth from '../../contexts/useAuth' @@ -37,21 +38,25 @@ const TickerUsersForm: FC = ({ onSubmit, ticker, defaultValue }) => { } const updateTickerUsers: SubmitHandler = () => { - putTickerUsersApi(token, ticker, users).then(response => { - if (response.status !== 'success') { - createNotification({ content: 'Failed to update users', severity: 'error' }) - } else { + handleApiCall(putTickerUsersApi(token, ticker, users), { + onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['tickerUsers', ticker.id] }) createNotification({ content: 'Users were successfully updated', severity: 'success' }) onSubmit() - } + }, + onError: () => { + createNotification({ content: 'Failed to update users', severity: 'error' }) + }, + onFailure: error => { + createNotification({ content: error as string, severity: 'error' }) + }, }) } useEffect(() => { - fetchUsersApi(token) - .then(response => response.data?.users) - .then(users => { + handleApiCall(fetchUsersApi(token), { + onSuccess: response => { + const users = response.data?.users if (!users) { return } @@ -61,7 +66,14 @@ const TickerUsersForm: FC = ({ onSubmit, ticker, defaultValue }) => { return !user.isSuperAdmin }) ) - }) + }, + onError: () => { + createNotification({ content: 'Failed to fetch users', severity: 'error' }) + }, + onFailure: error => { + createNotification({ content: error as string, severity: 'error' }) + }, + }) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) diff --git a/src/components/ticker/TickersDropdown.test.tsx b/src/components/ticker/TickersDropdown.test.tsx index f7548b9..409a05e 100644 --- a/src/components/ticker/TickersDropdown.test.tsx +++ b/src/components/ticker/TickersDropdown.test.tsx @@ -1,11 +1,12 @@ -import { render, screen } from '@testing-library/react' -import TickersDropdown from './TickersDropdown' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { Ticker } from '../../api/Ticker' -import { vi } from 'vitest' +import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { AuthProvider } from '../../contexts/AuthContext' import { MemoryRouter } from 'react-router' +import { vi } from 'vitest' +import { Ticker } from '../../api/Ticker' +import { AuthProvider } from '../../contexts/AuthContext' +import { NotificationProvider } from '../../contexts/NotificationContext' +import TickersDropdown from './TickersDropdown' describe('TickersDropdown', () => { beforeEach(() => { @@ -24,7 +25,9 @@ describe('TickersDropdown', () => { - + + + diff --git a/src/components/ticker/TickersDropdown.tsx b/src/components/ticker/TickersDropdown.tsx index fbbe787..f6dc91b 100644 --- a/src/components/ticker/TickersDropdown.tsx +++ b/src/components/ticker/TickersDropdown.tsx @@ -1,7 +1,9 @@ import { Box, Chip, FormControl, InputLabel, MenuItem, OutlinedInput, Select, SelectChangeEvent, SxProps, useTheme } from '@mui/material' import { FC, useEffect, useState } from 'react' +import { handleApiCall } from '../../api/Api' import { GetTickersQueryParams, Ticker, fetchTickersApi } from '../../api/Ticker' import useAuth from '../../contexts/useAuth' +import useNotification from '../../contexts/useNotification' interface Props { name: string @@ -11,6 +13,7 @@ interface Props { } const TickersDropdown: FC = ({ name, defaultValue, onChange, sx }) => { + const { createNotification } = useNotification() const [options, setOptions] = useState>([]) const [tickers, setTickers] = useState>(defaultValue) const { token } = useAuth() @@ -26,13 +29,19 @@ const TickersDropdown: FC = ({ name, defaultValue, onChange, sx }) => { } useEffect(() => { - const params = {} as GetTickersQueryParams - fetchTickersApi(token, params) - .then(response => response.data?.tickers) - .then(tickers => { + handleApiCall(fetchTickersApi(token, {} as GetTickersQueryParams), { + onSuccess: response => { + const tickers = response.data?.tickers if (!tickers) return setOptions(tickers) - }) + }, + onError: () => { + createNotification({ content: 'Failed to fetch tickers', severity: 'error' }) + }, + onFailure: error => { + createNotification({ content: error as string, severity: 'error' }) + }, + }) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) diff --git a/src/components/ticker/WebsiteForm.tsx b/src/components/ticker/WebsiteForm.tsx index 6ab4a6a..8b5ea29 100644 --- a/src/components/ticker/WebsiteForm.tsx +++ b/src/components/ticker/WebsiteForm.tsx @@ -4,6 +4,7 @@ import Grid from '@mui/material/Grid2' import { useQueryClient } from '@tanstack/react-query' import { FC } from 'react' import { SubmitHandler, useFieldArray, useForm } from 'react-hook-form' +import { handleApiCall } from '../../api/Api' import { putTickerWebsitesApi, Ticker, TickerWebsite } from '../../api/Ticker' import useAuth from '../../contexts/useAuth' import useNotification from '../../contexts/useNotification' @@ -37,10 +38,18 @@ const WebsiteForm: FC = ({ callback, ticker }) => { const queryClient = useQueryClient() const onSubmit: SubmitHandler = data => { - putTickerWebsitesApi(token, ticker, data.websites).finally(() => { - queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) - createNotification({ content: 'Websites were successfully updated', severity: 'success' }) - callback() + handleApiCall(putTickerWebsitesApi(token, ticker, data.websites), { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) + createNotification({ content: 'Websites were successfully updated', severity: 'success' }) + callback() + }, + onError: () => { + createNotification({ content: 'Failed to update websites', severity: 'error' }) + }, + onFailure: error => { + createNotification({ content: error as string, severity: 'error' }) + }, }) } diff --git a/src/components/ticker/form/TickerForm.tsx b/src/components/ticker/form/TickerForm.tsx index f6263b9..3e1cf1b 100644 --- a/src/components/ticker/form/TickerForm.tsx +++ b/src/components/ticker/form/TickerForm.tsx @@ -4,6 +4,7 @@ import { useQueryClient } from '@tanstack/react-query' import React, { FC, useCallback, useEffect } from 'react' import { FormProvider, SubmitHandler, useForm } from 'react-hook-form' import { MapContainer, Marker, TileLayer } from 'react-leaflet' +import { handleApiCall } from '../../../api/Api' import { postTickerApi, putTickerApi, Ticker, TickerFormData } from '../../../api/Ticker' import useAuth from '../../../contexts/useAuth' import useNotification from '../../../contexts/useNotification' @@ -78,22 +79,25 @@ const TickerForm: FC = ({ callback, id, ticker, setSubmitting }) => { const onSubmit: SubmitHandler = data => { setSubmitting(true) - if (ticker) { - putTickerApi(token, data, ticker.id).finally(() => { - queryClient.invalidateQueries({ queryKey: ['tickers'] }) - queryClient.invalidateQueries({ queryKey: ['ticker', ticker.id] }) - setSubmitting(false) - createNotification({ content: 'Ticker was successfully updated', severity: 'success' }) - callback() - }) - } else { - postTickerApi(token, data).finally(() => { + + const apiCall = ticker ? putTickerApi(token, data, ticker.id) : postTickerApi(token, data) + + handleApiCall(apiCall, { + onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['tickers'] }) - setSubmitting(false) - createNotification({ content: 'Ticker was successfully created', severity: 'success' }) + queryClient.invalidateQueries({ queryKey: ['ticker', ticker?.id] }) + createNotification({ content: `Ticker was successfully ${ticker ? 'updated' : 'created'}`, severity: 'success' }) callback() - }) - } + }, + onError: () => { + createNotification({ content: `Failed to ${ticker ? 'update' : 'create'} ticker`, severity: 'error' }) + }, + onFailure: error => { + createNotification({ content: error as string, severity: 'error' }) + }, + }) + + setSubmitting(false) } useEffect(() => {