Skip to content

Commit

Permalink
feat(equipment): implement error handling for equipment addition, upd…
Browse files Browse the repository at this point in the history
…ate, and deletion
  • Loading branch information
Perdolique committed Nov 18, 2024
1 parent f6e0f77 commit d7ca267
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 33 deletions.
9 changes: 2 additions & 7 deletions app/components/equipment/AddEquipmentForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
</template>

<script lang="ts" setup>
import { FetchError } from 'ofetch';
import EmptyState from '~/components/EmptyState.vue';
import EditEquipmentForm from '~/components/equipment/EditEquipmentForm.vue';
Expand All @@ -33,6 +32,7 @@
const groupId = ref('')
const isSubmitting = ref(false)
const { addToast } = useToaster()
const { showErrorToast } = useApiErrorToast()
const { groups, fetchGroups, hasError: hasGroupsError } = useEquipmentGroupsState()
const { types, fetchTypes, hasError: hasTypesError } = useEquipmentTypesState()
Expand Down Expand Up @@ -91,12 +91,7 @@
resetForm()
} catch (error) {
if (error instanceof FetchError) {
addToast({
title: 'Failed to add equipment 🥲',
message: error.data.message
})
}
showErrorToast(error, 'Failed to add equipment 🥲')
} finally {
isSubmitting.value = false
}
Expand Down
23 changes: 23 additions & 0 deletions app/composables/use-api-error-toast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { FetchError } from 'ofetch';

export default function useApiErrorToast() {
const { addToast } = useToaster();

function showErrorToast(error: unknown, title: string) {
if (error instanceof FetchError) {
addToast({
title,
message: error.data.message
});
} else {
addToast({
title: 'Error',
message: 'An unexpected error occurred'
});
}
}

return {
showErrorToast
};
}
23 changes: 10 additions & 13 deletions app/pages/equipment/item/[itemId]/edit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,26 @@
</template>

