Add per-project sub-navigation to the sidebar's Projects nav item
Inside a specific project, the "Projects" item in the sidebar now shows Overview/Shots/Assets/Tasks/Settings as sub-links (mirroring the existing top-of-page ProjectTabs), with the active tab highlighted to match the current route. When the sidebar is collapsed to icons, the Projects icon still links straight to /projects like any other nav item; hovering it reveals the same 5 sub-links as a flyout menu (a controlled Popover, not a DropdownMenu, so its own click-to-open behavior doesn't hijack the icon's normal navigation click). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,12 +27,65 @@
|
||||
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem v-for="item in navigationItems" :key="item.title">
|
||||
<SidebarMenuButton as-child :tooltip="isCollapsed ? item.title : undefined">
|
||||
<router-link :to="item.url" class="flex items-center gap-2">
|
||||
<component :is="item.icon" class="size-4" />
|
||||
<span class="group-data-[collapsible=icon]:hidden">{{ item.title }}</span>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
<!-- Collapsed sidebar + Projects (with an active project): hovering reveals sub-links,
|
||||
while the icon itself still navigates to /projects like any other nav item. -->
|
||||
<div
|
||||
v-if="item.title === 'Projects' && isCollapsed && projectTabs.length > 0"
|
||||
@mouseenter="showProjectFlyout = true"
|
||||
@mouseleave="showProjectFlyout = false"
|
||||
>
|
||||
<Popover :open="showProjectFlyout">
|
||||
<PopoverAnchor>
|
||||
<SidebarMenuButton as-child :title="item.title">
|
||||
<router-link :to="item.url" class="flex items-center gap-2">
|
||||
<component :is="item.icon" class="size-4" />
|
||||
<span class="group-data-[collapsible=icon]:hidden">{{ item.title }}</span>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
</PopoverAnchor>
|
||||
<PopoverContent
|
||||
side="right"
|
||||
align="start"
|
||||
class="w-48 p-1"
|
||||
@open-auto-focus.prevent
|
||||
@mouseenter="showProjectFlyout = true"
|
||||
@mouseleave="showProjectFlyout = false"
|
||||
>
|
||||
<router-link
|
||||
v-for="tab in projectTabs"
|
||||
:key="tab.id"
|
||||
:to="tab.route"
|
||||
class="flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground"
|
||||
:class="activeProjectTab === tab.id && 'bg-accent text-accent-foreground'"
|
||||
@click="showProjectFlyout = false"
|
||||
>
|
||||
<component :is="tab.icon" class="size-4" />
|
||||
<span>{{ tab.label }}</span>
|
||||
</router-link>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<SidebarMenuButton as-child :tooltip="isCollapsed ? item.title : undefined">
|
||||
<router-link :to="item.url" class="flex items-center gap-2">
|
||||
<component :is="item.icon" class="size-4" />
|
||||
<span class="group-data-[collapsible=icon]:hidden">{{ item.title }}</span>
|
||||
</router-link>
|
||||
</SidebarMenuButton>
|
||||
|
||||
<!-- Project sub-navigation - shown under "Projects" when inside a specific project -->
|
||||
<SidebarMenuSub v-if="item.title === 'Projects' && projectTabs.length > 0">
|
||||
<SidebarMenuSubItem v-for="tab in projectTabs" :key="tab.id">
|
||||
<SidebarMenuSubButton as-child :is-active="activeProjectTab === tab.id">
|
||||
<router-link :to="tab.route" class="flex items-center gap-2">
|
||||
<component :is="tab.icon" class="size-4" />
|
||||
<span class="truncate">{{ tab.label }}</span>
|
||||
</router-link>
|
||||
</SidebarMenuSubButton>
|
||||
</SidebarMenuSubItem>
|
||||
</SidebarMenuSub>
|
||||
</template>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
@@ -93,33 +146,41 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover'
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
useSidebar,
|
||||
SidebarProps
|
||||
} from '@/components/ui/sidebar'
|
||||
import {
|
||||
Clapperboard,
|
||||
Home,
|
||||
CheckSquare,
|
||||
FolderOpen,
|
||||
Users,
|
||||
Settings,
|
||||
import {
|
||||
Clapperboard,
|
||||
Home,
|
||||
CheckSquare,
|
||||
FolderOpen,
|
||||
Users,
|
||||
Settings,
|
||||
Folder,
|
||||
Key,
|
||||
Database,
|
||||
BarChart3,
|
||||
RotateCcw,
|
||||
LayoutDashboard,
|
||||
Camera,
|
||||
Package,
|
||||
ListTodo,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
@@ -131,6 +192,7 @@ const authStore = useAuthStore()
|
||||
const { isCoordinatorOrAdmin } = usePermission()
|
||||
const { state } = useSidebar()
|
||||
const route = useRoute()
|
||||
const showProjectFlyout = ref(false)
|
||||
const user = computed(() => authStore.user)
|
||||
const userRole = computed(() => authStore.user?.role || 'artist')
|
||||
|
||||
@@ -142,6 +204,38 @@ const isOnShotPage = computed(() => {
|
||||
return route.path.endsWith('/shots') || route.path.includes('/project/')
|
||||
})
|
||||
|
||||
// The project id when on a specific project's page (null on /projects, /projects/new, etc.)
|
||||
const currentProjectId = computed(() => {
|
||||
const id = route.params.projectId
|
||||
const parsed = typeof id === 'string' ? parseInt(id) : NaN
|
||||
return isNaN(parsed) ? null : parsed
|
||||
})
|
||||
|
||||
// Sub-nav items shown under the "Projects" nav item when inside a specific project
|
||||
const projectTabs = computed(() => {
|
||||
const id = currentProjectId.value
|
||||
if (id === null) return []
|
||||
return [
|
||||
{ id: 'overview', label: 'Overview', icon: LayoutDashboard, route: `/projects/${id}` },
|
||||
{ id: 'shots', label: 'Shots', icon: Camera, route: `/projects/${id}/shots` },
|
||||
{ id: 'assets', label: 'Assets', icon: Package, route: `/projects/${id}/assets` },
|
||||
{ id: 'tasks', label: 'Tasks', icon: ListTodo, route: `/projects/${id}/tasks` },
|
||||
{ id: 'settings', label: 'Settings', icon: Settings, route: `/projects/${id}/settings` },
|
||||
]
|
||||
})
|
||||
|
||||
const activeProjectTab = computed(() => {
|
||||
const id = currentProjectId.value
|
||||
if (id === null) return null
|
||||
const path = route.path
|
||||
if (path === `/projects/${id}`) return 'overview'
|
||||
if (path.startsWith(`/projects/${id}/shots`)) return 'shots'
|
||||
if (path.startsWith(`/projects/${id}/assets`)) return 'assets'
|
||||
if (path.startsWith(`/projects/${id}/tasks`)) return 'tasks'
|
||||
if (path.startsWith(`/projects/${id}/settings`)) return 'settings'
|
||||
return null
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<SidebarProps>(), {
|
||||
collapsible: "icon",
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user