Move new-project form to the dashboard; sidebar project list becomes a switcher; Kanban is the default project tab
- DashboardView: a "New project title" form now sits directly below the project grid (creating one stays on the dashboard -- the grid and the sidebar dropdown pick it up via the shared projects store, no navigation). - AppSidebar: the per-project RouterLink list is replaced by a <select>. It's a v-model-bound writable computed (selectedProjectId): the getter tracks route.params.id so it reflects whichever project is open, the setter router.pushes to the chosen one -- so it doubles as a project switcher from anywhere, not just a picker from the dashboard. Removed the now-unused .sidebar__link--project/.sidebar__projects/.sidebar__form CSS; added .sidebar__select, and .form--new-project back for the dashboard form. - ProjectView: activeTab now initialises to 'kanban' instead of 'all'. The tabs array (and so the tab bar's DOM order, All tasks first) is untouched -- only the default selection changed. Verified via headless Chrome: sidebar has no per-project links (a <select> listing both projects instead); the new-project form follows the grid and creating from it stays on /dashboard while the grid and dropdown both update; selecting a project in the dropdown navigates there and the dropdown reflects it once there; a project opens on Kanban with the tab bar still reading "All tasks, Kanban" in that order. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ import { ApiError } from '../lib/api'
|
||||
import { reorderColumn } from '../lib/cardOrder'
|
||||
import { useCardsStore } from '../stores/cards'
|
||||
import { useInboxStore } from '../stores/inbox'
|
||||
import { MAX_PROJECTS, useProjectsStore } from '../stores/projects'
|
||||
import { useProjectsStore } from '../stores/projects'
|
||||
import type { Card } from '../types'
|
||||
import KanbanCard from './KanbanCard.vue'
|
||||
|
||||
@@ -16,13 +16,8 @@ const inbox = useInboxStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const title = ref('')
|
||||
const createError = ref<ApiError | null>(null)
|
||||
const submitting = ref(false)
|
||||
const loadError = ref<string | null>(null)
|
||||
|
||||
const atLimit = computed(() => projects.projects.length >= MAX_PROJECTS)
|
||||
|
||||
onMounted(() => {
|
||||
if (!projects.loaded) void loadProjects()
|
||||
if (!inbox.loaded) void loadInbox()
|
||||
@@ -37,19 +32,14 @@ async function loadProjects() {
|
||||
}
|
||||
}
|
||||
|
||||
async function onCreate() {
|
||||
submitting.value = true
|
||||
createError.value = null
|
||||
try {
|
||||
const project = await projects.createProject(title.value)
|
||||
title.value = ''
|
||||
await router.push({ name: 'project', params: { id: project.id } })
|
||||
} catch (e) {
|
||||
createError.value = e instanceof ApiError ? e : new ApiError('Could not create the project.', 0)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
// The dropdown doubles as a project switcher: it reflects whichever project
|
||||
// (if any) is currently open, and selecting one navigates there.
|
||||
const selectedProjectId = computed<number | ''>({
|
||||
get: () => (route.name === 'project' ? Number(route.params.id) : ''),
|
||||
set: (id) => {
|
||||
if (id !== '') void router.push({ name: 'project', params: { id } })
|
||||
},
|
||||
})
|
||||
|
||||
// --- Inbox: a global holding area, not tied to any project. Cards drag in
|
||||
// and out of it from a project's kanban board (shared "kanban" group).
|
||||
@@ -126,47 +116,19 @@ async function onInboxChange(change: ColumnChange) {
|
||||
<p v-else-if="projects.loading && !projects.loaded" class="sidebar__note muted">Loading…</p>
|
||||
<p v-else-if="projects.projects.length === 0" class="sidebar__note muted">No projects yet.</p>
|
||||
|
||||
<nav v-else class="sidebar__nav sidebar__projects">
|
||||
<RouterLink
|
||||
v-for="project in projects.projects"
|
||||
:key="project.id"
|
||||
:to="{ name: 'project', params: { id: project.id } }"
|
||||
class="sidebar__link sidebar__link--project"
|
||||
active-class="sidebar__link--active"
|
||||
>
|
||||
<svg class="sidebar__icon" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<rect x="1" y="2" width="3.5" height="12" rx="1" />
|
||||
<rect x="6.25" y="2" width="3.5" height="9" rx="1" />
|
||||
<rect x="11.5" y="2" width="3.5" height="6" rx="1" />
|
||||
</svg>
|
||||
<span class="sidebar__link-text">{{ project.title }}</span>
|
||||
</RouterLink>
|
||||
</nav>
|
||||
|
||||
<form class="sidebar__form" @submit.prevent="onCreate">
|
||||
<input
|
||||
v-model="title"
|
||||
type="text"
|
||||
maxlength="255"
|
||||
required
|
||||
:disabled="atLimit"
|
||||
placeholder="New project"
|
||||
aria-label="New project title"
|
||||
/>
|
||||
<small v-if="createError?.fieldError('title')" class="field-error">
|
||||
{{ createError.fieldError('title') }}
|
||||
</small>
|
||||
<small
|
||||
v-else-if="createError && Object.keys(createError.details).length === 0"
|
||||
class="field-error"
|
||||
>
|
||||
{{ createError.message }}
|
||||
</small>
|
||||
<button type="submit" :disabled="submitting || atLimit">
|
||||
{{ submitting ? 'Creating…' : 'Add project' }}
|
||||
</button>
|
||||
<small v-if="atLimit" class="hint">Limit of {{ MAX_PROJECTS }} projects reached.</small>
|
||||
</form>
|
||||
<div v-else class="sidebar__select">
|
||||
<svg class="sidebar__icon" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<rect x="1" y="2" width="3.5" height="12" rx="1" />
|
||||
<rect x="6.25" y="2" width="3.5" height="9" rx="1" />
|
||||
<rect x="11.5" y="2" width="3.5" height="6" rx="1" />
|
||||
</svg>
|
||||
<select v-model="selectedProjectId" aria-label="Go to project">
|
||||
<option value="" disabled>Choose a project…</option>
|
||||
<option v-for="project in projects.projects" :key="project.id" :value="project.id">
|
||||
{{ project.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<hr class="sidebar__divider" />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user