Files
project-manager/web/src/components/CardManageMenu.vue
T
aneurinandClaude Sonnet 5 55fa9fef56 Split card editing/delete out into a CardConfigureView, mirroring projects
CardView is now just the read view -- header (inline back arrow +
card text) and its status badge, no tabs (there's nothing to tab
between). New CardConfigureView (/cards/:id/configure) holds the
"Edit text" form that used to live inline in CardView.

New CardManageMenu, mirroring ProjectManageMenu: a "Manage" dropdown
with "Configure" (hidden while already on the configure view) and
"Delete card" (with its own confirmation modal). Both CardView and
CardConfigureView use it in their header actions, same as the project
view and its own configure view.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 00:29:06 +01:00

125 lines
4.0 KiB
Vue

<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>