Files
LinkDesk/frontend/src/services/breadcrumb.ts
T
indigo 04c85be0f7 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>
2026-07-20 23:22:35 +08:00

149 lines
5.2 KiB
TypeScript

import type { RouteLocationNormalized } from 'vue-router'
import { useProjectsStore } from '@/stores/projects'
import { episodeService } from '@/services/episode'
import { shotService } from '@/services/shot'
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 {
static async generateBreadcrumbs(route: RouteLocationNormalized): Promise<BreadcrumbItem[]> {
const pathSegments = route.path.split('/').filter(Boolean)
const crumbs: BreadcrumbItem[] = [{ label: 'Home', href: '/' }]
// Handle different route patterns
if (pathSegments[0] === 'projects' && pathSegments[1]) {
const projectId = parseInt(pathSegments[1])
if (!isNaN(projectId)) {
// Get project information - initialize store here when needed
const projectsStore = useProjectsStore()
let project = projectsStore.getProjectById(projectId)
if (!project) {
try {
project = await projectsStore.getProject(projectId)
} catch (error) {
console.error('Failed to load project for breadcrumbs:', error)
}
}
// Add project breadcrumb
crumbs.push({
label: project ? project.name : `Project ${projectId}`,
href: `/projects/${projectId}`,
isProjectCrumb: true
})
// Handle tab-based navigation
const tab = route.meta?.tab as string
const tabLabel = route.meta?.tabLabel as string || this.getTabLabel(tab)
if (tab) {
// For shots tab with episode context
if (tab === 'shots' && route.params.episodeId) {
crumbs.push({
label: tabLabel,
href: `/projects/${projectId}/shots`,
isTabCrumb: true
})
// Add episode context
const episodeId = parseInt(route.params.episodeId as string)
if (!isNaN(episodeId)) {
try {
const episodes = await episodeService.getProjectEpisodes(projectId)
const episode = episodes.find(ep => ep.id === episodeId)
crumbs.push({
label: episode ? episode.name : `Episode ${episodeId}`,
isActive: true
})
} catch (error) {
console.error('Failed to load episode for breadcrumbs:', error)
crumbs.push({
label: `Episode ${episodeId}`,
isActive: true
})
}
}
} else {
// Regular tab navigation (Overview included, so the trail always reads Home > Project > Tab)
crumbs.push({
label: tabLabel,
isActive: true,
isTabCrumb: true
})
}
}
// Handle shot detail page (path: /projects/:projectId/shots/:shotId)
if (pathSegments[2] === 'shots' && pathSegments[3]) {
const shotId = parseInt(pathSegments[3])
if (!isNaN(shotId)) {
try {
const shot = await shotService.getShot(shotId)
// Replace the last item (Shots) with Shots > ShotName
if (crumbs.length > 1) {
crumbs[crumbs.length - 1] = {
label: 'Shots',
href: `/projects/${projectId}/shots`,
isTabCrumb: true
}
}
crumbs.push({
label: shot.name,
isActive: true
})
} catch (error) {
console.error('Failed to load shot for breadcrumbs:', error)
// Keep the default behavior - show shot ID
}
}
}
}
} else {
// Handle other routes
let currentPath = ''
pathSegments.forEach((segment, index) => {
currentPath += `/${segment}`
const isLast = index === pathSegments.length - 1
// Format segment label
const label = this.formatSegmentLabel(segment)
crumbs.push({
label,
href: isLast ? undefined : currentPath,
isActive: isLast
})
})
}
return crumbs
}
private static getTabLabel(tab: string): string {
switch (tab) {
case 'overview': return 'Overview'
case 'shots': return 'Shots'
case 'assets': return 'Assets'
case 'technical-specs': return 'Technical Specs'
default: return tab.charAt(0).toUpperCase() + tab.slice(1).replace(/-/g, ' ')
}
}
private static formatSegmentLabel(segment: string): string {
// Handle special cases
if (segment === 'api-keys') return 'API Keys'
if (segment === 'technical-specs') return 'Technical Specs'
// Default formatting
return segment.charAt(0).toUpperCase() + segment.slice(1).replace(/-/g, ' ')
}
}