Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e7fffa9
feat(team): add leave team feature with API support
Hariom01010 Aug 28, 2025
5af8c33
feat(team): move enum to teams.enum file
Hariom01010 Aug 28, 2025
032f31f
feat(team): add remove member functionality
Hariom01010 Aug 28, 2025
cd69dd2
feat(team): add types for remove team activity
Hariom01010 Aug 28, 2025
1f47186
refactor: resolve minor bot comments and apply small code improvements
Hariom01010 Aug 29, 2025
175a7aa
refactor: remove unnecessary query invalidation
Hariom01010 Aug 29, 2025
fea6b0f
refactor: include memberId in removeFromTeam mutation key
Hariom01010 Aug 29, 2025
4aed4b0
Merge branch 'feat(team)/leave-team' of https://github.com/Hariom0101…
Hariom01010 Aug 29, 2025
1e3be2a
feat(leave-team): add disabled state for dialog button
Hariom01010 Sep 3, 2025
82a39f3
feat(leave-team): updated leave team dialog to only close after
Hariom01010 Sep 3, 2025
0c67eb0
feat(leave-team): invalidate query to fetch all team
Hariom01010 Sep 5, 2025
fdc7239
feat(leave-team): remove get team member invalidation query
Hariom01010 Sep 6, 2025
0a695c6
Merge branch 'develop' of https://github.com/Hariom01010/todo-fronten…
Hariom01010 Sep 15, 2025
b0e9e3f
Merge branch 'feat(team)/leave-team' of https://github.com/Hariom0101…
Hariom01010 Sep 16, 2025
c9ca897
feat(leave-team): remove unecessary activity type
Hariom01010 Sep 17, 2025
28f2166
feat(remove-from-team): add isAuthLoading state to render shimmer until
Hariom01010 Sep 19, 2025
893cbe7
feat(remove-from-team): add state management to close the dialog once
Hariom01010 Sep 19, 2025
cf9ef63
feat(leave-team): rename types for clarity
Hariom01010 Sep 20, 2025
c336959
Merge branch 'feat(team)/leave-team' of https://github.com/Hariom0101…
Hariom01010 Sep 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/api/teams/teams.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import {
CreateTeamPayload,
GetTeamByIdReqDto,
GetTeamsDto,
GetUserRolesParams,
RemoveFromTeamParams,
TeamActivityTimeline,
TeamCreationCodeVerificationResponse,
TeamDto,
TTeamDto,
UserRolesDetails,
} from './teams.type'

export const TeamsApi = {
Expand Down Expand Up @@ -68,6 +71,26 @@ export const TeamsApi = {
return data
},
},
removeFromTeam: {
key: ({ teamId, memberId }: RemoveFromTeamParams) => [
'TeamsApi.removeFromTeam',
teamId,
memberId,
],
fn: async ({ teamId, memberId }: RemoveFromTeamParams): Promise<void> => {
await apiClient.delete(`/v1/teams/${teamId}/members/${memberId}`)
},
},
getUserRoles: {
key: ({ teamId, userId }: GetUserRolesParams) => ['TeamsApi.getUserRoles', teamId, userId],
fn: async ({ teamId, userId }: GetUserRolesParams): Promise<UserRolesDetails> => {
const { data } = await apiClient.get<UserRolesDetails>(
`/v1/teams/${teamId}/users/${userId}/roles`,
)
return data
},
},

