diff --git a/frontend/src/routes/dashboard/team/+page.svelte b/frontend/src/routes/dashboard/team/+page.svelte index 8102e6a..9accbf6 100644 --- a/frontend/src/routes/dashboard/team/+page.svelte +++ b/frontend/src/routes/dashboard/team/+page.svelte @@ -6,15 +6,18 @@ import { toast } from '$lib/toast.svelte'; import { getTeam, + listTeams, updateTeam, addMember, removeMember, updateMemberRole, deleteTeam, leaveTeam, + switchTeam, searchUsers, type TeamInfo, type TeamMember, + type TeamWithRole, type UserSearchResult } from '$lib/api/team'; @@ -27,9 +30,13 @@ // Page data let team = $state(null); let members = $state([]); + let allTeams = $state([]); let loading = $state(true); let error = $state(null); + // True when this is the user's only team — deleting/leaving would leave them teamless + let isLastTeam = $derived(allTeams.length <= 1); + // Current user's role — derived from members list let myRole = $derived(members.find((m) => m.user_id === auth.userId)?.role ?? 'member'); let canManage = $derived(myRole === 'owner' || myRole === 'admin'); @@ -73,15 +80,24 @@ let dangerError = $state(null); async function fetchTeam() { - if (!auth.teamId) return; loading = true; error = null; - const result = await getTeam(auth.teamId); - if (result.ok) { - team = result.data.team; - members = result.data.members; + if (!auth.teamId) { + loading = false; + return; + } + const [teamResult, teamsResult] = await Promise.all([ + getTeam(auth.teamId), + listTeams() + ]); + if (teamResult.ok) { + team = teamResult.data.team; + members = teamResult.data.members; } else { - error = result.error; + error = teamResult.error; + } + if (teamsResult.ok) { + allTeams = teamsResult.data; } loading = false; } @@ -218,7 +234,21 @@ dangerError = null; const result = myRole === 'owner' ? await deleteTeam(team.id) : await leaveTeam(team.id); if (result.ok) { - auth.logout(); + // Fetch remaining teams and switch to the first available one + const teamsResult = await listTeams(); + const remaining = teamsResult.ok ? teamsResult.data : []; + if (remaining.length > 0) { + const switchResult = await switchTeam(remaining[0].id); + if (switchResult.ok) { + auth.login(switchResult.data); + window.location.reload(); + return; + } + } + // No teams left — prompt user to create one + dangerLoading = false; + showDangerConfirm = false; + toast.error('No teams remaining. Use the sidebar to create a new team.'); } else { dangerError = result.error; dangerLoading = false; @@ -331,6 +361,19 @@ Loading team... + {:else if !auth.teamId} +
+
+ + + + +
+

No team yet

+

+ Use the team switcher in the sidebar to create your first team. +

+
{:else if team}
@@ -765,16 +808,13 @@ Delete this team

- Permanently deletes the team and destroys all running capsules. This cannot - be undone. + {#if isLastTeam}Create another team before deleting this one.{:else}Permanently deletes the team and destroys all running capsules. This cannot be undone.{/if}

@@ -784,15 +824,13 @@ Leave this team

- You will lose access to all capsules and resources belonging to this team. + {#if isLastTeam}Create another team before leaving this one.{:else}You will lose access to all capsules and resources belonging to this team.{/if}

diff --git a/internal/service/team.go b/internal/service/team.go index 0a8fa26..888e53f 100644 --- a/internal/service/team.go +++ b/internal/service/team.go @@ -18,7 +18,7 @@ import ( "git.omukk.dev/wrenn/sandbox/proto/hostagent/gen/hostagentv1connect" ) -var teamNameRE = regexp.MustCompile(`^[A-Za-z0-9 _]{1,128}$`) +var teamNameRE = regexp.MustCompile(`^[A-Za-z0-9 _\-@']{1,128}$`) // TeamService provides team management operations. type TeamService struct {