mirror of
https://github.com/koel/koel
synced 2024-11-10 14:44:13 +00:00
feat: improve error handling by using a composable
This commit is contained in:
parent
6a0106e352
commit
9fb80a04fc
55 changed files with 299 additions and 278 deletions
|
@ -30,7 +30,7 @@
|
|||
<AcceptInvitation v-if="layout === 'invitation'" />
|
||||
<ResetPasswordForm v-if="layout === 'reset-password'" />
|
||||
|
||||
<AppInitializer v-if="initializing" @success="onInitSuccess" @error="onInitError" />
|
||||
<AppInitializer v-if="authenticated" @success="onInitSuccess" @error="onInitError" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -79,10 +79,10 @@ const layout = ref<'main' | 'auth' | 'invitation' | 'reset-password'>()
|
|||
const { isCurrentScreen, getCurrentScreen, resolveRoute } = useRouter()
|
||||
const online = useOnline()
|
||||
|
||||
const initializing = ref(false)
|
||||
const authenticated = ref(false)
|
||||
const initialized = ref(false)
|
||||
|
||||
const triggerAppInitialization = () => (initializing.value = true)
|
||||
const triggerAppInitialization = () => (authenticated.value = true)
|
||||
|
||||
const onUserLoggedIn = () => {
|
||||
layout.value = 'main'
|
||||
|
@ -90,7 +90,7 @@ const onUserLoggedIn = () => {
|
|||
}
|
||||
|
||||
const onInitSuccess = async () => {
|
||||
initializing.value = false
|
||||
authenticated.value = false
|
||||
initialized.value = true
|
||||
|
||||
// call resolveRoute() after init() so that the onResolve hooks can use the stores
|
||||
|
@ -99,7 +99,7 @@ const onInitSuccess = async () => {
|
|||
}
|
||||
|
||||
const onInitError = () => {
|
||||
initializing.value = false
|
||||
authenticated.value = false
|
||||
layout.value = 'auth'
|
||||
}
|
||||
|
||||
|
|
|
@ -3,8 +3,9 @@ import { expect, it } from 'vitest'
|
|||
import { downloadService, playbackService } from '@/services'
|
||||
import factory from '@/__tests__/factory'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import AlbumCard from './AlbumCard.vue'
|
||||
import { commonStore, songStore } from '@/stores'
|
||||
import { eventBus } from '@/utils'
|
||||
import AlbumCard from './AlbumCard.vue'
|
||||
|
||||
let album: Album
|
||||
|
||||
|
@ -60,5 +61,13 @@ new class extends UnitTestCase {
|
|||
expect(fetchMock).toHaveBeenCalledWith(album)
|
||||
expect(shuffleMock).toHaveBeenCalledWith(songs, true)
|
||||
})
|
||||
|
||||
it('requests context menu', async () => {
|
||||
this.renderComponent()
|
||||
const emitMock = this.mock(eventBus, 'emit')
|
||||
await this.trigger(screen.getByTestId('artist-album-card'), 'contextMenu')
|
||||
|
||||
expect(emitMock).toHaveBeenCalledWith('ALBUM_CONTEXT_MENU_REQUESTED', expect.any(MouseEvent), album)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { downloadService, playbackService } from '@/services'
|
|||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { commonStore, songStore } from '@/stores'
|
||||
import ArtistCard from './ArtistCard.vue'
|
||||
import { eventBus } from '@/utils'
|
||||
|
||||
let artist: Artist
|
||||
|
||||
|
@ -62,5 +63,13 @@ new class extends UnitTestCase {
|
|||
expect(fetchMock).toHaveBeenCalledWith(artist)
|
||||
expect(playMock).toHaveBeenCalledWith(songs, true)
|
||||
})
|
||||
|
||||
it('requests context menu', async () => {
|
||||
this.renderComponent()
|
||||
const emitMock = this.mock(eventBus, 'emit')
|
||||
await this.trigger(screen.getByTestId('artist-album-card'), 'contextMenu')
|
||||
|
||||
expect(emitMock).toHaveBeenCalledWith('ARTIST_CONTEXT_MENU_REQUESTED', expect.any(MouseEvent), artist)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,14 +24,12 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { authService } from '@/services'
|
||||
import { useMessageToaster } from '@/composables'
|
||||
import { useErrorHandler, useMessageToaster } from '@/composables'
|
||||
|
||||
import Btn from '@/components/ui/form/Btn.vue'
|
||||
import TextInput from '@/components/ui/form/TextInput.vue'
|
||||
import FormRow from '@/components/ui/form/FormRow.vue'
|
||||
|
||||
const { toastSuccess, toastError } = useMessageToaster()
|
||||
|
||||
const emit = defineEmits<{ (e: 'cancel'): void }>()
|
||||
const email = ref('')
|
||||
const loading = ref(false)
|
||||
|
@ -45,13 +43,9 @@ const requestResetPasswordLink = async () => {
|
|||
try {
|
||||
loading.value = true
|
||||
await authService.requestResetPasswordLink(email.value)
|
||||
toastSuccess('Password reset link sent. Please check your email.')
|
||||
} catch (err: any) {
|
||||
if (err.response.status === 404) {
|
||||
toastError('No user with this email address found.')
|
||||
} else {
|
||||
toastError(err.response.data?.message || 'An unknown error occurred.')
|
||||
}
|
||||
useMessageToaster().toastSuccess('Password reset link sent. Please check your email.')
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler().handleHttpError(error, { 404: 'No user with this email address found.' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
|
|
@ -77,8 +77,9 @@ const login = async () => {
|
|||
password.value = ''
|
||||
|
||||
emit('loggedin')
|
||||
} catch (err) {
|
||||
} catch (error: unknown) {
|
||||
failed.value = true
|
||||
logger.error(error)
|
||||
window.setTimeout(() => (failed.value = false), 2000)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
import { computed, ref } from 'vue'
|
||||
import { authService } from '@/services'
|
||||
import { base64Decode } from '@/utils'
|
||||
import { useMessageToaster, useRouter } from '@/composables'
|
||||
import { useErrorHandler, useMessageToaster, useRouter } from '@/composables'
|
||||
|
||||
import PasswordField from '@/components/ui/form/PasswordField.vue'
|
||||
import Btn from '@/components/ui/form/Btn.vue'
|
||||
|
@ -38,7 +38,7 @@ const validPayload = computed(() => email.value && token.value)
|
|||
|
||||
try {
|
||||
[email.value, token.value] = base64Decode(decodeURIComponent(getRouteParam('payload')!)).split('|')
|
||||
} catch (err) {
|
||||
} catch (error: unknown) {
|
||||
toastError('Invalid reset password link.')
|
||||
}
|
||||
|
||||
|
@ -49,8 +49,8 @@ const submit = async () => {
|
|||
toastSuccess('Password set.')
|
||||
await authService.login(email.value, password.value)
|
||||
setTimeout(() => go('/', true))
|
||||
} catch (err: any) {
|
||||
toastError(err.response.data?.message || 'Failed to set new password. Please try again.')
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler().handleHttpError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ const loginWithGoogle = async () => {
|
|||
try {
|
||||
window.onmessage = (msg: MessageEvent) => emit('success', msg.data)
|
||||
openPopup('/auth/google/redirect', 'Google Login', 768, 640, window)
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
emit('error', error)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,16 +44,15 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { invitationService } from '@/services'
|
||||
import { useDialogBox, useRouter } from '@/composables'
|
||||
import { parseValidationError } from '@/utils'
|
||||
import { useErrorHandler, useRouter } from '@/composables'
|
||||
|
||||
import Btn from '@/components/ui/form/Btn.vue'
|
||||
import PasswordField from '@/components/ui/form/PasswordField.vue'
|
||||
import TextInput from '@/components/ui/form/TextInput.vue'
|
||||
import FormRow from '@/components/ui/form/FormRow.vue'
|
||||
|
||||
const { showErrorDialog } = useDialogBox()
|
||||
const { getRouteParam } = useRouter()
|
||||
const { handleHttpError } = useErrorHandler('dialog')
|
||||
|
||||
const name = ref('')
|
||||
const password = ref('')
|
||||
|
@ -68,9 +67,8 @@ const submit = async () => {
|
|||
loading.value = true
|
||||
await invitationService.accept(token, name.value, password.value)
|
||||
window.location.href = '/'
|
||||
} catch (err: any) {
|
||||
const msg = err.response.status === 422 ? parseValidationError(err.response.data)[0] : 'Unknown error.'
|
||||
showErrorDialog(msg, 'Error')
|
||||
} catch (error: unknown) {
|
||||
handleHttpError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
@ -79,14 +77,8 @@ const submit = async () => {
|
|||
onMounted(async () => {
|
||||
try {
|
||||
userProspect.value = await invitationService.getUserProspect(token)
|
||||
} catch (err: any) {
|
||||
if (err.response?.status === 404) {
|
||||
validToken.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const msg = err.response?.status === 422 ? parseValidationError(err.response?.data)[0] : 'Unknown error.'
|
||||
showErrorDialog(msg, 'Error')
|
||||
} catch (error: unknown) {
|
||||
handleHttpError(error, { 404: () => (validToken.value = false) })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -15,13 +15,13 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { plusService } from '@/services'
|
||||
import { forceReloadWindow, logger } from '@/utils'
|
||||
import { useDialogBox } from '@/composables'
|
||||
import { forceReloadWindow } from '@/utils'
|
||||
import { useDialogBox, useErrorHandler } from '@/composables'
|
||||
|
||||
import Btn from '@/components/ui/form/Btn.vue'
|
||||
import TextInput from '@/components/ui/form/TextInput.vue'
|
||||
|
||||
const { showSuccessDialog, showErrorDialog } = useDialogBox()
|
||||
const { showSuccessDialog } = useDialogBox()
|
||||
const licenseKey = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
|
@ -31,9 +31,8 @@ const validateLicenseKey = async () => {
|
|||
await plusService.activateLicense(licenseKey.value)
|
||||
await showSuccessDialog('Thanks for purchasing Koel Plus! Koel will now refresh to activate the changes.')
|
||||
forceReloadWindow()
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
await showErrorDialog('Failed to activate Koel Plus. Please try again.')
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler('dialog').handleHttpError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
|
|
@ -72,8 +72,8 @@
|
|||
import isMobile from 'ismobilejs'
|
||||
import { defineAsyncComponent, onMounted, ref, watch } from 'vue'
|
||||
import { albumStore, artistStore, preferenceStore } from '@/stores'
|
||||
import { useThirdPartyServices } from '@/composables'
|
||||
import { logger, requireInjection } from '@/utils'
|
||||
import { useErrorHandler, useThirdPartyServices } from '@/composables'
|
||||
import { requireInjection } from '@/utils'
|
||||
import { CurrentSongKey } from '@/symbols'
|
||||
|
||||
import ProfileAvatar from '@/components/ui/ProfileAvatar.vue'
|
||||
|
@ -103,8 +103,8 @@ const fetchSongInfo = async (_song: Song) => {
|
|||
try {
|
||||
artist.value = await artistStore.resolve(_song.artist_id)
|
||||
album.value = await albumStore.resolve(_song.album_id)
|
||||
} catch (error) {
|
||||
logger.log('Failed to fetch media information', error)
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler().handleHttpError(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
import { ref } from 'vue'
|
||||
import { playlistFolderStore } from '@/stores'
|
||||
import { logger } from '@/utils'
|
||||
import { useDialogBox, useMessageToaster, useOverlay } from '@/composables'
|
||||
import { useDialogBox, useErrorHandler, useMessageToaster, useOverlay } from '@/composables'
|
||||
|
||||
import Btn from '@/components/ui/form/Btn.vue'
|
||||
import TextInput from '@/components/ui/form/TextInput.vue'
|
||||
|
@ -35,7 +35,7 @@ import FormRow from '@/components/ui/form/FormRow.vue'
|
|||
|
||||
const { showOverlay, hideOverlay } = useOverlay()
|
||||
const { toastSuccess } = useMessageToaster()
|
||||
const { showErrorDialog, showConfirmDialog } = useDialogBox()
|
||||
const { showConfirmDialog } = useDialogBox()
|
||||
|
||||
const name = ref('')
|
||||
|
||||
|
@ -49,8 +49,8 @@ const submit = async () => {
|
|||
const folder = await playlistFolderStore.store(name.value)
|
||||
close()
|
||||
toastSuccess(`Playlist folder "${folder.name}" created.`)
|
||||
} catch (error) {
|
||||
showErrorDialog('Something went wrong. Please try again.', 'Error')
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler('dialog').handleHttpError(error)
|
||||
logger.error(error)
|
||||
} finally {
|
||||
hideOverlay()
|
||||
|
|
|
@ -35,8 +35,8 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref, toRef } from 'vue'
|
||||
import { playlistFolderStore, playlistStore } from '@/stores'
|
||||
import { logger, pluralize } from '@/utils'
|
||||
import { useDialogBox, useMessageToaster, useModal, useOverlay, useRouter } from '@/composables'
|
||||
import { pluralize } from '@/utils'
|
||||
import { useDialogBox, useErrorHandler, useMessageToaster, useModal, useOverlay, useRouter } from '@/composables'
|
||||
|
||||
import Btn from '@/components/ui/form/Btn.vue'
|
||||
import TextInput from '@/components/ui/form/TextInput.vue'
|
||||
|
@ -45,7 +45,7 @@ import SelectBox from '@/components/ui/form/SelectBox.vue'
|
|||
|
||||
const { showOverlay, hideOverlay } = useOverlay()
|
||||
const { toastSuccess } = useMessageToaster()
|
||||
const { showConfirmDialog, showErrorDialog } = useDialogBox()
|
||||
const { showConfirmDialog } = useDialogBox()
|
||||
const { go } = useRouter()
|
||||
const { getFromContext } = useModal()
|
||||
|
||||
|
@ -70,9 +70,8 @@ const submit = async () => {
|
|||
close()
|
||||
toastSuccess(`Playlist "${playlist.name}" created.`)
|
||||
go(`playlist/${playlist.id}`)
|
||||
} catch (error) {
|
||||
showErrorDialog('Something went wrong. Please try again.', 'Error')
|
||||
logger.error(error)
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler('dialog').handleHttpError(error)
|
||||
} finally {
|
||||
hideOverlay()
|
||||
}
|
||||
|
|
|
@ -19,9 +19,8 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { logger } from '@/utils'
|
||||
import { playlistFolderStore } from '@/stores'
|
||||
import { useDialogBox, useMessageToaster, useModal, useOverlay } from '@/composables'
|
||||
import { useDialogBox, useErrorHandler, useMessageToaster, useModal, useOverlay } from '@/composables'
|
||||
|
||||
import Btn from '@/components/ui/form/Btn.vue'
|
||||
import TextInput from '@/components/ui/form/TextInput.vue'
|
||||
|
@ -29,7 +28,7 @@ import FormRow from '@/components/ui/form/FormRow.vue'
|
|||
|
||||
const { showOverlay, hideOverlay } = useOverlay()
|
||||
const { toastSuccess } = useMessageToaster()
|
||||
const { showConfirmDialog, showErrorDialog } = useDialogBox()
|
||||
const { showConfirmDialog } = useDialogBox()
|
||||
const folder = useModal().getFromContext<PlaylistFolder>('folder')
|
||||
|
||||
const name = ref(folder.name)
|
||||
|
@ -41,9 +40,8 @@ const submit = async () => {
|
|||
await playlistFolderStore.rename(folder, name.value)
|
||||
toastSuccess('Playlist folder renamed.')
|
||||
close()
|
||||
} catch (error) {
|
||||
showErrorDialog('Something went wrong. Please try again.', 'Error')
|
||||
logger.error(error)
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler('dialog').handleHttpError(error)
|
||||
} finally {
|
||||
hideOverlay()
|
||||
}
|
||||
|
|
|
@ -36,9 +36,8 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRef } from 'vue'
|
||||
import { logger } from '@/utils'
|
||||
import { playlistFolderStore, playlistStore } from '@/stores'
|
||||
import { useDialogBox, useMessageToaster, useModal, useOverlay } from '@/composables'
|
||||
import { useDialogBox, useErrorHandler, useMessageToaster, useModal, useOverlay } from '@/composables'
|
||||
|
||||
import Btn from '@/components/ui/form/Btn.vue'
|
||||
import TextInput from '@/components/ui/form/TextInput.vue'
|
||||
|
@ -47,7 +46,7 @@ import SelectBox from '@/components/ui/form/SelectBox.vue'
|
|||
|
||||
const { showOverlay, hideOverlay } = useOverlay()
|
||||
const { toastSuccess } = useMessageToaster()
|
||||
const { showConfirmDialog, showErrorDialog } = useDialogBox()
|
||||
const { showConfirmDialog } = useDialogBox()
|
||||
const playlist = useModal().getFromContext<Playlist>('playlist')
|
||||
|
||||
const name = ref(playlist.name)
|
||||
|
@ -68,9 +67,8 @@ const submit = async () => {
|
|||
|
||||
toastSuccess('Playlist updated.')
|
||||
close()
|
||||
} catch (error) {
|
||||
showErrorDialog('Something went wrong. Please try again.', 'Error')
|
||||
logger.error(error)
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler('dialog').handleHttpError(error)
|
||||
} finally {
|
||||
hideOverlay()
|
||||
}
|
||||
|
|
|
@ -16,9 +16,9 @@
|
|||
<script setup lang="ts">
|
||||
import { sortBy } from 'lodash'
|
||||
import { computed, onMounted, ref, Ref, toRefs } from 'vue'
|
||||
import { useAuthorization, useDialogBox } from '@/composables'
|
||||
import { useAuthorization, useDialogBox, useErrorHandler } from '@/composables'
|
||||
import { playlistCollaborationService } from '@/services'
|
||||
import { eventBus, logger } from '@/utils'
|
||||
import { eventBus } from '@/utils'
|
||||
|
||||
import ListSkeleton from '@/components/ui/skeletons/PlaylistCollaboratorListSkeleton.vue'
|
||||
import ListItem from '@/components/playlist/PlaylistCollaboratorListItem.vue'
|
||||
|
@ -62,8 +62,8 @@ const removeCollaborator = async (collaborator: PlaylistCollaborator) => {
|
|||
collaborators.value = collaborators.value.filter(({ id }) => id !== collaborator.id)
|
||||
await playlistCollaborationService.removeCollaborator(playlist.value, collaborator)
|
||||
eventBus.emit('PLAYLIST_COLLABORATOR_REMOVED', playlist.value)
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler().handleHttpError(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -54,9 +54,8 @@
|
|||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { ref, toRef } from 'vue'
|
||||
import { playlistFolderStore, playlistStore } from '@/stores'
|
||||
import { logger } from '@/utils'
|
||||
import {
|
||||
useDialogBox,
|
||||
useDialogBox, useErrorHandler,
|
||||
useKoelPlus,
|
||||
useMessageToaster,
|
||||
useModal,
|
||||
|
@ -81,7 +80,7 @@ const {
|
|||
|
||||
const { showOverlay, hideOverlay } = useOverlay()
|
||||
const { toastSuccess } = useMessageToaster()
|
||||
const { showConfirmDialog, showErrorDialog } = useDialogBox()
|
||||
const { showConfirmDialog } = useDialogBox()
|
||||
const { go } = useRouter()
|
||||
const { isPlus } = useKoelPlus()
|
||||
|
||||
|
@ -121,9 +120,8 @@ const submit = async () => {
|
|||
close()
|
||||
toastSuccess(`Playlist "${playlist.name}" created.`)
|
||||
go(`playlist/${playlist.id}`)
|
||||
} catch (error) {
|
||||
showErrorDialog('Something went wrong. Please try again.', 'Error')
|
||||
logger.error(error)
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler('dialog').handleHttpError(error)
|
||||
} finally {
|
||||
hideOverlay()
|
||||
}
|
||||
|
|
|
@ -59,8 +59,16 @@ import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
|||
import { reactive, toRef } from 'vue'
|
||||
import { cloneDeep, isEqual } from 'lodash'
|
||||
import { playlistFolderStore, playlistStore } from '@/stores'
|
||||
import { eventBus, logger } from '@/utils'
|
||||
import { useDialogBox, useKoelPlus, useMessageToaster, useModal, useOverlay, useSmartPlaylistForm } from '@/composables'
|
||||
import { eventBus } from '@/utils'
|
||||
import {
|
||||
useDialogBox,
|
||||
useErrorHandler,
|
||||
useKoelPlus,
|
||||
useMessageToaster,
|
||||
useModal,
|
||||
useOverlay,
|
||||
useSmartPlaylistForm
|
||||
} from '@/composables'
|
||||
import CheckBox from '@/components/ui/form/CheckBox.vue'
|
||||
import TextInput from '@/components/ui/form/TextInput.vue'
|
||||
import FormRow from '@/components/ui/form/FormRow.vue'
|
||||
|
@ -68,13 +76,11 @@ import SelectBox from '@/components/ui/form/SelectBox.vue'
|
|||
|
||||
const { showOverlay, hideOverlay } = useOverlay()
|
||||
const { toastSuccess } = useMessageToaster()
|
||||
const { showConfirmDialog, showErrorDialog } = useDialogBox()
|
||||
const { showConfirmDialog } = useDialogBox()
|
||||
const {isPlus} = useKoelPlus()
|
||||
|
||||
const playlist = useModal().getFromContext<Playlist>('playlist')
|
||||
|
||||
const folders = toRef(playlistFolderStore.state, 'folders')
|
||||
|
||||
const mutablePlaylist = reactive(cloneDeep(playlist))
|
||||
|
||||
const isPristine = () => isEqual(mutablePlaylist.rules, playlist.rules)
|
||||
|
@ -112,9 +118,8 @@ const submit = async () => {
|
|||
toastSuccess(`Playlist "${playlist.name}" updated.`)
|
||||
eventBus.emit('PLAYLIST_UPDATED', playlist)
|
||||
close()
|
||||
} catch (error) {
|
||||
showErrorDialog('Something went wrong. Please try again.', 'Error')
|
||||
logger.error(error)
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler('dialog').handleHttpError(error)
|
||||
} finally {
|
||||
hideOverlay()
|
||||
}
|
||||
|
|
|
@ -73,8 +73,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { authService, UpdateCurrentProfileData } from '@/services'
|
||||
import { logger, parseValidationError } from '@/utils'
|
||||
import { useDialogBox, useMessageToaster, useAuthorization } from '@/composables'
|
||||
import { useMessageToaster, useAuthorization, useErrorHandler } from '@/composables'
|
||||
|
||||
import Btn from '@/components/ui/form/Btn.vue'
|
||||
import PasswordField from '@/components/ui/form/PasswordField.vue'
|
||||
|
@ -84,13 +83,9 @@ import TextInput from '@/components/ui/form/TextInput.vue'
|
|||
import FormRow from '@/components/ui/form/FormRow.vue'
|
||||
|
||||
const { toastSuccess } = useMessageToaster()
|
||||
const { showErrorDialog } = useDialogBox()
|
||||
|
||||
const profile = ref<UpdateCurrentProfileData>({} as UpdateCurrentProfileData)
|
||||
|
||||
const isDemo = window.IS_DEMO
|
||||
|
||||
const { currentUser } = useAuthorization()
|
||||
const profile = ref<UpdateCurrentProfileData>({} as UpdateCurrentProfileData)
|
||||
const isDemo = window.IS_DEMO
|
||||
|
||||
onMounted(() => {
|
||||
profile.value = {
|
||||
|
@ -116,10 +111,8 @@ const update = async () => {
|
|||
profile.value.current_password = null
|
||||
delete profile.value.new_password
|
||||
toastSuccess('Profile updated.')
|
||||
} catch (error: any) {
|
||||
const msg = error.response.status === 422 ? parseValidationError(error.response.data)[0] : 'Unknown error.'
|
||||
await showErrorDialog(msg, 'Error')
|
||||
logger.log(error)
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler('dialog').handleHttpError(error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -37,8 +37,7 @@
|
|||
import { faCompactDisc } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed, ref, toRef, watch } from 'vue'
|
||||
import { albumStore, commonStore, preferenceStore as preferences } from '@/stores'
|
||||
import { useAuthorization, useInfiniteScroll, useMessageToaster, useRouter } from '@/composables'
|
||||
import { logger } from '@/utils'
|
||||
import { useAuthorization, useErrorHandler, useInfiniteScroll, useRouter } from '@/composables'
|
||||
|
||||
import AlbumCard from '@/components/album/AlbumCard.vue'
|
||||
import AlbumCardSkeleton from '@/components/ui/skeletons/ArtistAlbumCardSkeleton.vue'
|
||||
|
@ -54,10 +53,7 @@ const gridContainer = ref<HTMLDivElement>()
|
|||
const viewMode = ref<ArtistAlbumViewMode>('thumbnails')
|
||||
const albums = toRef(albumStore.state, 'albums')
|
||||
|
||||
const {
|
||||
ToTopButton,
|
||||
makeScrollable
|
||||
} = useInfiniteScroll(gridContainer, async () => await fetchAlbums())
|
||||
const { ToTopButton, makeScrollable } = useInfiniteScroll(gridContainer, async () => await fetchAlbums())
|
||||
|
||||
watch(viewMode, () => (preferences.albums_view_mode = viewMode.value))
|
||||
|
||||
|
@ -87,10 +83,9 @@ useRouter().onScreenActivated('Albums', async () => {
|
|||
|
||||
try {
|
||||
await makeScrollable()
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
useMessageToaster().toastError('Failed to load albums.')
|
||||
} catch (error: unknown) {
|
||||
initialized = false
|
||||
useErrorHandler().handleHttpError(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -90,10 +90,10 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, ref, toRef, watch } from 'vue'
|
||||
import { eventBus, logger, pluralize } from '@/utils'
|
||||
import { eventBus, pluralize } from '@/utils'
|
||||
import { albumStore, artistStore, commonStore, songStore } from '@/stores'
|
||||
import { downloadService } from '@/services'
|
||||
import { useDialogBox, useRouter, useSongList, useSongListControls } from '@/composables'
|
||||
import { useErrorHandler, useRouter, useSongList, useSongListControls } from '@/composables'
|
||||
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
import AlbumThumbnail from '@/components/ui/ArtistAlbumThumbnail.vue'
|
||||
|
@ -110,7 +110,6 @@ const AlbumInfo = defineAsyncComponent(() => import('@/components/album/AlbumInf
|
|||
const AlbumCard = defineAsyncComponent(() => import('@/components/album/AlbumCard.vue'))
|
||||
const AlbumCardSkeleton = defineAsyncComponent(() => import('@/components/ui/skeletons/ArtistAlbumCardSkeleton.vue'))
|
||||
|
||||
const { showErrorDialog } = useDialogBox()
|
||||
const { getRouteParam, go, onScreenActivated } = useRouter()
|
||||
|
||||
const albumId = ref<number>()
|
||||
|
@ -175,9 +174,8 @@ watch(albumId, async id => {
|
|||
context.entity = album.value
|
||||
|
||||
sort('track')
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
showErrorDialog('Failed to load album. Please try again.', 'Error')
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler('dialog').handleHttpError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
|
|
@ -63,16 +63,15 @@
|
|||
<script lang="ts" setup>
|
||||
import { faVolumeOff } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed, ref, toRef, watch } from 'vue'
|
||||
import { logger, pluralize, secondsToHumanReadable } from '@/utils'
|
||||
import { pluralize, secondsToHumanReadable } from '@/utils'
|
||||
import { commonStore, queueStore, songStore } from '@/stores'
|
||||
import { playbackService } from '@/services'
|
||||
import {
|
||||
useMessageToaster,
|
||||
useKoelPlus,
|
||||
useRouter,
|
||||
useSongList,
|
||||
useSongListControls,
|
||||
useLocalStorage
|
||||
useLocalStorage, useErrorHandler
|
||||
} from '@/composables'
|
||||
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
|
@ -101,7 +100,6 @@ const {
|
|||
|
||||
const { SongListControls, config } = useSongListControls('Songs')
|
||||
|
||||
const { toastError } = useMessageToaster()
|
||||
const { go, onScreenActivated } = useRouter()
|
||||
const { isPlus } = useKoelPlus()
|
||||
const { get: lsGet, set: lsSet } = useLocalStorage()
|
||||
|
@ -152,8 +150,7 @@ const fetchSongs = async () => {
|
|||
own_songs_only: ownSongsOnly.value
|
||||
})
|
||||
} catch (error: any) {
|
||||
toastError(error.response.data?.message || 'Failed to load songs.')
|
||||
logger.error(error)
|
||||
useErrorHandler().handleHttpError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
|
|
@ -37,8 +37,7 @@
|
|||
import { faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed, ref, toRef, watch } from 'vue'
|
||||
import { artistStore, commonStore, preferenceStore as preferences } from '@/stores'
|
||||
import { useAuthorization, useInfiniteScroll, useMessageToaster, useRouter } from '@/composables'
|
||||
import { logger } from '@/utils'
|
||||
import { useAuthorization, useErrorHandler, useInfiniteScroll, useRouter } from '@/composables'
|
||||
|
||||
import ArtistCard from '@/components/artist/ArtistCard.vue'
|
||||
import ArtistCardSkeleton from '@/components/ui/skeletons/ArtistAlbumCardSkeleton.vue'
|
||||
|
@ -86,10 +85,9 @@ useRouter().onScreenActivated('Artists', async () => {
|
|||
|
||||
try {
|
||||
await makeScrollable()
|
||||
} catch (error: any) {
|
||||
logger.error(error)
|
||||
useMessageToaster().toastError(error.response.data?.message || 'Failed to load artists.')
|
||||
} catch (error: unknown) {
|
||||
initialized = false
|
||||
useErrorHandler().handleHttpError(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -86,10 +86,16 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, ref, toRef, watch } from 'vue'
|
||||
import { eventBus, logger, pluralize } from '@/utils'
|
||||
import { eventBus, pluralize } from '@/utils'
|
||||
import { albumStore, artistStore, commonStore, songStore } from '@/stores'
|
||||
import { downloadService } from '@/services'
|
||||
import { useDialogBox, useRouter, useSongList, useSongListControls, useThirdPartyServices } from '@/composables'
|
||||
import {
|
||||
useErrorHandler,
|
||||
useRouter,
|
||||
useSongList,
|
||||
useSongListControls,
|
||||
useThirdPartyServices
|
||||
} from '@/composables'
|
||||
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
import ArtistThumbnail from '@/components/ui/ArtistAlbumThumbnail.vue'
|
||||
|
@ -106,7 +112,6 @@ const AlbumCardSkeleton = defineAsyncComponent(() => import('@/components/ui/ske
|
|||
type Tab = 'Songs' | 'Albums' | 'Info'
|
||||
const activeTab = ref<Tab>('Songs')
|
||||
|
||||
const { showErrorDialog } = useDialogBox()
|
||||
const { getRouteParam, go, onScreenActivated } = useRouter()
|
||||
|
||||
const artistId = ref<number>()
|
||||
|
@ -161,9 +166,8 @@ watch(artistId, async id => {
|
|||
])
|
||||
|
||||
context.entity = artist.value
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
showErrorDialog('Failed to load artist. Please try again.', 'Error')
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler('dialog').handleHttpError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
|
|
@ -50,10 +50,10 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { faTags } from '@fortawesome/free-solid-svg-icons'
|
||||
import { eventBus, logger, pluralize, secondsToHumanReadable } from '@/utils'
|
||||
import { eventBus, pluralize, secondsToHumanReadable } from '@/utils'
|
||||
import { playbackService } from '@/services'
|
||||
import { genreStore, songStore } from '@/stores'
|
||||
import { useDialogBox, useRouter, useSongList, useSongListControls } from '@/composables'
|
||||
import { useErrorHandler, useRouter, useSongList, useSongListControls } from '@/composables'
|
||||
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
import ScreenEmptyState from '@/components/ui/ScreenEmptyState.vue'
|
||||
|
@ -78,7 +78,6 @@ const {
|
|||
|
||||
const { SongListControls, config } = useSongListControls('Genre')
|
||||
|
||||
const { showErrorDialog } = useDialogBox()
|
||||
const { getRouteParam, go, onRouteChanged } = useRouter()
|
||||
|
||||
let sortField: SongListSortField = 'title'
|
||||
|
@ -122,9 +121,8 @@ const fetch = async () => {
|
|||
|
||||
page.value = fetched.nextPage
|
||||
songs.value.push(...fetched.songs)
|
||||
} catch (e) {
|
||||
showErrorDialog('Failed to fetch genre details or genre was not found.', 'Error')
|
||||
logger.error(e)
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler('dialog').handleHttpError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
|
|
@ -37,9 +37,9 @@
|
|||
import { faVolumeOff } from '@fortawesome/free-solid-svg-icons'
|
||||
import { sample } from 'lodash'
|
||||
import { computed, ref } from 'vue'
|
||||
import { eventBus, logger } from '@/utils'
|
||||
import { eventBus } from '@/utils'
|
||||
import { commonStore, overviewStore, userStore } from '@/stores'
|
||||
import { useAuthorization, useDialogBox, useRouter } from '@/composables'
|
||||
import { useAuthorization, useErrorHandler, useRouter } from '@/composables'
|
||||
|
||||
import MostPlayedSongs from '@/components/screens/home/MostPlayedSongs.vue'
|
||||
import RecentlyPlayedSongs from '@/components/screens/home/RecentlyPlayedSongs.vue'
|
||||
|
@ -53,7 +53,6 @@ import BtnScrollToTop from '@/components/ui/BtnScrollToTop.vue'
|
|||
import ScreenBase from '@/components/screens/ScreenBase.vue'
|
||||
|
||||
const { isAdmin } = useAuthorization()
|
||||
const { showErrorDialog } = useDialogBox()
|
||||
|
||||
const greetings = [
|
||||
'Oh hai!',
|
||||
|
@ -82,9 +81,8 @@ useRouter().onScreenActivated('Home', async () => {
|
|||
try {
|
||||
await overviewStore.init()
|
||||
initialized = true
|
||||
} catch (e) {
|
||||
showErrorDialog('Failed to load home screen data. Please try again.', 'Error')
|
||||
logger.error(e)
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler('dialog').handleHttpError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
|
|
@ -74,10 +74,17 @@
|
|||
import { faFile } from '@fortawesome/free-regular-svg-icons'
|
||||
import { differenceBy } from 'lodash'
|
||||
import { ref, toRef, watch } from 'vue'
|
||||
import { eventBus, logger, pluralize } from '@/utils'
|
||||
import { eventBus, pluralize } from '@/utils'
|
||||
import { commonStore, playlistStore, songStore } from '@/stores'
|
||||
import { downloadService, playlistCollaborationService } from '@/services'
|
||||
import { usePlaylistManagement, useRouter, useSongList, useAuthorization, useSongListControls } from '@/composables'
|
||||
import {
|
||||
usePlaylistManagement,
|
||||
useRouter,
|
||||
useSongList,
|
||||
useAuthorization,
|
||||
useSongListControls,
|
||||
useErrorHandler
|
||||
} from '@/composables'
|
||||
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
import ScreenEmptyState from '@/components/ui/ScreenEmptyState.vue'
|
||||
|
@ -141,8 +148,8 @@ const fetchDetails = async (refresh = false) => {
|
|||
|
||||
sortField.value ??= (playlist.value?.is_smart ? 'title' : 'position')
|
||||
sort(sortField.value, 'asc')
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler().handleHttpError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
|
|
@ -55,10 +55,10 @@
|
|||
<script lang="ts" setup>
|
||||
import { faCoffee } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed, ref, toRef } from 'vue'
|
||||
import { logger, pluralize } from '@/utils'
|
||||
import { pluralize } from '@/utils'
|
||||
import { commonStore, queueStore, songStore } from '@/stores'
|
||||
import { cache, playbackService } from '@/services'
|
||||
import { useDialogBox, useRouter, useSongList, useSongListControls } from '@/composables'
|
||||
import { useErrorHandler, useRouter, useSongList, useSongListControls } from '@/composables'
|
||||
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
import ScreenEmptyState from '@/components/ui/ScreenEmptyState.vue'
|
||||
|
@ -66,7 +66,6 @@ import SongListSkeleton from '@/components/ui/skeletons/SongListSkeleton.vue'
|
|||
import ScreenBase from '@/components/screens/ScreenBase.vue'
|
||||
|
||||
const { go, onScreenActivated } = useRouter()
|
||||
const { showErrorDialog } = useDialogBox()
|
||||
|
||||
const {
|
||||
SongList,
|
||||
|
@ -100,9 +99,8 @@ const shuffleSome = async () => {
|
|||
loading.value = true
|
||||
await queueStore.fetchRandom()
|
||||
await playbackService.playFirstInQueue()
|
||||
} catch (e) {
|
||||
showErrorDialog('Failed to fetch songs to play. Please try again.', 'Error')
|
||||
logger.error(e)
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler('dialog').handleHttpError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
@ -141,9 +139,8 @@ onScreenActivated('Queue', async () => {
|
|||
if (!song) {
|
||||
throw new Error('Song not found')
|
||||
}
|
||||
} catch (e) {
|
||||
showErrorDialog('Song not found. Please double check and try again.', 'Error')
|
||||
logger.error(e)
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler('dialog').handleHttpError(error)
|
||||
return
|
||||
} finally {
|
||||
cache.remove('song-to-queue')
|
||||
|
|
|
@ -41,8 +41,8 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { commonStore, settingStore } from '@/stores'
|
||||
import { forceReloadWindow, parseValidationError } from '@/utils'
|
||||
import { useDialogBox, useMessageToaster, useOverlay, useRouter } from '@/composables'
|
||||
import { forceReloadWindow, } from '@/utils'
|
||||
import { useDialogBox, useErrorHandler, useMessageToaster, useOverlay, useRouter } from '@/composables'
|
||||
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
import Btn from '@/components/ui/form/Btn.vue'
|
||||
|
@ -51,7 +51,7 @@ import ScreenBase from '@/components/screens/ScreenBase.vue'
|
|||
import FormRow from '@/components/ui/form/FormRow.vue'
|
||||
|
||||
const { toastSuccess } = useMessageToaster()
|
||||
const { showConfirmDialog, showErrorDialog } = useDialogBox()
|
||||
const { showConfirmDialog } = useDialogBox()
|
||||
const { go } = useRouter()
|
||||
const { showOverlay, hideOverlay } = useOverlay()
|
||||
|
||||
|
@ -81,9 +81,8 @@ const save = async () => {
|
|||
// Make sure we're back to home first.
|
||||
go('home')
|
||||
forceReloadWindow()
|
||||
} catch (err: any) {
|
||||
const msg = err.response.status === 422 ? parseValidationError(err.response.data)[0] : 'Unknown error.'
|
||||
showErrorDialog(msg, 'Error')
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler('dialog').handleHttpError(error)
|
||||
} finally {
|
||||
hideOverlay()
|
||||
}
|
||||
|
|
|
@ -57,9 +57,9 @@ const render = async (viz: Visualizer) => {
|
|||
|
||||
try {
|
||||
destroyVisualizer = await viz.init(el.value!)
|
||||
} catch (e) {
|
||||
} catch (error: unknown) {
|
||||
// in e.g., DOM testing, the call will fail due to the lack of proper API support
|
||||
logger.warn('Failed to initialize visualizer', e)
|
||||
logger.warn('Failed to initialize visualizer', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -166,9 +166,9 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { isEqual } from 'lodash'
|
||||
import { defaultCover, eventBus, logger, pluralize } from '@/utils'
|
||||
import { defaultCover, eventBus, pluralize } from '@/utils'
|
||||
import { songStore, SongUpdateData } from '@/stores'
|
||||
import { useDialogBox, useMessageToaster, useModal, useOverlay } from '@/composables'
|
||||
import { useDialogBox, useErrorHandler, useMessageToaster, useModal, useOverlay } from '@/composables'
|
||||
import { genres } from '@/config'
|
||||
|
||||
import Btn from '@/components/ui/form/Btn.vue'
|
||||
|
@ -183,7 +183,7 @@ import TabPanelContainer from '@/components/ui/tabs/TabPanelContainer.vue'
|
|||
|
||||
const { showOverlay, hideOverlay } = useOverlay()
|
||||
const { toastSuccess } = useMessageToaster()
|
||||
const { showConfirmDialog, showErrorDialog } = useDialogBox()
|
||||
const { showConfirmDialog } = useDialogBox()
|
||||
const { getFromContext } = useModal()
|
||||
|
||||
const songs = getFromContext<Song[]>('songs')
|
||||
|
@ -260,9 +260,8 @@ const submit = async () => {
|
|||
toastSuccess(`Updated ${pluralize(songs, 'song')}.`)
|
||||
eventBus.emit('SONGS_UPDATED')
|
||||
close()
|
||||
} catch (error) {
|
||||
showErrorDialog('Something went wrong. Please try again.', 'Error')
|
||||
logger.error(error)
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler('dialog').handleHttpError(error)
|
||||
} finally {
|
||||
hideOverlay()
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ const thumbnailUrl = ref<String | null>(null)
|
|||
watchEffect(async () => {
|
||||
try {
|
||||
thumbnailUrl.value = await albumStore.fetchThumbnail(album.value)
|
||||
} catch (e) {
|
||||
} catch (error: unknown) {
|
||||
thumbnailUrl.value = null
|
||||
}
|
||||
})
|
||||
|
|
|
@ -37,21 +37,19 @@ import { orderBy } from 'lodash'
|
|||
import { computed, ref, toRefs } from 'vue'
|
||||
import { albumStore, artistStore, queueStore, songStore } from '@/stores'
|
||||
import { playbackService } from '@/services'
|
||||
import { defaultCover, logger } from '@/utils'
|
||||
import { usePolicies, useMessageToaster, useRouter, useFileReader } from '@/composables'
|
||||
import { defaultCover } from '@/utils'
|
||||
import { usePolicies, useMessageToaster, useRouter, useFileReader, useErrorHandler } from '@/composables'
|
||||
import { acceptedImageTypes } from '@/config'
|
||||
|
||||
const { toastSuccess } = useMessageToaster()
|
||||
const { go } = useRouter()
|
||||
const { currentUserCan } = usePolicies()
|
||||
|
||||
const props = defineProps<{ entity: Album | Artist }>()
|
||||
const { entity } = toRefs(props)
|
||||
|
||||
const droppable = ref(false)
|
||||
|
||||
const { toastError } = useMessageToaster()
|
||||
const { currentUserCan } = usePolicies()
|
||||
|
||||
const forAlbum = computed(() => entity.value.type === 'albums')
|
||||
const sortFields = computed(() => forAlbum.value ? ['disc', 'track'] : ['album_id', 'disc', 'track'])
|
||||
|
||||
|
@ -126,10 +124,7 @@ const onDrop = async (event: DragEvent) => {
|
|||
await artistStore.uploadImage(entity.value as Artist, url)
|
||||
}
|
||||
})
|
||||
} catch (e: any) {
|
||||
const message = e.response.data?.message || 'Unknown error.'
|
||||
toastError(`Failed to upload: ${message}`)
|
||||
|
||||
} catch (error: unknown) {
|
||||
// restore the backup image
|
||||
if (forAlbum.value) {
|
||||
(entity.value as Album).cover = backupImage!
|
||||
|
@ -137,7 +132,7 @@ const onDrop = async (event: DragEvent) => {
|
|||
(entity.value as Artist).image = backupImage!
|
||||
}
|
||||
|
||||
logger.error(e)
|
||||
useErrorHandler().handleHttpError(error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -97,8 +97,8 @@ const open = async (t = 0, l = 0) => {
|
|||
try {
|
||||
await preventOffScreen(el.value!)
|
||||
initSubmenus()
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
} catch (error: unknown) {
|
||||
logger.error(error)
|
||||
// in a non-browser environment (e.g., unit testing), these two functions are broken due to calls to
|
||||
// getBoundingClientRect() and querySelectorAll()
|
||||
}
|
||||
|
|
|
@ -15,21 +15,19 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, toRef, toRefs } from 'vue'
|
||||
import { defaultCover, logger } from '@/utils'
|
||||
import { playlistStore, userStore } from '@/stores'
|
||||
import { useAuthorization, useFileReader, useKoelPlus, useMessageToaster } from '@/composables'
|
||||
import { computed, ref, toRefs } from 'vue'
|
||||
import { defaultCover } from '@/utils'
|
||||
import { playlistStore } from '@/stores'
|
||||
import { useAuthorization, useErrorHandler, useFileReader, useKoelPlus } from '@/composables'
|
||||
import { acceptedImageTypes } from '@/config'
|
||||
|
||||
const props = defineProps<{ playlist: Playlist }>()
|
||||
const { playlist } = toRefs(props)
|
||||
|
||||
const droppable = ref(false)
|
||||
const user = toRef(userStore.state, 'current')
|
||||
|
||||
const { isAdmin } = useAuthorization()
|
||||
const { isPlus } = useKoelPlus()
|
||||
const { toastError } = useMessageToaster()
|
||||
|
||||
const backgroundImage = computed(() => `url(${playlist.value.cover || defaultCover })`)
|
||||
|
||||
|
@ -72,14 +70,10 @@ const onDrop = async (event: DragEvent) => {
|
|||
playlist.value.cover = url
|
||||
await playlistStore.uploadCover(playlist.value, url)
|
||||
})
|
||||
} catch (e: any) {
|
||||
const message = e.response.data?.message || 'Unknown error.'
|
||||
toastError(`Failed to upload: ${message}`)
|
||||
|
||||
} catch (error: unknown) {
|
||||
// restore the backup image
|
||||
playlist.value.cover = backupImage
|
||||
|
||||
logger.error(e)
|
||||
useErrorHandler().handleHttpError(error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, ref, toRefs, watch } from 'vue'
|
||||
import { youTubeService } from '@/services'
|
||||
import { logger } from '@/utils'
|
||||
import { useErrorHandler } from '@/composables'
|
||||
|
||||
const Btn = defineAsyncComponent(() => import('@/components/ui/form/Btn.vue'))
|
||||
const YouTubeVideo = defineAsyncComponent(() => import('@/components/ui/youtube/YouTubeVideoItem.vue'))
|
||||
|
@ -36,8 +36,8 @@ const loadMore = async () => {
|
|||
const result = await youTubeService.searchVideosBySong(song.value, nextPageToken)
|
||||
nextPageToken = result.nextPageToken
|
||||
videos.value.push(...result.items)
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler().handleHttpError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
|
|
@ -44,8 +44,7 @@
|
|||
import { isEqual } from 'lodash'
|
||||
import { reactive } from 'vue'
|
||||
import { CreateUserData, userStore } from '@/stores'
|
||||
import { parseValidationError } from '@/utils'
|
||||
import { useDialogBox, useMessageToaster, useOverlay } from '@/composables'
|
||||
import { useDialogBox, useErrorHandler, useMessageToaster, useOverlay } from '@/composables'
|
||||
|
||||
import Btn from '@/components/ui/form/Btn.vue'
|
||||
import TooltipIcon from '@/components/ui/TooltipIcon.vue'
|
||||
|
@ -55,7 +54,7 @@ import FormRow from '@/components/ui/form/FormRow.vue'
|
|||
|
||||
const { showOverlay, hideOverlay } = useOverlay()
|
||||
const { toastSuccess } = useMessageToaster()
|
||||
const { showErrorDialog, showConfirmDialog } = useDialogBox()
|
||||
const { showConfirmDialog } = useDialogBox()
|
||||
|
||||
const emptyUserData: CreateUserData = {
|
||||
name: '',
|
||||
|
@ -73,9 +72,8 @@ const submit = async () => {
|
|||
await userStore.store(newUser)
|
||||
toastSuccess(`New user "${newUser.name}" created.`)
|
||||
close()
|
||||
} catch (err: any) {
|
||||
const msg = err.response.status === 422 ? parseValidationError(err.response.data)[0] : 'Unknown error.'
|
||||
showErrorDialog(msg, 'Error')
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler('dialog').handleHttpError(error)
|
||||
} finally {
|
||||
hideOverlay()
|
||||
}
|
||||
|
|
|
@ -54,9 +54,8 @@
|
|||
<script lang="ts" setup>
|
||||
import { isEqual } from 'lodash'
|
||||
import { reactive, watch } from 'vue'
|
||||
import { logger, parseValidationError } from '@/utils'
|
||||
import { UpdateUserData, userStore } from '@/stores'
|
||||
import { useDialogBox, useMessageToaster, useModal, useOverlay } from '@/composables'
|
||||
import { useDialogBox, useErrorHandler, useMessageToaster, useModal, useOverlay } from '@/composables'
|
||||
|
||||
import Btn from '@/components/ui/form/Btn.vue'
|
||||
import TooltipIcon from '@/components/ui/TooltipIcon.vue'
|
||||
|
@ -67,7 +66,7 @@ import FormRow from '@/components/ui/form/FormRow.vue'
|
|||
|
||||
const { showOverlay, hideOverlay } = useOverlay()
|
||||
const { toastSuccess } = useMessageToaster()
|
||||
const { showConfirmDialog, showErrorDialog } = useDialogBox()
|
||||
const { showConfirmDialog } = useDialogBox()
|
||||
|
||||
const user = useModal().getFromContext<User>('user')
|
||||
|
||||
|
@ -91,10 +90,8 @@ const submit = async () => {
|
|||
await userStore.update(user, updateData)
|
||||
toastSuccess('User profile updated.')
|
||||
close()
|
||||
} catch (error: any) {
|
||||
const msg = error.response.status === 422 ? parseValidationError(error.response.data)[0] : 'Unknown error.'
|
||||
showErrorDialog(msg, 'Error')
|
||||
logger.error(error)
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler('dialog').handleHttpError(error)
|
||||
} finally {
|
||||
hideOverlay()
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<main class="space-y-5">
|
||||
<FormRow>
|
||||
<template #label>Emails</template>
|
||||
<TextArea ref="emailsEl" v-model="rawEmails" name="emails" required title="Emails" />
|
||||
<TextArea ref="emailsEl" v-model="rawEmails" name="emails" class="!min-h-[8rem]" required title="Emails" />
|
||||
<template #help>To invite multiple users, input one email per line.</template>
|
||||
</FormRow>
|
||||
<FormRow>
|
||||
|
@ -28,8 +28,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { parseValidationError } from '@/utils'
|
||||
import { useDialogBox, useMessageToaster, useOverlay } from '@/composables'
|
||||
import { useDialogBox, useErrorHandler, useMessageToaster, useOverlay } from '@/composables'
|
||||
import { invitationService } from '@/services'
|
||||
|
||||
import Btn from '@/components/ui/form/Btn.vue'
|
||||
|
@ -40,7 +39,7 @@ import FormRow from '@/components/ui/form/FormRow.vue'
|
|||
|
||||
const { showOverlay, hideOverlay } = useOverlay()
|
||||
const { toastSuccess } = useMessageToaster()
|
||||
const { showErrorDialog, showConfirmDialog } = useDialogBox()
|
||||
const { showConfirmDialog } = useDialogBox()
|
||||
|
||||
const emailsEl = ref<InstanceType<typeof TextArea>>()
|
||||
const rawEmails = ref('')
|
||||
|
@ -81,9 +80,8 @@ const submit = async () => {
|
|||
await invitationService.invite(validEmails, isAdmin.value)
|
||||
toastSuccess(`Invitation${validEmails.length === 1 ? '' : 's'} sent.`)
|
||||
close()
|
||||
} catch (err: any) {
|
||||
const msg = err.response.status === 422 ? parseValidationError(err.response.data)[0] : 'Unknown error.'
|
||||
showErrorDialog(msg, 'Error')
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler('dialog').handleHttpError(error)
|
||||
} finally {
|
||||
hideOverlay()
|
||||
}
|
||||
|
@ -101,14 +99,3 @@ const maybeClose = async () => {
|
|||
await showConfirmDialog('Discard all changes?') && close()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
textarea {
|
||||
min-height: 8rem !important;
|
||||
}
|
||||
|
||||
small.help {
|
||||
margin: .75rem 0 .5rem;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -50,8 +50,8 @@ import { faCircleCheck, faShield } from '@fortawesome/free-solid-svg-icons'
|
|||
import { computed, toRefs } from 'vue'
|
||||
import { userStore } from '@/stores'
|
||||
import { invitationService } from '@/services'
|
||||
import { useAuthorization, useDialogBox, useMessageToaster, useRouter } from '@/composables'
|
||||
import { eventBus, parseValidationError } from '@/utils'
|
||||
import { useAuthorization, useDialogBox, useErrorHandler, useMessageToaster, useRouter } from '@/composables'
|
||||
import { eventBus } from '@/utils'
|
||||
|
||||
import Btn from '@/components/ui/form/Btn.vue'
|
||||
import UserAvatar from '@/components/user/UserAvatar.vue'
|
||||
|
@ -60,7 +60,7 @@ const props = defineProps<{ user: User }>()
|
|||
const { user } = toRefs(props)
|
||||
|
||||
const { toastSuccess } = useMessageToaster()
|
||||
const { showConfirmDialog, showErrorDialog } = useDialogBox()
|
||||
const { showConfirmDialog } = useDialogBox()
|
||||
const { go } = useRouter()
|
||||
|
||||
const { currentUser } = useAuthorization()
|
||||
|
@ -82,14 +82,10 @@ const revokeInvite = async () => {
|
|||
try {
|
||||
await invitationService.revoke(user.value)
|
||||
toastSuccess(`Invitation for ${user.value.email} revoked.`)
|
||||
} catch (err: any) {
|
||||
if (err.response.status === 404) {
|
||||
showErrorDialog('Cannot revoke the invite. Maybe it has been accepted?', 'Revocation Failed')
|
||||
return
|
||||
}
|
||||
|
||||
const msg = err.response.status === 422 ? parseValidationError(err.response.data)[0] : 'Unknown error.'
|
||||
showErrorDialog(msg, 'Error')
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler('dialog').handleHttpError(error, {
|
||||
404: 'Cannot revoke the invite. Maybe it has been accepted?'
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useOverlay } from '@/composables'
|
||||
import { useErrorHandler, useOverlay } from '@/composables'
|
||||
import { commonStore, preferenceStore as preferences } from '@/stores'
|
||||
import { socketListener, socketService, uploadService } from '@/services'
|
||||
|
||||
|
@ -42,9 +42,9 @@ onMounted(async () => {
|
|||
await socketService.init() && socketListener.listen()
|
||||
|
||||
emits('success')
|
||||
} catch (err) {
|
||||
emits('error', err)
|
||||
throw err
|
||||
} catch (error: unknown) {
|
||||
useErrorHandler().handleHttpError(error)
|
||||
emits('error', error)
|
||||
} finally {
|
||||
hideOverlay()
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ export * from './useAuthorization'
|
|||
export * from './useContextMenu'
|
||||
export * from './useDialogBox'
|
||||
export * from './useDragAndDrop'
|
||||
export * from './useErrorHandler'
|
||||
export * from './useFileReader'
|
||||
export * from './useFloatingUi'
|
||||
export * from './useInfiniteScroll'
|
||||
|
|
|
@ -97,8 +97,8 @@ export const useDroppable = (acceptedTypes: DraggableType[]) => {
|
|||
|
||||
try {
|
||||
return JSON.parse(event.dataTransfer?.getData(`application/x-koel.${type}`)!)
|
||||
} catch (e) {
|
||||
logger.warn('Failed to parse dropped data', e)
|
||||
} catch (error: unknown) {
|
||||
logger.warn('Failed to parse dropped data', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ export const useDroppable = (acceptedTypes: DraggableType[]) => {
|
|||
default:
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
logger.error(error, event)
|
||||
}
|
||||
}
|
||||
|
@ -141,7 +141,7 @@ export const useDroppable = (acceptedTypes: DraggableType[]) => {
|
|||
default:
|
||||
throw new Error(`Unknown drag type: ${type}`)
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
logger.error(error, event)
|
||||
return <Song[]>[]
|
||||
}
|
||||
|
|
62
resources/assets/js/composables/useErrorHandler.ts
Normal file
62
resources/assets/js/composables/useErrorHandler.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { useMessageToaster, useDialogBox } from '@/composables'
|
||||
import axios, { AxiosResponse } from 'axios'
|
||||
import { logger, parseValidationError } from '@/utils'
|
||||
|
||||
export interface StatusMessageMap {
|
||||
[key: AxiosResponse['status']]: string | Closure
|
||||
}
|
||||
|
||||
type ErrorMessageDriver = 'toast' | 'dialog'
|
||||
|
||||
export const useErrorHandler = (driver: ErrorMessageDriver = 'toast') => {
|
||||
const { toastError } = useMessageToaster()
|
||||
const { showErrorDialog } = useDialogBox()
|
||||
|
||||
const showGenericError = () => {
|
||||
if (driver === 'toast') {
|
||||
toastError('An unknown error occurred.')
|
||||
} else {
|
||||
showErrorDialog('An unknown error occurred.')
|
||||
}
|
||||
}
|
||||
|
||||
const handleHttpError = (
|
||||
error: unknown,
|
||||
statusMessageMap: StatusMessageMap = {}
|
||||
) => {
|
||||
logger.error(error)
|
||||
|
||||
if (!axios.isAxiosError(error)) {
|
||||
return showGenericError()
|
||||
}
|
||||
|
||||
if (!error.response?.status || !statusMessageMap.hasOwnProperty(error.response.status)) {
|
||||
showError('An unknown error occurred.')
|
||||
return
|
||||
}
|
||||
|
||||
if (error.response?.status === 422) {
|
||||
return showError(parseValidationError(error.response.data)[0])
|
||||
}
|
||||
|
||||
if (typeof statusMessageMap[error.response!.status!] === 'string') {
|
||||
showError(statusMessageMap[error.response!.status!]) // @ts-ignore
|
||||
} else {
|
||||
return statusMessageMap[error.response!.status!]() // @ts-ignore
|
||||
}
|
||||
}
|
||||
|
||||
const showError = (message: string) => {
|
||||
if (driver === 'toast') {
|
||||
toastError(message)
|
||||
} else {
|
||||
showErrorDialog(message)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handleHttpError,
|
||||
parseValidationError,
|
||||
showGenericError
|
||||
}
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
import { playlistStore } from '@/stores'
|
||||
import { eventBus, logger, pluralize } from '@/utils'
|
||||
import { useDialogBox, useMessageToaster } from '@/composables'
|
||||
import { eventBus, pluralize } from '@/utils'
|
||||
import { useErrorHandler, useMessageToaster } from '@/composables'
|
||||
|
||||
export const usePlaylistManagement = () => {
|
||||
const { handleHttpError } = useErrorHandler('dialog')
|
||||
const { toastSuccess } = useMessageToaster()
|
||||
const { showErrorDialog } = useDialogBox()
|
||||
|
||||
const addSongsToPlaylist = async (playlist: Playlist, songs: Song[]) => {
|
||||
if (playlist.is_smart || songs.length === 0) return
|
||||
|
@ -13,9 +13,8 @@ export const usePlaylistManagement = () => {
|
|||
await playlistStore.addSongs(playlist, songs)
|
||||
eventBus.emit('PLAYLIST_UPDATED', playlist)
|
||||
toastSuccess(`Added ${pluralize(songs, 'song')} into "${playlist.name}."`)
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
showErrorDialog('Something went wrong. Please try again.', 'Error')
|
||||
} catch (error: unknown) {
|
||||
handleHttpError(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,9 +25,8 @@ export const usePlaylistManagement = () => {
|
|||
await playlistStore.removeSongs(playlist, songs)
|
||||
eventBus.emit('PLAYLIST_SONGS_REMOVED', playlist, songs)
|
||||
toastSuccess(`Removed ${pluralize(songs, 'song')} from "${playlist.name}."`)
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
showErrorDialog('Something went wrong. Please try again.', 'Error')
|
||||
} catch (error: unknown) {
|
||||
handleHttpError(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -95,8 +95,8 @@ export const routes: Route[] = [
|
|||
const playlist = await playlistCollaborationService.acceptInvite(params.id)
|
||||
Router.go(`/playlist/${playlist.id}`, true)
|
||||
return true
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
} catch (error: unknown) {
|
||||
logger.error(error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,8 +69,8 @@ const init = async () => {
|
|||
state.song = data.song || null
|
||||
connected.value = true
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
} catch (error: unknown) {
|
||||
logger.error(error)
|
||||
authenticated.value = false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,10 +109,10 @@ class PlaybackService {
|
|||
notification.onclick = () => window.focus()
|
||||
|
||||
window.setTimeout(() => notification.close(), 5000)
|
||||
} catch (e) {
|
||||
} catch (error: unknown) {
|
||||
// Notification fails.
|
||||
// @link https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification
|
||||
logger.error(e)
|
||||
logger.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -151,7 +151,7 @@ class PlaybackService {
|
|||
song: song.id,
|
||||
position: 0
|
||||
})
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
logger.error(error)
|
||||
}
|
||||
|
||||
|
@ -161,7 +161,7 @@ class PlaybackService {
|
|||
await this.player.media.play()
|
||||
navigator.mediaSession && (navigator.mediaSession.playbackState = 'playing')
|
||||
this.showNotification(song)
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
// convert this into a warning, as an error will cause Cypress to fail the tests entirely
|
||||
logger.warn(error)
|
||||
}
|
||||
|
@ -276,7 +276,7 @@ class PlaybackService {
|
|||
|
||||
try {
|
||||
await this.player.media.play()
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
logger.error(error)
|
||||
}
|
||||
|
||||
|
@ -386,7 +386,7 @@ class PlaybackService {
|
|||
song: currentSong.id,
|
||||
position: Math.ceil(media.currentTime)
|
||||
})
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
logger.error(error)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { reactive } from 'vue'
|
|||
import { http } from '@/services'
|
||||
import { albumStore, commonStore, overviewStore, songStore } from '@/stores'
|
||||
import { logger } from '@/utils'
|
||||
import axios from 'axios'
|
||||
|
||||
interface UploadResult {
|
||||
song: Song
|
||||
|
@ -92,10 +93,16 @@ export const uploadService = {
|
|||
this.proceed() // upload the next file
|
||||
|
||||
window.setTimeout(() => this.remove(file), 1000)
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
logger.error(error)
|
||||
file.message = `Upload failed: ${error.response?.data?.message || 'Unknown error'}`
|
||||
file.status = 'Errored'
|
||||
|
||||
if (axios.isAxiosError(error) && error.response?.data?.message) {
|
||||
file.message = `Upload failed: ${error.response.data.message}`
|
||||
} else {
|
||||
file.message = 'Upload failed: Unknown error.'
|
||||
}
|
||||
|
||||
this.proceed() // upload the next file
|
||||
}
|
||||
},
|
||||
|
|
|
@ -71,8 +71,8 @@ export const albumStore = {
|
|||
album = this.syncWithVault(
|
||||
await cache.remember<Album>(['album', id], async () => await http.get<Album>(`albums/${id}`))
|
||||
)[0]
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
} catch (error: unknown) {
|
||||
logger.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -61,8 +61,8 @@ export const artistStore = {
|
|||
artist = this.syncWithVault(
|
||||
await cache.remember<Artist>(['artist', id], async () => await http.get<Artist>(`artists/${id}`))
|
||||
)[0]
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
} catch (error: unknown) {
|
||||
logger.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ export const playlistStore = {
|
|||
try {
|
||||
this.setupSmartPlaylist(playlist)
|
||||
this.state.playlists.push(playlist)
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
logger.warn(`Failed to setup smart playlist "${playlist.name}".`, error)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -149,8 +149,8 @@ export const queueStore = {
|
|||
saveState () {
|
||||
try {
|
||||
http.silently.put('queue/state', { songs: this.state.songs.map(({ id }) => id) })
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
} catch (error: unknown) {
|
||||
logger.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,8 +72,8 @@ export const songStore = {
|
|||
if (!song) {
|
||||
try {
|
||||
song = this.syncWithVault(await http.get<Song>(`songs/${id}`))[0]
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
} catch (error: unknown) {
|
||||
logger.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ export const forceReloadWindow = (): void => {
|
|||
export const copyText = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
} catch (e) {
|
||||
} catch (error: unknown) {
|
||||
let copyArea = document.querySelector<HTMLTextAreaElement>('#copyArea')
|
||||
|
||||
if (!copyArea) {
|
||||
|
|
|
@ -51,6 +51,7 @@ export type ServerValidationError = {
|
|||
message: string
|
||||
errors: Record<string, string[]>
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the validation error from the server into a flattened array of messages.
|
||||
*/
|
||||
|
|
Loading…
Reference in a new issue