Skip to content

Commit 335715b

Browse files
committed
refactor(frontend): migrate admin teams detail page to route-based navigation
Convert admin teams detail page from query parameter-based sections (?section=limits) to route-based navigation (/admin/teams/:id/limits) following established patterns. Changes: - Add useTeamDetailCache composable for instant tab switching without heading flash - Create TeamDetailTabs component with SettingsMenu for left sidebar navigation - Create TeamDetailPageHeading component with breadcrumb support - Split view into separate files: general.vue, limits.vue, members.vue - Update router with new tab routes and redirect from base path to /general - Remove old query-based [id].vue view file
1 parent 765b455 commit 335715b

File tree

9 files changed

+482
-178
lines changed

9 files changed

+482
-178
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<script setup lang="ts">
2+
import { useI18n } from 'vue-i18n'
3+
import { DsPageHeading } from '@/components/ui/ds-page-heading'
4+
import { Skeleton } from '@/components/ui/skeleton'
5+
import {
6+
Breadcrumb,
7+
BreadcrumbItem,
8+
BreadcrumbLink,
9+
BreadcrumbList,
10+
BreadcrumbPage,
11+
BreadcrumbSeparator,
12+
} from '@/components/ui/breadcrumb'
13+
import type { Team } from '@/views/admin/teams/types'
14+
15+
interface Props {
16+
team: Team | null
17+
isLoading?: boolean
18+
}
19+
20+
defineProps<Props>()
21+
const { t } = useI18n()
22+
</script>
23+
24+
<template>
25+
<!-- When team is loaded -->
26+
<DsPageHeading v-if="team" :title="team.name" :show-border="false">
27+
<Breadcrumb>
28+
<BreadcrumbList>
29+
<BreadcrumbItem>
30+
<BreadcrumbLink as-child>
31+
<RouterLink to="/admin/teams">
32+
{{ t('adminTeams.title') }}
33+
</RouterLink>
34+
</BreadcrumbLink>
35+
</BreadcrumbItem>
36+
<BreadcrumbSeparator />
37+
<BreadcrumbItem>
38+
<BreadcrumbPage>{{ team.name }}</BreadcrumbPage>
39+
</BreadcrumbItem>
40+
</BreadcrumbList>
41+
</Breadcrumb>
42+
</DsPageHeading>
43+
44+
<!-- Loading state (shows skeleton breadcrumb) -->
45+
<DsPageHeading v-else :title="t('adminTeams.teamDetail.titleLoading')" :show-border="false">
46+
<Breadcrumb>
47+
<BreadcrumbList>
48+
<BreadcrumbItem>
49+
<BreadcrumbLink as-child>
50+
<RouterLink to="/admin/teams">
51+
{{ t('adminTeams.title') }}
52+
</RouterLink>
53+
</BreadcrumbLink>
54+
</BreadcrumbItem>
55+
<BreadcrumbSeparator />
56+
<BreadcrumbItem>
57+
<Skeleton class="h-4 w-48" />
58+
</BreadcrumbItem>
59+
</BreadcrumbList>
60+
</Breadcrumb>
61+
</DsPageHeading>
62+
</template>
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue'
3+
import { useRoute, useRouter } from 'vue-router'
4+
import { SettingsMenu, SettingsMenuGroup, SettingsMenuItem } from '@/components/ui/settings-menu'
5+
import type { Team } from '@/views/admin/teams/types'
6+
7+
interface Props {
8+
team: Team
9+
teamId: string
10+
}
11+
12+
const props = defineProps<Props>()
13+
const route = useRoute()
14+
const router = useRouter()
15+
16+
// Navigation menu items
17+
const menuItems = computed(() => [
18+
{ id: 'general', label: 'General', path: `/admin/teams/${props.teamId}/general` },
19+
{ id: 'limits', label: 'Limits', path: `/admin/teams/${props.teamId}/limits` },
20+
{ id: 'members', label: 'Members', path: `/admin/teams/${props.teamId}/members` }
21+
])
22+
23+
// Map route names to section IDs
24+
const routeToSectionMap: Record<string, string> = {
25+
'AdminTeamDetailGeneral': 'general',
26+
'AdminTeamDetailLimits': 'limits',
27+
'AdminTeamDetailMembers': 'members',
28+
}
29+
30+
// Get current section from route name
31+
const currentSection = computed(() => {
32+
const routeName = route.name as string
33+
return routeToSectionMap[routeName] || 'general'
34+
})
35+
36+
// Navigate to a section (for mobile buttons)
37+
function navigateToSection(sectionId: string) {
38+
const item = menuItems.value.find(item => item.id === sectionId)
39+
if (item) {
40+
router.push(item.path)
41+
}
42+
}
43+
</script>
44+
45+
<template>
46+
<div class="flex flex-col space-y-8 md:flex-row md:space-x-12 md:space-y-0">
47+
<!-- Desktop Sidebar Navigation -->
48+
<aside class="hidden md:block w-56 shrink-0">
49+
<SettingsMenu>
50+
<SettingsMenuGroup>
51+
<SettingsMenuItem
52+
v-for="item in menuItems"
53+
:key="item.id"
54+
:to="item.path"
55+
:active="currentSection === item.id"
56+
>
57+
{{ item.label }}
58+
</SettingsMenuItem>
59+
</SettingsMenuGroup>
60+
</SettingsMenu>
61+
</aside>
62+
63+
<!-- Mobile Navigation -->
64+
<div class="block md:hidden">
65+
<nav class="flex space-x-1 p-1 bg-muted/50 rounded-lg">
66+
<button
67+
v-for="item in menuItems"
68+
:key="item.id"
69+
@click="navigateToSection(item.id)"
70+
class="flex-1 px-3 py-2 text-sm font-medium rounded-md transition-colors"
71+
:class="currentSection === item.id
72+
? 'bg-background text-foreground shadow-sm'
73+
: 'text-muted-foreground hover:text-foreground'"
74+
>
75+
{{ item.label }}
76+
</button>
77+
</nav>
78+
</div>
79+
80+
<!-- Content Area Slot -->
81+
<div class="flex-1">
82+
<slot />
83+
</div>
84+
</div>
85+
</template>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export { default as TeamDetailTabs } from './TeamDetailTabs.vue'
2+
export { default as TeamDetailPageHeading } from './TeamDetailPageHeading.vue'
3+
export { default as GeneralTab } from './TeamDetailGeneral.vue'
4+
export { default as LimitsTab } from './TeamDetailLimits.vue'
5+
export { default as MembersTab } from './TeamDetailMembers.vue'
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { ref, watch } from 'vue'
2+
import { useRoute } from 'vue-router'
3+
import { useI18n } from 'vue-i18n'
4+
import { useEventBus } from '@/composables/useEventBus'
5+
import { useBreadcrumbs } from '@/composables/useBreadcrumbs'
6+
import { TeamService, type Team } from '@/services/teamService'
7+
8+
export function useTeamDetailCache() {
9+
const route = useRoute()
10+
const { t } = useI18n()
11+
const eventBus = useEventBus()
12+
const { setBreadcrumbs } = useBreadcrumbs()
13+
14+
const team = ref<Team | null>(null)
15+
const isLoading = ref(true)
16+
const error = ref<string | null>(null)
17+
18+
const teamId = route.params.id as string
19+
const storageKeyName = `admin_team_name_${teamId}`
20+
const storageKeyTeam = `admin_team_data_${teamId}`
21+
22+
async function loadAndSetTeam() {
23+
try {
24+
isLoading.value = true
25+
const fetchedTeam = await TeamService.getTeamAsAdmin(teamId)
26+
27+
team.value = fetchedTeam
28+
error.value = null
29+
30+
// Cache the team name for instant loading on tab switches
31+
eventBus.setState(storageKeyName, fetchedTeam.name)
32+
33+
// Cache the full team object for instant loading
34+
eventBus.setState(storageKeyTeam, fetchedTeam)
35+
36+
// Update breadcrumbs with team name
37+
setBreadcrumbs([
38+
{ label: t('adminTeams.title'), href: '/admin/teams' },
39+
{ label: fetchedTeam.name }
40+
])
41+
} catch (err) {
42+
error.value = err instanceof Error ? err.message : 'An unknown error occurred'
43+
team.value = null
44+
45+
// Clear cached data on error
46+
eventBus.clearState(storageKeyName)
47+
eventBus.clearState(storageKeyTeam)
48+
} finally {
49+
isLoading.value = false
50+
}
51+
}
52+
53+
function initializeCache() {
54+
// Set initial breadcrumbs with loading state
55+
setBreadcrumbs([
56+
{ label: t('adminTeams.title'), href: '/admin/teams' },
57+
{ label: 'Loading...' }
58+
])
59+
60+
// Load cached team data immediately to prevent flicker
61+
const cachedName = eventBus.getState<string>(storageKeyName)
62+
const cachedTeam = eventBus.getState<Team>(storageKeyTeam)
63+
64+
if (cachedTeam && !team.value) {
65+
team.value = cachedTeam
66+
} else if (cachedName && !team.value) {
67+
// At minimum, show cached name
68+
team.value = {
69+
name: cachedName
70+
} as Team
71+
}
72+
}
73+
74+
function setupWatchers() {
75+
// Watch for team ID changes in route to clear cached data
76+
watch(
77+
() => route.params.id,
78+
(newId, oldId) => {
79+
if (newId && oldId && newId !== oldId) {
80+
// Clear old team's cached data
81+
const oldStorageKeyName = `admin_team_name_${oldId}`
82+
const oldStorageKeyTeam = `admin_team_data_${oldId}`
83+
eventBus.clearState(oldStorageKeyName)
84+
eventBus.clearState(oldStorageKeyTeam)
85+
86+
// Reset team to null to trigger loading state
87+
team.value = null
88+
89+
// Load new team
90+
loadAndSetTeam()
91+
}
92+
}
93+
)
94+
95+
// Watch team value changes to update cache
96+
watch(
97+
() => team.value,
98+
(newTeam) => {
99+
if (newTeam) {
100+
eventBus.setState(storageKeyName, newTeam.name)
101+
eventBus.setState(storageKeyTeam, newTeam)
102+
}
103+
},
104+
{ deep: true }
105+
)
106+
}
107+
108+
function cleanupWatchers() {
109+
// No specific cleanup needed - Vue handles watch cleanup automatically
110+
// This function exists for API consistency with MCP pattern
111+
}
112+
113+
return {
114+
team,
115+
isLoading,
116+
error,
117+
teamId,
118+
loadAndSetTeam,
119+
initializeCache,
120+
setupWatchers,
121+
cleanupWatchers
122+
}
123+
}

