Files
project-manager/web/src/components/CardManageMenu.vue
T

125 lines
4.0 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useDialog } from '../composables/useDialog'
import { ApiError, apiRequest } from '../lib/api'
import { useCardsStore } from '../stores/cards'
import { useInboxStore } from '../stores/inbox'
// The "Manage" dropdown + its delete-card confirmation modal, shared by the
// card view and its configuration view -- each links to the other, and
// either can delete the card. Mirrors ProjectManageMenu.
const props = defineProps<{
cardId: number
cardText: string
/** null for an inbox card -- there's no project to return to, so a delete
* (or the "Back to project" link on the pages using this menu) goes to
* the dashboard instead. */
projectId: number | null
}>()
const route = useRoute()
const router = useRouter()
const cards = useCardsStore()
const inbox = useInboxStore()
const menuOpen = ref(false)
const confirmingDelete = ref(false)
const deleting = ref(false)
const deleteError = ref<string | null>(null)
const cancelButton = ref<HTMLButtonElement>()
function askDelete() {
menuOpen.value = false
deleteError.value = null
confirmingDelete.value = true
}
async function confirmDelete() {
deleting.value = true
deleteError.value = null
try {
await apiRequest(`/cards/${props.cardId}`, { method: 'DELETE', auth: true })
// Refresh whichever store holds this card -- a project's board, or the
// sidebar inbox -- so it no longer shows the card we just deleted.
await (props.projectId !== null ? cards.load(props.projectId) : inbox.load())
await router.push(
props.projectId !== null ? { name: 'project', params: { id: props.projectId } } : { name: 'dashboard' },
)
} catch (e) {
deleteError.value = e instanceof ApiError ? e.message : 'Could not delete the card.'
deleting.value = false
}
}
// The modal takes priority: while it's open, Escape closes it, not the menu
// underneath (askDelete() already closes the menu when the modal opens, so
// the two are never both open at once).
useDialog(confirmingDelete, () => (confirmingDelete.value = false), cancelButton)
useDialog(menuOpen, () => (menuOpen.value = false))
</script>
<template>
<div class="menu">
<button
type="button"
class="menu__toggle"
aria-haspopup="true"
:aria-expanded="menuOpen"
@click="menuOpen = !menuOpen"
>
Manage &#9662;
</button>
<template v-if="menuOpen">
<div class="menu__backdrop" @click="menuOpen = false" />
<ul class="menu__list" role="menu">
<li v-if="route.name !== 'card-configure'" role="none">
<RouterLink
:to="{ name: 'card-configure', params: { id: cardId } }"
role="menuitem"
class="menu__item"
@click="menuOpen = false"
>
Configure
</RouterLink>
</li>
<li role="none">
<button type="button" role="menuitem" class="menu__item menu__item--danger" @click="askDelete">
Delete card
</button>
</li>
</ul>
</template>
</div>
<div
v-if="confirmingDelete"
class="modal"
role="dialog"
aria-modal="true"
aria-labelledby="confirm-delete-title"
>
<div class="modal__backdrop" @click="confirmingDelete = false" />
<div class="modal__dialog">
<h2 id="confirm-delete-title">Delete this card?</h2>
<p class="muted">&ldquo;{{ cardText }}&rdquo; will be permanently deleted.</p>
<p v-if="deleteError" class="form-error">{{ deleteError }}</p>
<div class="modal__actions">
<button
ref="cancelButton"
type="button"
class="btn-secondary"
:disabled="deleting"
@click="confirmingDelete = false"
>
Cancel
</button>
<button type="button" class="btn-danger" :disabled="deleting" @click="confirmDelete">
{{ deleting ? 'Deleting…' : 'Delete card' }}
</button>
</div>
</div>
</div>
</template>