04c85be0f7
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>
149 lines
5.2 KiB
TypeScript
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, ' ')
|
|
}
|
|
} |