Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions src/api/tasks/tasks.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export type TEditTask = {
export type GetTaskReqDto = {
teamId?: string
status?: string
assigneeId?: string[]
}

export type GetTasksDto = {
Expand Down
13 changes: 13 additions & 0 deletions src/lib/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ export const apiClient = axios.create({
baseURL: backendUrl,
timeout: 30000,
withCredentials: true,
paramsSerializer: {
serialize: (params) => {
const searchParams = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach((v) => searchParams.append(key, v))
} else if (value !== undefined && value !== null) {
searchParams.append(key, String(value))
}
})
return searchParams.toString()
},
},
})

apiClient.interceptors.request.use(
Expand Down
143 changes: 143 additions & 0 deletions src/modules/teams/components/team-filters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { useState } from 'react'
import { TTeam } from '@/api/teams/teams.type'
import { Button } from '@/components/ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from '@/components/ui/command'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { cn } from '@/lib/utils'
import { useNavigate, useSearch } from '@tanstack/react-router'
import { CheckIcon, UserIcon } from 'lucide-react'

type TeamFiltersProps = {
teamId: string
team?: TTeam
}

export const TeamFilters = ({ teamId, team }: TeamFiltersProps) => {
const navigate = useNavigate()
const searchParams = useSearch({ from: '/_internal/teams/$teamId/todos' })
const assigneeIds = Array.isArray(searchParams.assigneeId)
? searchParams.assigneeId
: searchParams.assigneeId
? [searchParams.assigneeId]
: []

const teamMembers = team?.users ?? []

const [selectedIds, setSelectedIds] = useState<string[]>(assigneeIds)
const [isOpen, setIsOpen] = useState(false)

useState(() => {
setSelectedIds(assigneeIds)
})

const handleAssigneeToggle = (memberId: string) => {
setSelectedIds((prev) =>
prev.includes(memberId) ? prev.filter((id) => id !== memberId) : [...prev, memberId],
)
}

const handleClearAssignees = () => {
setSelectedIds([])
}

const applyFilters = () => {
navigate({
to: '/teams/$teamId/todos',
params: { teamId },
search: (prev) => ({
status: prev.status,
search: prev.search,
assigneeId: selectedIds.length ? selectedIds : undefined,
}),
})
}

return (
<DropdownMenu
open={isOpen}
onOpenChange={(open) => {
setIsOpen(open)
if (!open) {
applyFilters()
}
}}
>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="-ml-3 border border-red-500 text-red-500 hover:border-red-600 hover:bg-red-50 hover:text-red-600"
>
Filter
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-56">
<DropdownMenuLabel>Filter by</DropdownMenuLabel>
<DropdownMenuSeparator />

<DropdownMenuSub>
<DropdownMenuSubTrigger>
<UserIcon className="mr-2 size-4" />
<span>Assignee</span>
<span className="ml-auto flex size-4 items-center justify-center font-mono text-xs">
{selectedIds.length > 0 ? selectedIds.length : ''}
</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="p-0" sideOffset={8}>
<Command>
<CommandInput placeholder="Search team members..." autoFocus />
<CommandList>
<CommandEmpty>No member found.</CommandEmpty>
<CommandGroup>
<CommandItem
onSelect={handleClearAssignees}
className="justify-center text-center font-medium"
>
Clear filters
</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup>
{teamMembers.map((member) => {
const isSelected = selectedIds.includes(member.id)
return (
<CommandItem key={member.id} onSelect={() => handleAssigneeToggle(member.id)}>
<div
className={cn(
'border-primary mr-2 flex size-4 items-center justify-center rounded-sm border',
isSelected
? 'bg-primary text-primary-foreground'
: 'opacity-50 [&_svg]:invisible',
)}
>
<CheckIcon className={cn('h-4 w-4')} />
</div>
<span>{member.name}</span>
</CommandItem>
)
})}
</CommandGroup>
</CommandList>
</Command>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>
)
}
9 changes: 7 additions & 2 deletions src/modules/teams/team-tasks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { TASK_STATUS_ENUM } from '@/api/tasks/tasks.enum'
import { GetTaskReqDto, TTask } from '@/api/tasks/tasks.types'
import { TeamsApi } from '@/api/teams/teams.api'
import { TTeam } from '@/api/teams/teams.type'
import { TeamFilters } from './components/team-filters'
import { Searchbar } from '@/components/common/searchbar'
import { EditTodoButton } from '@/components/todos/edit-task-button'
import { IncludeDoneSwitch } from '@/components/todos/include-done-switch'
Expand Down Expand Up @@ -116,11 +117,12 @@ export const TeamTasks = ({ teamId }: TeamTasksProps) => {
const queryParams: GetTaskReqDto = {
teamId,
...(includeDoneTasks && { status: TASK_STATUS_ENUM.DONE }),
assigneeId: searchParams.assigneeId,
}

const { data: team, isLoading: isLoadingTeam } = useQuery({
queryKey: TeamsApi.getTeamById.key({ teamId }),
queryFn: () => TeamsApi.getTeamById.fn({ teamId }),
queryKey: TeamsApi.getTeamById.key({ teamId, member: true }),
queryFn: () => TeamsApi.getTeamById.fn({ teamId, member: true }),
})

const { data: tasks, isLoading: isLoadingTasks } = useQuery({
Expand All @@ -138,6 +140,7 @@ export const TeamTasks = ({ teamId }: TeamTasksProps) => {
search: (prev) => ({
status: prev.status || undefined,
search: searchValue || undefined,
assigneeId: prev.assigneeId,
}),
})
}
Expand All @@ -149,6 +152,7 @@ export const TeamTasks = ({ teamId }: TeamTasksProps) => {
search: (prev) => ({
status: includeDone ? TASK_STATUS_ENUM.DONE : undefined,
search: prev.search || undefined,
assigneeId: prev.assigneeId,
}),
})
}
Expand All @@ -166,6 +170,7 @@ export const TeamTasks = ({ teamId }: TeamTasksProps) => {
onStatusChange={handleIncludeDoneChange}
initialChecked={includeDoneTasks}
/>
<TeamFilters teamId={teamId} team={team} />
</div>

<div className="overflow-hidden rounded-md border">
Expand Down
5 changes: 5 additions & 0 deletions src/routes/_internal.teams.$teamId.todos.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ export const Route = createFileRoute('/_internal/teams/$teamId/todos')({
return {
status: search.status as string | undefined,
search: search.search as string | undefined,
assigneeId: Array.isArray(search.assigneeId)
? search.assigneeId
: search.assigneeId
? [search.assigneeId]
: undefined,
}
},
})
Expand Down