Make Explore and Kanban real routes, not tab state
/projects/:id (name "project") is now Explore; /projects/:id/kanban (name "project-kanban") is Kanban. ProjectView.vue becomes a layout: header + sub-nav, loading the project and its cards (both children need the cards list) and rendering the active one via <RouterView>. ProjectExploreView.vue and ProjectKanbanView.vue hold what used to be each tab's own template/logic; Kanban additionally loads its own statuses, since Explore has no use for them. The sub-nav is now RouterLinks (active state matched on route.name), not buttons toggling local state. App.vue: the top-level <RouterView> was keyed by the full route path to force a fresh instance per project/card id -- with Explore/Kanban now separate paths under one layout, that would also remount the layout (and re-fetch the project) on every tab switch. Keyed by the matched route's top-level path + params instead, which is the same value for both of a project's child routes. AppSidebar: the project switcher and the inbox-drag refresh both used to check route.name === 'project' for "this project is open" -- fixed to cover both routes for the switcher, and narrowed to 'project-kanban' specifically for the inbox-drag refresh, since Kanban is the only route with a draggable list a card could have moved to/from. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import CardRow from '../components/CardRow.vue'
|
||||
import { ApiError } from '../lib/api'
|
||||
import { useCardsStore } from '../stores/cards'
|
||||
|
||||
// The project's cards, flat and sorted by name -- ProjectView (the parent
|
||||
// layout) has already loaded them into this store, keyed to the current
|
||||
// project, so there's nothing to fetch here.
|
||||
const cards = useCardsStore()
|
||||
|
||||
const actionError = ref<string | null>(null)
|
||||
const newText = ref('')
|
||||
const submitting = ref(false)
|
||||
|
||||
const summary = computed(() => {
|
||||
const total = cards.cards.length
|
||||
if (total === 0) return 'No cards yet.'
|
||||
return `${total} card${total === 1 ? '' : 's'}.`
|
||||
})
|
||||
|
||||
// No manual order here -- sort by name, case-insensitively.
|
||||
const sortedCards = computed(() =>
|
||||
[...cards.cards].sort((a, b) => a.text.localeCompare(b.text, undefined, { sensitivity: 'base' })),
|
||||
)
|
||||
|
||||
/** Run a store mutation, surfacing failures and resyncing from the server. */
|
||||
async function run(op: Promise<unknown>) {
|
||||
actionError.value = null
|
||||
try {
|
||||
await op
|
||||
} catch (e) {
|
||||
actionError.value = e instanceof ApiError ? e.message : 'Something went wrong.'
|
||||
if (cards.projectId !== null) await cards.load(cards.projectId)
|
||||
}
|
||||
}
|
||||
|
||||
async function onCreate() {
|
||||
submitting.value = true
|
||||
actionError.value = null
|
||||
try {
|
||||
await cards.add(newText.value)
|
||||
newText.value = ''
|
||||
} catch (e) {
|
||||
actionError.value = e instanceof ApiError ? e.message : 'Could not add the card.'
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p v-if="actionError" class="form-error">{{ actionError }}</p>
|
||||
|
||||
<p class="muted">{{ summary }}</p>
|
||||
|
||||
<p v-if="cards.loading && !cards.loaded" class="muted">Loading…</p>
|
||||
|
||||
<ul v-else-if="sortedCards.length" class="cards">
|
||||
<CardRow
|
||||
v-for="card in sortedCards"
|
||||
:key="card.id"
|
||||
:card="card"
|
||||
@delete="run(cards.remove(card))"
|
||||
/>
|
||||
</ul>
|
||||
|
||||
<form class="kanban__new form--new-card" @submit.prevent="onCreate">
|
||||
<input
|
||||
v-model="newText"
|
||||
class="field field--compact"
|
||||
type="text"
|
||||
maxlength="1000"
|
||||
required
|
||||
placeholder="New card"
|
||||
aria-label="New card"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn-icon"
|
||||
:disabled="submitting"
|
||||
:title="submitting ? 'Adding…' : 'Add card'"
|
||||
:aria-label="submitting ? 'Adding…' : 'Add card'"
|
||||
>+</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user