services/frontend/src/router/index.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,10 +266,33 @@ const routes: RouteRecordRaw[] = [
266266
name: 'AdminTeams',
267267
component: () => import('../views/admin/teams/index.vue'),
268268
},
269+
// Redirect base path to /general
269270
{
270271
path: 'teams/:id',
271-
name: 'AdminTeamDetail',
272-
component: () => import('../views/admin/teams/[id].vue'),
272+
redirect: (to) => {
273+
return {
274+
name: 'AdminTeamDetailGeneral',
275+
params: to.params
276+
}
277+
}
278+
},
279+
// General tab (default)
280+
{
281+
path: 'teams/:id/general',
282+
name: 'AdminTeamDetailGeneral',
283+
component: () => import('../views/admin/teams/[id]/general.vue'),
284+
},
285+
// Limits tab
286+
{
287+
path: 'teams/:id/limits',
288+
name: 'AdminTeamDetailLimits',
289+
component: () => import('../views/admin/teams/[id]/limits.vue'),
290+
},
291+
// Members tab
292+
{
293+
path: 'teams/:id/members',
294+
name: 'AdminTeamDetailMembers',
295+
component: () => import('../views/admin/teams/[id]/members.vue'),
273296
},
274297
{
275298
path: 'mcp-server-catalog',

0 commit comments

Comments
 (0)