<script lang="ts" setup>
import { FetchError } from 'ofetch'
import EditEquipmentForm from '~/components/equipment/EditEquipmentForm.vue'
import PageContent from '~/components/layout/PageContent.vue'
import EmptyState from '~/components/EmptyState.vue'
definePageMeta({
layout: 'page'
layout: 'page',
middleware: 'admin'
})
const route = useRoute()
const router = useRouter()
const isSubmitting = ref(false)
const { addToast } = useToaster()
const { showErrorToast } = useApiErrorToast()
// TODO: create an utility function
const itemId = computed(() => route.params.itemId?.toString() ?? '')
const itemId = route.params.itemId?.toString() ?? ''
// TODO: use useAsyncData
const { data, error } = await useFetch(`/api/equipment/items/${itemId.value}`)
const { data, error } = await useFetch(`/api/equipment/items/${itemId}`)
const { groups, fetchGroups } = useEquipmentGroupsState()
const { types, fetchTypes } = useEquipmentTypesState()
Expand Down Expand Up @@ -79,7 +80,7 @@
const errorText = computed(() => {
if (error.value?.statusCode === 404) {
return `Item with ID ${itemId.value} not found`
return `Item with ID ${itemId} not found`
}
return 'Something went wrong'
Expand All @@ -93,8 +94,9 @@
try {
isSubmitting.value = true
await $fetch(`/api/equipment/items/${itemId.value}`, {
await $fetch(`/api/equipment/items/${itemId}`, {
method: 'PATCH',
body: {
name: name.value,
description: description.value,
Expand All @@ -109,14 +111,9 @@
message: 'The equipment has been successfully updated'
})
router.push(`/equipment/item/${itemId.value}`)
router.push(`/equipment/item/${itemId}`)
} catch (error) {
if (error instanceof FetchError) {
addToast({
title: 'Failed to update equipment 🥲',
message: error.data.message
})
}
showErrorToast(error, 'Failed to update equipment 🥲')
} finally {
isSubmitting.value = false
}
Expand Down
71 changes: 58 additions & 13 deletions app/pages/equipment/item/[itemId]/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<PageContent :page-title="itemName">
<template #actions>
<PerdMenu
v-if="user.isAdmin"
icon="tabler:adjustments"
text="Manage"
>
Expand All @@ -14,9 +15,15 @@

<OptionButton
icon="tabler:trash"
@click="onDelete"
@click="showDeleteConfirmation"
>
Delete
<template v-if="isDeleting">
Deleting...
</template>

<template v-else>
Delete
</template>
</OptionButton>
</PerdMenu>
</template>
Expand Down Expand Up @@ -101,6 +108,15 @@
</div>
</div>
</PageContent>

<ConfirmationDialog
v-model="isDeleteDialogOpened"
header-text="Delete item"
confirm-button-text="Delete"
@confirm="deleteItem"
>
Item <strong>{{ itemName }}</strong> will be deleted
</ConfirmationDialog>
</template>

<script lang="ts" setup>
Expand All @@ -110,18 +126,20 @@
import PerdMenu from '~/components/PerdMenu.vue';
import OptionButton from '~/components/PerdMenu/OptionButton.vue';
import PerdTag from '~/components/PerdTag.vue';
import ConfirmationDialog from '~/components/dialogs/ConfirmationDialog.vue';
definePageMeta({
layout: 'page'
})
const route = useRoute()
const router = useRouter()
const { user } = useUserStore()
const { addToast } = useToaster()
const itemId = computed(() => route.params.itemId)
const itemId = route.params.itemId?.toString() ?? ''
const itemName = ref('')
const description = ref('')
const { data, error } = await useFetch(`/api/equipment/items/${itemId.value}`)
const { data, error } = await useFetch(`/api/equipment/items/${itemId}`)
itemName.value = data.value?.equipment.name ?? '¯\\_(ツ)_/¯'
description.value = data.value?.equipment.description ?? ''
Expand All @@ -136,7 +154,7 @@
const errorText = computed(() => {
if (error.value?.statusCode === 404) {
return `Item with ID ${itemId.value} not found`
return `Item with ID ${itemId} not found`
}
return 'Something went wrong'
Expand All @@ -145,16 +163,43 @@
const weight = computed(() => data.value?.equipment.weight ?? 0)
const formattedWeight = computed(() => formatWeight(weight.value))
function onEdit() {
router.push(`/equipment/item/${itemId.value}/edit`)
const isDeleting = ref(false)
const isDeleteDialogOpened = ref(false)
const { showErrorToast } = useApiErrorToast()
async function deleteItem() {
if (isDeleting.value) {
return
}
try {
isDeleting.value = true
await $fetch(`/api/equipment/items/${itemId}`, {
method: 'DELETE'
})
addToast({
title: 'Item deleted',
message: `Item ${itemName.value} has been deleted`
})
await navigateTo('/manager/equipment', {
replace: true
})
} catch (error) {
showErrorToast(error, 'Failed to delete item')
} finally {
isDeleting.value = false
}
}
function onDelete() {
// TODO: Implement deletion
addToast({
title: '¯\\(ツ)/¯',
message: 'This feature is not implemented yet'
})
function showDeleteConfirmation() {
isDeleteDialogOpened.value = true
}
function onEdit() {
router.push(`/equipment/item/${itemId}/edit`)
}
</script>

Expand Down
33 changes: 33 additions & 0 deletions server/api/equipment/items/[itemId]/index.delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { eq } from 'drizzle-orm'
import * as v from 'valibot'

const paramsSchema = v.object({
itemId: stringToIntegerValidator
})

function validateParams(params: unknown) {
return v.parse(paramsSchema, params)
}

export default defineEventHandler(async (event) => {
await validateAdmin(event)
const { itemId } = await getValidatedRouterParams(event, validateParams)

const deleted = await event.context.db
.delete(tables.equipment)
.where(
eq(tables.equipment.id, itemId)
)
.returning({
id: tables.equipment.id
})

if (deleted.length === 0) {
throw createError({
statusCode: 404,
message: `Item with ID ${itemId} not found`
})
}

setResponseStatus(event, 204)
})
File renamed without changes.

0 comments on commit d7ca267

Please sign in to comment.