Add tab/project quick-switch dropdowns to the project breadcrumb
The project header breadcrumb now always shows the active tab (previously Overview was hidden), and both the project-name and tab-level crumbs become dropdowns: project name lists all projects (with a checkmark on the active one, plus "All Projects"), and the tab crumb lists Overview/Shots/Assets/Tasks/Schedule/Settings with a checkmark on the current tab. The trailing ">" separator is skipped after any crumb that renders as a dropdown. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,13 +8,53 @@
|
||||
<Breadcrumb class="flex-1">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem v-for="(crumb, index) in breadcrumbs" :key="index">
|
||||
<BreadcrumbLink v-if="crumb.href" :href="crumb.href">
|
||||
<DropdownMenu v-if="crumb.isProjectCrumb && projectsForSwitcher.length > 0">
|
||||
<DropdownMenuTrigger
|
||||
class="flex items-center gap-1 hover:text-foreground transition-colors outline-none"
|
||||
:class="{ 'font-semibold text-foreground': crumb.isActive }"
|
||||
>
|
||||
{{ crumb.label }}
|
||||
<ChevronDown class="h-3 w-3" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem v-for="project in projectsForSwitcher" :key="project.id" as-child>
|
||||
<router-link :to="`/projects/${project.id}`" class="flex items-center justify-between gap-4 w-full">
|
||||
{{ project.name }}
|
||||
<Check v-if="String(project.id) === projectIdParam" class="h-4 w-4 flex-shrink-0" />
|
||||
</router-link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem as-child>
|
||||
<router-link to="/projects">All Projects</router-link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownMenu v-else-if="crumb.isTabCrumb && projectIdParam">
|
||||
<DropdownMenuTrigger
|
||||
class="flex items-center gap-1 hover:text-foreground transition-colors outline-none"
|
||||
:class="{ 'font-semibold text-foreground': crumb.isActive }"
|
||||
>
|
||||
{{ crumb.label }}
|
||||
<ChevronDown class="h-3 w-3" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem v-for="tabItem in projectTabItems" :key="tabItem.tab" as-child>
|
||||
<router-link :to="`/projects/${projectIdParam}${tabItem.path}`" class="flex items-center justify-between gap-4 w-full">
|
||||
{{ tabItem.label }}
|
||||
<Check v-if="currentTab === tabItem.tab" class="h-4 w-4 flex-shrink-0" />
|
||||
</router-link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<BreadcrumbLink v-else-if="crumb.href" :href="crumb.href">
|
||||
{{ crumb.label }}
|
||||
</BreadcrumbLink>
|
||||
<BreadcrumbPage v-else :class="{ 'font-semibold': crumb.isActive }">
|
||||
{{ crumb.label }}
|
||||
</BreadcrumbPage>
|
||||
<BreadcrumbSeparator v-if="index < breadcrumbs.length - 1" />
|
||||
<BreadcrumbSeparator v-if="index < breadcrumbs.length - 1 && !isDropdownCrumb(crumb)" />
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
@@ -76,7 +116,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { SidebarTrigger } from '@/components/ui/sidebar'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
@@ -97,10 +137,11 @@ import {
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from '@/components/ui/breadcrumb'
|
||||
import { User, Settings, LogOut } from 'lucide-vue-next'
|
||||
import { User, Settings, LogOut, ChevronDown, Check } from 'lucide-vue-next'
|
||||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useProjectsStore } from '@/stores/projects'
|
||||
import { BreadcrumbService, type BreadcrumbItem as BreadcrumbData } from '@/services/breadcrumb'
|
||||
import ThemeToggle from '@/components/ui/theme/ThemeToggle.vue'
|
||||
import NotificationCenter from './NotificationCenter.vue'
|
||||
@@ -123,6 +164,42 @@ const { getAvatarUrl } = useAvatarUrl()
|
||||
// Generate breadcrumbs based on current route with enhanced context
|
||||
const breadcrumbs = ref<BreadcrumbData[]>([])
|
||||
|
||||
// The current tab's breadcrumb (Overview/Shots/Assets/...) becomes a quick-nav
|
||||
// dropdown instead of a plain link/label - see BreadcrumbItem.isTabCrumb.
|
||||
const projectIdParam = computed(() => {
|
||||
const id = route.params.projectId
|
||||
return typeof id === 'string' ? id : Array.isArray(id) ? id[0] : null
|
||||
})
|
||||
|
||||
const currentTab = computed(() => route.meta?.tab as string | undefined)
|
||||
|
||||
const projectTabItems = [
|
||||
{ tab: 'overview', label: 'Overview', path: '' },
|
||||
{ tab: 'shots', label: 'Shots', path: '/shots' },
|
||||
{ tab: 'assets', label: 'Assets', path: '/assets' },
|
||||
{ tab: 'tasks', label: 'Tasks', path: '/tasks' },
|
||||
{ tab: 'schedule', label: 'Schedule', path: '/schedule' },
|
||||
{ tab: 'settings', label: 'Settings', path: '/settings' }
|
||||
]
|
||||
|
||||
// Project-name breadcrumb becomes a project-switcher dropdown - see BreadcrumbItem.isProjectCrumb.
|
||||
const projectsStore = useProjectsStore()
|
||||
const projectsForSwitcher = computed(() => projectsStore.projects)
|
||||
|
||||
onMounted(() => {
|
||||
if (projectsStore.projects.length === 0 && !projectsStore.isLoading) {
|
||||
projectsStore.fetchProjects()
|
||||
}
|
||||
})
|
||||
|
||||
// Mirrors the v-if conditions that actually render a crumb as a dropdown,
|
||||
// so the trailing ">" separator can be skipped for it.
|
||||
function isDropdownCrumb(crumb: BreadcrumbData): boolean {
|
||||
if (crumb.isProjectCrumb) return projectsForSwitcher.value.length > 0
|
||||
if (crumb.isTabCrumb) return !!projectIdParam.value
|
||||
return false
|
||||
}
|
||||
|
||||
const updateBreadcrumbs = async () => {
|
||||
try {
|
||||
breadcrumbs.value = await BreadcrumbService.generateBreadcrumbs(route)
|
||||
|
||||
@@ -7,6 +7,10 @@ export interface BreadcrumbItem {
|
||||
label: string
|
||||
href?: string
|
||||
isActive?: boolean
|
||||
/** True for the crumb representing the current project tab (Overview/Shots/Assets/...), so the header can render a tab-switcher dropdown on it. */
|
||||
isTabCrumb?: boolean
|
||||
/** True for the crumb representing the current project name, so the header can render a project-switcher dropdown on it. */
|
||||
isProjectCrumb?: boolean
|
||||
}
|
||||
|
||||
export class BreadcrumbService {
|
||||
@@ -33,7 +37,8 @@ export class BreadcrumbService {
|
||||
// Add project breadcrumb
|
||||
crumbs.push({
|
||||
label: project ? project.name : `Project ${projectId}`,
|
||||
href: `/projects/${projectId}`
|
||||
href: `/projects/${projectId}`,
|
||||
isProjectCrumb: true
|
||||
})
|
||||
|
||||
// Handle tab-based navigation
|
||||
@@ -45,7 +50,8 @@ export class BreadcrumbService {
|
||||
if (tab === 'shots' && route.params.episodeId) {
|
||||
crumbs.push({
|
||||
label: tabLabel,
|
||||
href: `/projects/${projectId}/shots`
|
||||
href: `/projects/${projectId}/shots`,
|
||||
isTabCrumb: true
|
||||
})
|
||||
|
||||
// Add episode context
|
||||
@@ -66,11 +72,12 @@ export class BreadcrumbService {
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (pathSegments[2] || tab !== 'overview') {
|
||||
// Regular tab navigation (don't show Overview in breadcrumbs unless explicitly navigated to)
|
||||
} else {
|
||||
// Regular tab navigation (Overview included, so the trail always reads Home > Project > Tab)
|
||||
crumbs.push({
|
||||
label: tabLabel,
|
||||
isActive: true
|
||||
isActive: true,
|
||||
isTabCrumb: true
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -85,7 +92,8 @@ export class BreadcrumbService {
|
||||
if (crumbs.length > 1) {
|
||||
crumbs[crumbs.length - 1] = {
|
||||
label: 'Shots',
|
||||
href: `/projects/${projectId}/shots`
|
||||
href: `/projects/${projectId}/shots`,
|
||||
isTabCrumb: true
|
||||
}
|
||||
}
|
||||
crumbs.push({
|
||||
|
||||
Reference in New Issue
Block a user