Team Management

Switching Teams

Multi-team navigation and context.

Users can belong to multiple teams and seamlessly switch between them. The current team context is persisted across sessions using cookies.

Overview

The team switching system provides:

Instant Switching

Switch teams without page reload.

Persistent Selection

Current team is saved in a cookie.

Default Team

Auto-selects default team on first visit.

Sidebar UI

Built-in TeamSwitcher component.

Team Store

Team state is managed by a Pinia store in stores/team.ts:

State

PropertyTypeDescription
teamsTeam[]All teams the user belongs to
currentTeamIdnumber | nullCurrently selected team ID (cookie)

Methods

MethodDescription
setCurrentTeamId(id)Switch to a different team
getCurrentTeam()Get the currently selected team
getCurrentTeamId()Get the current team ID
getTeams()Get all user's teams
getTeamById(id)Get a specific team by ID
setCurrentTeamIdToDefault()Reset to the default team
reset()Clear all team state

How It Works

Initial Load

User logs in
      │
      ▼
┌─────────────────────┐
│  Fetch all teams    │ ◀── GET /teams
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│ Check cookie value  │ ◀── 'nuda-current-team-id'
└──────────┬──────────┘
           │
     ┌─────┴─────┐
     ▼           ▼
┌─────────┐ ┌─────────────┐
│ Cookie  │ │  No cookie  │
│ exists  │ │  or invalid │
└────┬────┘ └──────┬──────┘
     │             │
     ▼             ▼
┌─────────┐ ┌─────────────┐
│ Validate│ │ Use default │
│ team ID │ │    team     │
└────┬────┘ └──────┬──────┘
     │             │
     └──────┬──────┘
            ▼
┌─────────────────────┐
│ Set currentTeamId   │
└─────────────────────┘

Team Switching

// When user clicks a team in the switcher
teamStore.setCurrentTeamId(team.id)

// The cookie is automatically updated
// UI reactively updates to show new team context

The current team ID is stored in a cookie:

const currentTeamId = useCookie<number | null>('nuda-current-team-id')

Benefits

  • Persists across sessions — User returns to last-used team
  • SSR compatible — Available on server-side render
  • No API call needed — Team context is immediate on page load

Validation

On each page load, the store validates the cookie value:

watch(() => getAllTeams.data.value, (newTeams) => {
  if (currentTeamId.value && teams.value.length > 0) {
    // Validate that the current team ID exists
    const teamExists = teams.value.some(team => team.id === currentTeamId.value)
    if (!teamExists) {
      // Reset to default team if invalid
      const defaultTeam = teams.value.find(team => team.default)
      if (defaultTeam) {
        currentTeamId.value = defaultTeam.id
      }
    }
  }
})

This handles cases where:

  • User was removed from a team
  • Team was deleted
  • Cookie was manually modified

TeamSwitcher Component

The TeamSwitcher.vue component provides the UI for switching teams:

Location

components/app/TeamSwitcher.vue

Features

  • Dropdown menu in the sidebar
  • Shows current team with avatar
  • Lists all available teams
  • Pin icon indicates default team
  • "Add team" button to create new teams
  • Loading skeleton during fetch

Usage

The component is already included in the app sidebar:

<template>
  <Sidebar>
    <SidebarHeader>
      <TeamSwitcher />
    </SidebarHeader>
    <!-- ... -->
  </Sidebar>
</template>

Team Type

The Team type includes role and default status:

export type Team = {
  id: number
  name: string
  default: boolean
  role: TeamRole
  avatarUrl: string | null
  createdAt: string
  updatedAt: string
}

TeamRole Class

The TeamRole class provides utilities for role comparison:

const team = teamStore.getCurrentTeam()

// Check specific roles
if (team.role.equalsRole(TeamRoleValue.OWNER, TeamRoleValue.SUPER_ADMIN)) {
  // User is owner or super admin
}

// Compare role levels
if (team.role.isHigherThan(TeamRole.admin)) {
  // User has higher permissions than admin
}

// Display name
team.role.toDisplayName() // "Super Admin"

Common Patterns

Get Current Team

const teamStore = useTeamStore()

const currentTeam = computed(() => teamStore.getCurrentTeam())

Switch Team Programmatically

const teamStore = useTeamStore()

// Switch to a specific team
teamStore.setCurrentTeamId(teamId)

// Reset to default team
teamStore.setCurrentTeamIdToDefault()

Check if Team is Selected

const teamStore = useTeamStore()

const hasTeamSelected = computed(() => !!teamStore.getCurrentTeamId())

Conditional Rendering Based on Role

<template>
  <div v-if="isAdmin">
    <Button>Admin Action</Button>
  </div>
</template>

<script setup lang="ts">
const teamStore = useTeamStore()

const isAdmin = computed(() => {
  const team = teamStore.getCurrentTeam()
  return team?.role.equalsRole(
    TeamRoleValue.OWNER,
    TeamRoleValue.SUPER_ADMIN,
    TeamRoleValue.ADMIN
  )
})
</script>

React to Team Changes

const teamStore = useTeamStore()

watch(
  () => teamStore.getCurrentTeamId(),
  (newTeamId) => {
    // Refetch data for new team context
    console.log('Switched to team:', newTeamId)
  }
)

API Context

When making API calls, the current team context can be included:

// In a composable or service
const teamStore = useTeamStore()
const currentTeamId = teamStore.getCurrentTeamId()

// Include in API requests
const response = await api.get(`/teams/${currentTeamId}/resources`)

Default Team Behavior

Setting Default Team

The default team is set when:

  1. User creates their first team (becomes default)
  2. User accepts an invitation (if no default exists)

Default Team Restrictions

  • Cannot be deleted
  • Indicated by pin icon in TeamSwitcher
  • Auto-selected when cookie is invalid

Customization

Custom Team Switcher

Create your own team switcher component:

<template>
  <Select v-model="selectedTeamId" @update:model-value="switchTeam">
    <SelectTrigger>
      <SelectValue :placeholder="currentTeam?.name" />
    </SelectTrigger>
    <SelectContent>
      <SelectItem 
        v-for="team in teams" 
        :key="team.id" 
        :value="team.id"
      >
        {{ team.name }}
      </SelectItem>
    </SelectContent>
  </Select>
</template>

<script setup lang="ts">
const teamStore = useTeamStore()

const teams = computed(() => teamStore.getTeams())
const currentTeam = computed(() => teamStore.getCurrentTeam())
const selectedTeamId = ref(teamStore.getCurrentTeamId())

const switchTeam = (teamId: number) => {
  teamStore.setCurrentTeamId(teamId)
}
</script>

Modify the cookie name in stores/team.ts:

const currentTeamId = useCookie<number | null>('my-app-team-id')

Resources