Added category deletion interface
This commit is contained in:
@@ -2,11 +2,23 @@
|
||||
import type { Category } from "~/models/Category";
|
||||
import { ScrollAreaRoot, ScrollAreaViewport, ScrollAreaScrollbar, ScrollAreaThumb } from "radix-vue"
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { categories } = defineProps<{
|
||||
categories: Category[]
|
||||
}>()
|
||||
|
||||
const sortedCategories = computed(() => categories.toSorted((a, b) => a.name.localeCompare(b.name)))
|
||||
|
||||
const deleteDialogOpened = ref<boolean>(false)
|
||||
|
||||
function openDeleteDialog() {
|
||||
deleteDialogOpened.value = true
|
||||
}
|
||||
|
||||
function closeDeleteDialog() {
|
||||
deleteDialogOpened.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -20,6 +32,7 @@ const sortedCategories = computed(() => categories.toSorted((a, b) => a.name.loc
|
||||
v-for="item in sortedCategories"
|
||||
:key="item.id"
|
||||
:category="item"
|
||||
@on-delete-category="openDeleteDialog"
|
||||
/>
|
||||
</div>
|
||||
</ScrollAreaViewport>
|
||||
@@ -35,4 +48,25 @@ const sortedCategories = computed(() => categories.toSorted((a, b) => a.name.loc
|
||||
|
||||
<CalendarCategoryTableFooter />
|
||||
</div>
|
||||
|
||||
<UiDialog v-model:open="deleteDialogOpened">
|
||||
<UiDialogContent
|
||||
:disable-outside-pointer-events="true"
|
||||
:trap-focus="true"
|
||||
class="min-w-96 border-indigo-200 dark:bg-slate-950 dark:border-indigo-950"
|
||||
@escape-key-down="closeDeleteDialog"
|
||||
@focus-outside="closeDeleteDialog"
|
||||
@interact-outside="closeDeleteDialog"
|
||||
@pointer-down-outside="closeDeleteDialog"
|
||||
>
|
||||
<UiDialogTitle>
|
||||
{{ $t('entity.category.deleteDialog.title') }}
|
||||
</UiDialogTitle>
|
||||
<UiDialogDescription>
|
||||
{{ $t('entity.category.deleteDialog.subtitle') }}
|
||||
</UiDialogDescription>
|
||||
|
||||
<CalendarFormDeleteCategory />
|
||||
</UiDialogContent>
|
||||
</UiDialog>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { PhCheck } from "@phosphor-icons/vue"
|
||||
import { PhCheck, PhTrash } from "@phosphor-icons/vue"
|
||||
import { useToast } from "~/components/ui/toast"
|
||||
import { ToastLifetime } from "~/components/ui/toast/use-toast"
|
||||
import { cn } from "~/lib/utils"
|
||||
@@ -12,12 +12,17 @@ const { category } = defineProps<{
|
||||
category: Category
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "on-delete-category", payload: Category): void
|
||||
}>()
|
||||
|
||||
type RowMode = "edit" | "view"
|
||||
const currentMode = ref<RowMode>("view")
|
||||
|
||||
const rowRef = ref<HTMLDivElement | null>(null)
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
const { focused: inputFocused } = useFocus(inputRef)
|
||||
const rowHovered = useElementHover(rowRef)
|
||||
|
||||
/**
|
||||
* Toggle view mode options
|
||||
@@ -86,6 +91,11 @@ async function submitUpdate() {
|
||||
duration: ToastLifetime.SHORT
|
||||
})
|
||||
}
|
||||
|
||||
function handleQueryDelete() {
|
||||
categorySkeleton.value = structuredClone(toRaw(category))
|
||||
emit("on-delete-category", category)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -148,19 +158,30 @@ async function submitUpdate() {
|
||||
</div>
|
||||
</form>
|
||||
<menu class="w-fit absolute top-1/2 -translate-y-1/2 right-2 flex items-center gap-2">
|
||||
<li>
|
||||
<li
|
||||
v-if="currentMode === 'edit' && categorySkeleton"
|
||||
>
|
||||
<UiButton
|
||||
v-if="currentMode === 'edit' && categorySkeleton"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
class="w-6 h-6 rounded-full bg-emerald-500 hover:bg-emerald-600 text-white"
|
||||
:to="{ name: 'calendar.category.edit', params: { id: category.id } }"
|
||||
:title="$t('ui.actions.edit')"
|
||||
@click="submitUpdate"
|
||||
>
|
||||
<PhCheck size="14" weight="bold" />
|
||||
</UiButton>
|
||||
</li>
|
||||
<li v-else-if="rowHovered">
|
||||
<UiButton
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
class="w-6 h-6 rounded-full hover:bg-red-600 hover:text-white"
|
||||
:title="$t('ui.actions.delete')"
|
||||
@click="handleQueryDelete"
|
||||
>
|
||||
<PhTrash size="14" weight="bold" />
|
||||
</UiButton>
|
||||
</li>
|
||||
</menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { CalendarFormCategories } from "#components";
|
||||
import { PhX } from "@phosphor-icons/vue";
|
||||
|
||||
const { toggleCategoriesModal } = useCalendar()
|
||||
const { isCategoriesModalOpen } = storeToRefs(useCalendar())
|
||||
const { categories, isCategoriesModalOpen } = storeToRefs(useCalendar())
|
||||
|
||||
function handleClosing() {
|
||||
toggleCategoriesModal(false)
|
||||
@@ -36,7 +35,7 @@ function handleClosing() {
|
||||
<PhX size="20" />
|
||||
</UiButton>
|
||||
|
||||
<CalendarFormCategories />
|
||||
<CalendarCategoryTable :categories />
|
||||
</UiAlertDialogContent>
|
||||
</UiAlertDialog>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
const { categories } = storeToRefs(useCalendar())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarCategoryTable :categories />
|
||||
</template>
|
||||
85
components/calendar/form/DeleteCategory.vue
Normal file
85
components/calendar/form/DeleteCategory.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<script lang="ts" setup>
|
||||
import { PhCircleNotch } from "@phosphor-icons/vue";
|
||||
import { useToast } from "~/components/ui/toast";
|
||||
import { ToastLifetime } from "~/components/ui/toast/use-toast";
|
||||
|
||||
const { deleteCategoryFromSkeleton, cancelLatestRequest } = useCategoryStore()
|
||||
const { isDeletingCategory, categorySkeleton } = storeToRefs(useCategoryStore())
|
||||
|
||||
const { toast } = useToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
const isLoading = ref<boolean>(false)
|
||||
|
||||
const formErrors = reactive<{ message: string | null }>({
|
||||
message: null
|
||||
})
|
||||
|
||||
async function handleAction(): Promise<void> {
|
||||
if (isLoading.value && !categorySkeleton.value) return
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
const categoryName = categorySkeleton.value?.name
|
||||
|
||||
try {
|
||||
await deleteCategoryFromSkeleton()
|
||||
|
||||
toast({
|
||||
title: t("entity.calendar.category.deletedToast.title", { category: categoryName }),
|
||||
variant: "success",
|
||||
duration: ToastLifetime.MEDIUM
|
||||
})
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
formErrors.message = err.message
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on the cancel button
|
||||
*
|
||||
* Must cancel the abortController in the store, and stop the loading
|
||||
*/
|
||||
function handleCancel(): void {
|
||||
cancelLatestRequest()
|
||||
isLoading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="handleAction">
|
||||
<div class="grid grid-cols-2 gap-y-4">
|
||||
<div class="text-red-500 ml-8">
|
||||
<span class="text-sm">
|
||||
{{ formErrors.message }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="flex gap-2 justify-between">
|
||||
<UiButton type="button" size="sm" variant="outline" @click="() => isDeletingCategory = false">
|
||||
{{ $t('ui.action.back') }}
|
||||
</UiButton>
|
||||
|
||||
<div class="flex gap-2 justify-end">
|
||||
<Transition name="fade-delay">
|
||||
<UiButton v-if="isLoading" type="button" size="sm" variant="destructive" @click.prevent="handleCancel">
|
||||
{{ $t('ui.action.cancel') }}
|
||||
</UiButton>
|
||||
</Transition>
|
||||
|
||||
<UiButton size="sm" variant="destructive" :disabled="isLoading">
|
||||
<Transition name="fade">
|
||||
<PhCircleNotch v-if="isLoading" size="20" class="animate-spin"/>
|
||||
</Transition>
|
||||
|
||||
{{ $t('ui.action.delete') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</footer>
|
||||
</form>
|
||||
</template>
|
||||
Reference in New Issue
Block a user