From 91fc41ab9b6a41cd91950be11868a86380d27c36 Mon Sep 17 00:00:00 2001 From: Achintya-Chatterjee Date: Wed, 10 Dec 2025 03:15:24 +0530 Subject: [PATCH 1/3] feat: Implement assignee filter for team todos --- src/api/tasks/tasks.types.ts | 1 + src/lib/api-client.ts | 13 ++ src/modules/teams/components/team-filters.tsx | 140 ++++++++++++++++++ src/modules/teams/team-tasks.tsx | 9 ++ src/routes/_internal.teams.$teamId.todos.tsx | 5 + 5 files changed, 168 insertions(+) create mode 100644 src/modules/teams/components/team-filters.tsx diff --git a/src/api/tasks/tasks.types.ts b/src/api/tasks/tasks.types.ts index cda0325..5f86ad4 100644 --- a/src/api/tasks/tasks.types.ts +++ b/src/api/tasks/tasks.types.ts @@ -39,6 +39,7 @@ export type TEditTask = { export type GetTaskReqDto = { teamId?: string status?: string + assigneeId?: string[] } export type GetTasksDto = { diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index 78654ed..b943988 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -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( diff --git a/src/modules/teams/components/team-filters.tsx b/src/modules/teams/components/team-filters.tsx new file mode 100644 index 0000000..373b049 --- /dev/null +++ b/src/modules/teams/components/team-filters.tsx @@ -0,0 +1,140 @@ +import { TeamsApi } from '@/api/teams/teams.api' +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 { useQuery } from '@tanstack/react-query' +import { useNavigate, useSearch } from '@tanstack/react-router' +import { CheckIcon, UserIcon } from 'lucide-react' + +type TeamFiltersProps = { + teamId: string +} + +export const TeamFilters = ({ teamId }: TeamFiltersProps) => { + const navigate = useNavigate() + const searchParams = useSearch({ from: '/_internal/teams/$teamId/todos' }) + const assigneeIds = ( + Array.isArray(searchParams.assigneeId) + ? searchParams.assigneeId + : searchParams.assigneeId + ? [searchParams.assigneeId] + : [] + ) as string[] + + const { data: team } = useQuery({ + queryKey: TeamsApi.getTeamById.key({ teamId, member: true }), + queryFn: () => TeamsApi.getTeamById.fn({ teamId, member: true }), + }) + + const teamMembers = team?.users ?? [] + + const handleAssigneeToggle = (memberId: string) => { + const newAssigneeIds = assigneeIds.includes(memberId) + ? assigneeIds.filter((id) => id !== memberId) + : [...assigneeIds, memberId] + + navigate({ + to: '/teams/$teamId/todos', + params: { teamId }, + search: (prev) => ({ + status: prev.status, + search: prev.search, + assigneeId: newAssigneeIds.length ? newAssigneeIds : undefined, + }), + }) + } + + const handleClearAssignees = () => { + navigate({ + to: '/teams/$teamId/todos', + params: { teamId }, + search: (prev) => ({ + status: prev.status, + search: prev.search, + assigneeId: undefined, + }), + }) + } + + return ( + + + + + + Filter by + + + + + + Assignee + + {assigneeIds.length > 0 ? assigneeIds.length : ''} + + + + + + + No member found. + + + Clear filters + + + + + {teamMembers.map((member) => { + const isSelected = assigneeIds.includes(member.id) + return ( + handleAssigneeToggle(member.id)}> +
+ +
+ {member.name} +
+ ) + })} +
+
+
+
+
+
+
+ ) +} diff --git a/src/modules/teams/team-tasks.tsx b/src/modules/teams/team-tasks.tsx index 46cfe35..8a03a5d 100644 --- a/src/modules/teams/team-tasks.tsx +++ b/src/modules/teams/team-tasks.tsx @@ -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' @@ -116,6 +117,11 @@ export const TeamTasks = ({ teamId }: TeamTasksProps) => { const queryParams: GetTaskReqDto = { teamId, ...(includeDoneTasks && { status: TASK_STATUS_ENUM.DONE }), + assigneeId: (Array.isArray(searchParams.assigneeId) + ? searchParams.assigneeId + : searchParams.assigneeId + ? [searchParams.assigneeId] + : undefined) as string[] | undefined, } const { data: team, isLoading: isLoadingTeam } = useQuery({ @@ -138,6 +144,7 @@ export const TeamTasks = ({ teamId }: TeamTasksProps) => { search: (prev) => ({ status: prev.status || undefined, search: searchValue || undefined, + assigneeId: prev.assigneeId, }), }) } @@ -149,6 +156,7 @@ export const TeamTasks = ({ teamId }: TeamTasksProps) => { search: (prev) => ({ status: includeDone ? TASK_STATUS_ENUM.DONE : undefined, search: prev.search || undefined, + assigneeId: prev.assigneeId, }), }) } @@ -166,6 +174,7 @@ export const TeamTasks = ({ teamId }: TeamTasksProps) => { onStatusChange={handleIncludeDoneChange} initialChecked={includeDoneTasks} /> +
diff --git a/src/routes/_internal.teams.$teamId.todos.tsx b/src/routes/_internal.teams.$teamId.todos.tsx index b9bef5e..2497783 100644 --- a/src/routes/_internal.teams.$teamId.todos.tsx +++ b/src/routes/_internal.teams.$teamId.todos.tsx @@ -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) as string[] | undefined, } }, }) From 8439e339fa3d57188ff0cf3aba262ee169cee34a Mon Sep 17 00:00:00 2001 From: Achintya-Chatterjee Date: Wed, 10 Dec 2025 21:40:02 +0530 Subject: [PATCH 2/3] refactor: optimize team data fetching in TeamFilters - Remove duplicate team query in TeamFilters component - Pass team data as prop from parent TeamTasks component - Update TeamTasks query to include member=true for users list - Remove unused imports and error handling from TeamFilters - Eliminate redundant network request and improve data consistency --- src/modules/teams/components/team-filters.tsx | 11 +++-------- src/modules/teams/team-tasks.tsx | 12 ++++-------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/modules/teams/components/team-filters.tsx b/src/modules/teams/components/team-filters.tsx index 373b049..370ce84 100644 --- a/src/modules/teams/components/team-filters.tsx +++ b/src/modules/teams/components/team-filters.tsx @@ -1,4 +1,4 @@ -import { TeamsApi } from '@/api/teams/teams.api' +import { TTeam } from '@/api/teams/teams.type' import { Button } from '@/components/ui/button' import { Command, @@ -20,15 +20,15 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { cn } from '@/lib/utils' -import { useQuery } from '@tanstack/react-query' import { useNavigate, useSearch } from '@tanstack/react-router' import { CheckIcon, UserIcon } from 'lucide-react' type TeamFiltersProps = { teamId: string + team?: TTeam } -export const TeamFilters = ({ teamId }: TeamFiltersProps) => { +export const TeamFilters = ({ teamId, team }: TeamFiltersProps) => { const navigate = useNavigate() const searchParams = useSearch({ from: '/_internal/teams/$teamId/todos' }) const assigneeIds = ( @@ -39,11 +39,6 @@ export const TeamFilters = ({ teamId }: TeamFiltersProps) => { : [] ) as string[] - const { data: team } = useQuery({ - queryKey: TeamsApi.getTeamById.key({ teamId, member: true }), - queryFn: () => TeamsApi.getTeamById.fn({ teamId, member: true }), - }) - const teamMembers = team?.users ?? [] const handleAssigneeToggle = (memberId: string) => { diff --git a/src/modules/teams/team-tasks.tsx b/src/modules/teams/team-tasks.tsx index 8a03a5d..f98a325 100644 --- a/src/modules/teams/team-tasks.tsx +++ b/src/modules/teams/team-tasks.tsx @@ -117,16 +117,12 @@ export const TeamTasks = ({ teamId }: TeamTasksProps) => { const queryParams: GetTaskReqDto = { teamId, ...(includeDoneTasks && { status: TASK_STATUS_ENUM.DONE }), - assigneeId: (Array.isArray(searchParams.assigneeId) - ? searchParams.assigneeId - : searchParams.assigneeId - ? [searchParams.assigneeId] - : undefined) as string[] | undefined, + 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({ @@ -174,7 +170,7 @@ export const TeamTasks = ({ teamId }: TeamTasksProps) => { onStatusChange={handleIncludeDoneChange} initialChecked={includeDoneTasks} /> - +
From bfc9d0cda38fe30ac2822e4beb3b090507192efa Mon Sep 17 00:00:00 2001 From: Achintya-Chatterjee Date: Sat, 13 Dec 2025 03:25:42 +0530 Subject: [PATCH 3/3] refactor: address PR feedback for assignee filter - Remove unnecessary type assertions for improved type safety - Implement debounced filter application (applies on dropdown close) - Adjust button styling to match search bar height (border: 1px) - Optimize spacing with -ml-3 for visual balance - Maintain red button color per original design requirements Addresses feedback from PR review regarding code quality, UX improvements, and styling consistency. --- src/modules/teams/components/team-filters.tsx | 58 +++++++++++-------- src/routes/_internal.teams.$teamId.todos.tsx | 4 +- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/src/modules/teams/components/team-filters.tsx b/src/modules/teams/components/team-filters.tsx index 370ce84..c4c5ac9 100644 --- a/src/modules/teams/components/team-filters.tsx +++ b/src/modules/teams/components/team-filters.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react' import { TTeam } from '@/api/teams/teams.type' import { Button } from '@/components/ui/button' import { @@ -31,50 +32,57 @@ type TeamFiltersProps = { 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] - : [] - ) as string[] + const assigneeIds = Array.isArray(searchParams.assigneeId) + ? searchParams.assigneeId + : searchParams.assigneeId + ? [searchParams.assigneeId] + : [] const teamMembers = team?.users ?? [] - const handleAssigneeToggle = (memberId: string) => { - const newAssigneeIds = assigneeIds.includes(memberId) - ? assigneeIds.filter((id) => id !== memberId) - : [...assigneeIds, memberId] + const [selectedIds, setSelectedIds] = useState(assigneeIds) + const [isOpen, setIsOpen] = useState(false) - navigate({ - to: '/teams/$teamId/todos', - params: { teamId }, - search: (prev) => ({ - status: prev.status, - search: prev.search, - assigneeId: newAssigneeIds.length ? newAssigneeIds : undefined, - }), - }) + 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: undefined, + assigneeId: selectedIds.length ? selectedIds : undefined, }), }) } return ( - + { + setIsOpen(open) + if (!open) { + applyFilters() + } + }} + > @@ -88,7 +96,7 @@ export const TeamFilters = ({ teamId, team }: TeamFiltersProps) => { Assignee - {assigneeIds.length > 0 ? assigneeIds.length : ''} + {selectedIds.length > 0 ? selectedIds.length : ''} @@ -107,7 +115,7 @@ export const TeamFilters = ({ teamId, team }: TeamFiltersProps) => { {teamMembers.map((member) => { - const isSelected = assigneeIds.includes(member.id) + const isSelected = selectedIds.includes(member.id) return ( handleAssigneeToggle(member.id)}>