verifyTeamCreationCode: {
key: ['TeamsApi.verifyTeamCreationCode'],
fn: async ({ code }: { code: string }): Promise<TeamCreationCodeVerificationResponse> => {
Expand Down
9 changes: 9 additions & 0 deletions src/api/teams/teams.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export enum TeamRoles {
OWNER = 'owner',
ADMIN = 'admin',
MEMBER = 'member',
}

export enum RolesScope {
TEAM = 'TEAM',
}
40 changes: 40 additions & 0 deletions src/api/teams/teams.type.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { RolesScope, TeamRoles } from './teams.enum'

export type TTeam = {
id: string
name: string
Expand Down Expand Up @@ -51,6 +53,8 @@ export enum TeamActivityActions {
TEAM_CREATED = 'team_created',
MEMBER_JOINED_TEAM = 'member_joined_team',
MEMBER_ADDED_TO_TEAM = 'member_added_to_team',
MEMBER_REMOVED_FROM_TEAM = 'member_removed_from_team',
MEMBER_LEFT_TEAM = 'member_left_team',
}

export type BaseActivity = {
Expand Down Expand Up @@ -101,6 +105,16 @@ export type MemberJoinActivity = BaseActivity & {
performed_by_name: string
}

export type RemoveTeamMemberActivity = BaseActivity & {
action: TeamActivityActions.MEMBER_REMOVED_FROM_TEAM
performed_by_name: string
}

export type MemberLeftTeamActivity = BaseActivity & {
action: TeamActivityActions.MEMBER_LEFT_TEAM
performed_by_name: string
}

export type TeamActivity =
| TeamCreationActivity
| TaskAssignActivity
Expand All @@ -109,11 +123,37 @@ export type TeamActivity =
| ReassignExecutorActivity
| AddMemberActivity
| MemberJoinActivity
| RemoveTeamMemberActivity
| MemberLeftTeamActivity

export type TeamActivityTimeline = {
timeline: TeamActivity[]
}

export type TeamRole = {
role_id: string
role_name: TeamRoles
scope: RolesScope.TEAM
team_id: string
assigned_at: string
}

export type UserRolesDetails = {
team_id: string
user_id: string
roles: readonly TeamRole[]
}

export type TeamCreationCodeVerificationResponse = {
message: string
}

export type RemoveFromTeamParams = {
teamId: string
memberId: string
}

export type GetUserRolesParams = {
teamId: string
userId: string
}
67 changes: 67 additions & 0 deletions src/components/teams/leave-team-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { TasksApi } from '@/api/tasks/tasks.api'
import { TeamsApi } from '@/api/teams/teams.api'
import { Shimmer } from '@/components/common/shimmer'
import { Button } from '@/components/ui/button'
import { useAuth } from '@/hooks/useAuth'
import { LeaveTeamDialog } from '@/modules/teams/components/leave-team-dialog'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { useRouter } from '@tanstack/react-router'
import { LogOut } from 'lucide-react'
import { useState } from 'react'
import { toast } from 'sonner'

export const LeaveTeamButton = ({ teamId }: { teamId: string }) => {
const router = useRouter()
const queryClient = useQueryClient()
const [showLeaveTeamDialog, setShowLeaveTeamDialog] = useState(false)

const { isLoading: isAuthLoading, user } = useAuth()
const leaveTeamMutation = useMutation({
mutationFn: TeamsApi.removeFromTeam.fn,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: TeamsApi.getTeams.key,
})
queryClient.invalidateQueries({
queryKey: TasksApi.getTasks.key({ teamId }),
})
setShowLeaveTeamDialog(false)
toast.success('Successfully left team')
router.navigate({
to: '/dashboard',
search: { status: undefined, tab: undefined, search: undefined },
})
},
onError: () => {
toast.error('Failed to leave team')
},
})
if (isAuthLoading) {
return <Shimmer />
}

const handleLeaveTeam = () => {
if (user?.id) {
leaveTeamMutation.mutate({ teamId, memberId: user.id })
} else {
toast.error('User ID not found')
}
}

return (
<LeaveTeamDialog
title="Leave Team"
description="Are you sure you want to leave this team? You will lose access to its tasks and members."
buttonText={leaveTeamMutation.isPending ? 'Leaving' : 'Leave Team'}
open={showLeaveTeamDialog}
onOpenChange={setShowLeaveTeamDialog}
onSubmit={handleLeaveTeam}
isSubmitting={leaveTeamMutation.isPending}
>
<Button variant="destructive" size="sm" className="mx-1">
<LogOut />
Leave Team
</Button>
</LeaveTeamDialog>
)
}
78 changes: 74 additions & 4 deletions src/components/teams/team-members.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { TasksApi } from '@/api/tasks/tasks.api'
import { TeamsApi } from '@/api/teams/teams.api'
import { TeamRoles } from '@/api/teams/teams.enum'
import { TTeamUser } from '@/api/teams/teams.type'
import { Searchbar } from '@/components/common/searchbar'
import { Shimmer } from '@/components/common/shimmer'
Expand All @@ -19,10 +21,13 @@ import {
TableHeader,
TableRow,
} from '@/components/ui/table'
import { useAuth } from '@/hooks/useAuth'
import { DateFormats, DateUtil } from '@/lib/date-util'
import { useQuery } from '@tanstack/react-query'
import { LeaveTeamDialog } from '@/modules/teams/components/leave-team-dialog'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { MoreVertical } from 'lucide-react'
import { useMemo, useState } from 'react'
import { toast } from 'sonner'

const PocLabel = () => {
return (
Expand All @@ -46,13 +51,24 @@ type TeamMembersProps = {

export const TeamMembers = ({ teamId }: TeamMembersProps) => {
const [searchQuery, setSearchQuery] = useState('')
const queryClient = useQueryClient()
const { user, isLoading: isAuthLoading } = useAuth()
const [activeDialogMemberId, setActiveDialogMemberId] = useState<string | null>(null)

const { data, isLoading } = useQuery({
queryKey: TeamsApi.getTeamById.key({ teamId, member: true }),
queryFn: () => TeamsApi.getTeamById.fn({ teamId, member: true }),
})
const userId = user?.id
const { data: userRole, isLoading: isUserRoleLoading } = useQuery({
queryKey: TeamsApi.getUserRoles.key({ teamId, userId: userId }),
queryFn: () => {
return TeamsApi.getUserRoles.fn({ teamId, userId })
},
enabled: !!userId,
})
const isAdmin = userRole?.roles.find((role) => role.role_name == TeamRoles.ADMIN)

const isAdmin = true
const filteredMembers = useMemo(() => {
if (!searchQuery) {
return data?.users
Expand All @@ -70,6 +86,26 @@ export const TeamMembers = ({ teamId }: TeamMembersProps) => {
setSearchQuery(searchValue)
}

const removeMemberMutation = useMutation({
mutationFn: TeamsApi.removeFromTeam.fn,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: TeamsApi.getTeamById.key({ teamId, member: true }),
})
queryClient.invalidateQueries({
queryKey: TeamsApi.getTeams.key,
})
queryClient.invalidateQueries({
queryKey: TasksApi.getTasks.key(),
})
setActiveDialogMemberId(null)
toast.success('User removed Successfully')
},
onError: () => {
toast.error('Failed to remove member')
},
})

return (
<div>
<div className="flex items-center justify-between pb-4">
Expand All @@ -96,7 +132,7 @@ export const TeamMembers = ({ teamId }: TeamMembersProps) => {
</TableHeader>

<TableBody>
{isLoading
{isLoading || isUserRoleLoading || isAuthLoading
? new Array(5).fill(0).map((_, index) => (
<TableRow key={index}>
<TableCell colSpan={5}>
Expand Down Expand Up @@ -137,7 +173,41 @@ export const TeamMembers = ({ teamId }: TeamMembersProps) => {
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Change Role</DropdownMenuItem>
<DropdownMenuItem>Remove from team</DropdownMenuItem>
{member.id !== user?.id &&
member.id !== data?.created_by &&
member.id !== data?.poc_id ? (
<LeaveTeamDialog
title="Remove Member"
description="Are you sure you want to remove this member from the team? They will lose access to all tasks and team information."
buttonText="Remove Member"
open={activeDialogMemberId === member.id}
onOpenChange={(open) => {
if (open) {
setActiveDialogMemberId(member.id)
} else {
setActiveDialogMemberId(null)
}
}}
onSubmit={() => {
removeMemberMutation.mutate({
teamId,
memberId: member.id,
})
}}
isSubmitting={removeMemberMutation.isPending}
>
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault()
setActiveDialogMemberId(member.id)
}}
>
Remove from team
</DropdownMenuItem>
</LeaveTeamDialog>
) : (
<></>
)}
<DropdownMenuItem>View Assigned tasks</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
Expand Down
25 changes: 24 additions & 1 deletion src/lib/team-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { TeamActivity, TeamActivityActions } from '@/api/teams/teams.type'
import { TASK_STATUS_TO_TEXT_MAP } from '@/components/todos/todo-status-table'
import { LucideIcon, Minus, Plus, RefreshCcw, UserPlus, Users, UsersRound } from 'lucide-react'
import {
LucideIcon,
Minus,
Plus,
RefreshCcw,
UserMinus,
UserPlus,
Users,
UsersRound,
} from 'lucide-react'
import { DateFormats, DateUtil } from './date-util'

type ActivityUIData = {
Expand Down Expand Up @@ -66,6 +75,20 @@ export function getActivityUIData(activity: TeamActivity): ActivityUIData | unde
description: `${activity.performed_by_name} joined team ${activity.team_name}`,
date,
}
case TeamActivityActions.MEMBER_REMOVED_FROM_TEAM:
return {
icon: UserMinus,
title: 'Member removed from team',
description: `${activity.performed_by_name} removed a member from team ${activity.team_name}`,
date,
}
case TeamActivityActions.MEMBER_LEFT_TEAM:
return {
icon: UserMinus,
title: 'Member left team',
description: `${activity.performed_by_name} left the team ${activity.team_name}`,
date,
}
default:
return undefined
}
Expand Down
Loading