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">
|
<Breadcrumb class="flex-1">
|
||||||
<BreadcrumbList>
|
<BreadcrumbList>
|
||||||
<BreadcrumbItem v-for="(crumb, index) in breadcrumbs" :key="index">
|
<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 }}
|
{{ crumb.label }}
|
||||||
</BreadcrumbLink>
|
</BreadcrumbLink>
|
||||||
<BreadcrumbPage v-else :class="{ 'font-semibold': crumb.isActive }">
|
<BreadcrumbPage v-else :class="{ 'font-semibold': crumb.isActive }">
|
||||||
{{ crumb.label }}
|
{{ crumb.label }}
|
||||||
</BreadcrumbPage>
|
</BreadcrumbPage>
|
||||||
<BreadcrumbSeparator v-if="index < breadcrumbs.length - 1" />
|
<BreadcrumbSeparator v-if="index < breadcrumbs.length - 1 && !isDropdownCrumb(crumb)" />
|
||||||
</BreadcrumbItem>
|
</BreadcrumbItem>
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
@@ -76,7 +116,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { SidebarTrigger } from '@/components/ui/sidebar'
|
import { SidebarTrigger } from '@/components/ui/sidebar'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
@@ -97,10 +137,11 @@ import {
|
|||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator,
|
BreadcrumbSeparator,
|
||||||
} from '@/components/ui/breadcrumb'
|
} 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 { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { useProjectsStore } from '@/stores/projects'
|
||||||
import { BreadcrumbService, type BreadcrumbItem as BreadcrumbData } from '@/services/breadcrumb'
|
import { BreadcrumbService, type BreadcrumbItem as BreadcrumbData } from '@/services/breadcrumb'
|
||||||
import ThemeToggle from '@/components/ui/theme/ThemeToggle.vue'
|
import ThemeToggle from '@/components/ui/theme/ThemeToggle.vue'
|
||||||
import NotificationCenter from './NotificationCenter.vue'
|
import NotificationCenter from './NotificationCenter.vue'
|
||||||
@@ -123,6 +164,42 @@ const { getAvatarUrl } = useAvatarUrl()
|
|||||||
// Generate breadcrumbs based on current route with enhanced context
|
// Generate breadcrumbs based on current route with enhanced context
|
||||||
const breadcrumbs = ref<BreadcrumbData[]>([])
|
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 () => {
|
const updateBreadcrumbs = async () => {
|
||||||
try {
|
try {
|
||||||
breadcrumbs.value = await BreadcrumbService.generateBreadcrumbs(route)
|
breadcrumbs.value = await BreadcrumbService.generateBreadcrumbs(route)
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ export interface BreadcrumbItem {
|
|||||||
label: string
|
label: string
|
||||||
href?: string
|
href?: string
|
||||||
isActive?: boolean
|
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 {
|
export class BreadcrumbService {
|
||||||
@@ -33,7 +37,8 @@ export class BreadcrumbService {
|
|||||||
// Add project breadcrumb
|
// Add project breadcrumb
|
||||||
crumbs.push({
|
crumbs.push({
|
||||||
label: project ? project.name : `Project ${projectId}`,
|
label: project ? project.name : `Project ${projectId}`,
|
||||||
href: `/projects/${projectId}`
|
href: `/projects/${projectId}`,
|
||||||
|
isProjectCrumb: true
|
||||||
})
|
})
|
||||||
|
|
||||||
// Handle tab-based navigation
|
// Handle tab-based navigation
|
||||||
@@ -45,7 +50,8 @@ export class BreadcrumbService {
|
|||||||
if (tab === 'shots' && route.params.episodeId) {
|
if (tab === 'shots' && route.params.episodeId) {
|
||||||
crumbs.push({
|
crumbs.push({
|
||||||
label: tabLabel,
|
label: tabLabel,
|
||||||
href: `/projects/${projectId}/shots`
|
href: `/projects/${projectId}/shots`,
|
||||||
|
isTabCrumb: true
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add episode context
|
// Add episode context
|
||||||
@@ -66,11 +72,12 @@ export class BreadcrumbService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (pathSegments[2] || tab !== 'overview') {
|
} else {
|
||||||
// Regular tab navigation (don't show Overview in breadcrumbs unless explicitly navigated to)
|
// Regular tab navigation (Overview included, so the trail always reads Home > Project > Tab)
|
||||||
crumbs.push({
|
crumbs.push({
|
||||||
label: tabLabel,
|
label: tabLabel,
|
||||||
isActive: true
|
isActive: true,
|
||||||
|
isTabCrumb: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,7 +92,8 @@ export class BreadcrumbService {
|
|||||||
if (crumbs.length > 1) {
|
if (crumbs.length > 1) {
|
||||||
crumbs[crumbs.length - 1] = {
|
crumbs[crumbs.length - 1] = {
|
||||||
label: 'Shots',
|
label: 'Shots',
|
||||||
href: `/projects/${projectId}/shots`
|
href: `/projects/${projectId}/shots`,
|
||||||
|
isTabCrumb: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
crumbs.push({
|
crumbs.push({
|
||||||
|
|||||||
Reference in New Issue
Block a user