From 70e7dae4e9c4cd0099cd72623e98433f7f0e36df Mon Sep 17 00:00:00 2001 From: ANIR1604 Date: Tue, 25 Nov 2025 21:53:48 +0530 Subject: [PATCH 1/5] Stats Multi Selector Filter --- .../HomeComponents/BottomBar/BottomBar.tsx | 5 +- .../Tasks/StatsMultiSelectFilter.tsx | 120 ++++++++++++ .../components/HomeComponents/Tasks/Tasks.tsx | 172 ++++++++++++++++-- .../Tasks/__tests__/Tasks.test.tsx | 42 +++++ 4 files changed, 322 insertions(+), 17 deletions(-) create mode 100644 frontend/src/components/HomeComponents/Tasks/StatsMultiSelectFilter.tsx diff --git a/frontend/src/components/HomeComponents/BottomBar/BottomBar.tsx b/frontend/src/components/HomeComponents/BottomBar/BottomBar.tsx index 78fab316..150fd292 100644 --- a/frontend/src/components/HomeComponents/BottomBar/BottomBar.tsx +++ b/frontend/src/components/HomeComponents/BottomBar/BottomBar.tsx @@ -4,6 +4,7 @@ import { buttonVariants } from '@/components/ui/button'; import { BottomBarProps } from './bottom-bar-utils'; import { Icons } from '@/components/icons'; import { MultiSelectFilter } from '@/components/ui/multiSelect'; +import { StatsMultiSelectFilter } from '../Tasks/StatsMultiSelectFilter'; import { Popover, PopoverTrigger, @@ -59,7 +60,7 @@ const BottomBar: React.FC = ({ - = ({ onSelectionChange={setSelectedStatus} className="min-w-[200px]" /> - void; + className?: string; +} + +export function StatsMultiSelectFilter({ + title, + options, + selectedValues, + onSelectionChange, + className, +}: StatsMultiSelectFilterProps) { + const [open, setOpen] = React.useState(false); + + const handleSelect = (value: string) => { + if (value === ALL_ITEMS_VALUE) { + onSelectionChange([]); + setOpen(false); + return; + } + const newSelectedValues = selectedValues.includes(value) + ? selectedValues.filter((item) => item !== value) + : [...selectedValues, value]; + onSelectionChange(newSelectedValues); + }; + + return ( + + + + + + + + + + No results found. + + handleSelect(ALL_ITEMS_VALUE)} + className="text-muted-foreground cursor-pointer" + > + All {title} + + {options.map((option) => { + const isSelected = selectedValues.includes(option); + + // Parse option for stats formatting + const parts = option.split(' '); + const hasStats = parts.length === 3; + + return ( + handleSelect(option)} + > + + {hasStats ? ( + <> + {parts[0]} + + {parts[1]} {parts[2]} + + + ) : ( + option + )} + + ); + })} + + + + + + ); +} diff --git a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx index 92b0d0e4..b2b5ad24 100644 --- a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx +++ b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx @@ -1,4 +1,11 @@ -import { useEffect, useState, useCallback } from 'react'; +import { + useEffect, + useState, + useCallback, + useMemo, + Dispatch, + SetStateAction, +} from 'react'; import { Task } from '../../utils/types'; import { ReportsView } from './ReportsView'; import Fuse from 'fuse.js'; @@ -60,6 +67,7 @@ import { import Pagination from './Pagination'; import { url } from '@/components/utils/URLs'; import { MultiSelectFilter } from '@/components/ui/multiSelect'; +import { StatsMultiSelectFilter } from './StatsMultiSelectFilter'; import BottomBar from '../BottomBar/BottomBar'; import { addTaskToBackend, @@ -74,6 +82,67 @@ import { format } from 'date-fns'; import { Taskskeleton } from './Task-Skeleton'; const db = new TasksDatabase(); +type CompletionSummary = Record; + +const COMPLETED_STATUS = 'completed'; + +const roundPercentage = (completed: number, total: number) => + total === 0 ? 0 : Math.round((completed / total) * 100); + +const aggregateProjectStats = (tasks: Task[]): CompletionSummary => + tasks.reduce((acc, task) => { + if (!task.project) { + return acc; + } + if (!acc[task.project]) { + acc[task.project] = { total: 0, completed: 0 }; + } + acc[task.project].total += 1; + if (task.status === COMPLETED_STATUS) { + acc[task.project].completed += 1; + } + return acc; + }, {} as CompletionSummary); + +const aggregateTagStats = (tasks: Task[]): CompletionSummary => + tasks.reduce((acc, task) => { + (task.tags || []).forEach((tag) => { + if (!tag) { + return; + } + if (!acc[tag]) { + acc[tag] = { total: 0, completed: 0 }; + } + acc[tag].total += 1; + if (task.status === COMPLETED_STATUS) { + acc[tag].completed += 1; + } + }); + return acc; + }, {} as CompletionSummary); + +type LabelMaps = { + options: string[]; + valueToDisplay: Record; + displayToValue: Record; +}; + +const buildLabelMaps = (keys: string[], stats: CompletionSummary): LabelMaps => + keys.reduce( + (acc, key) => { + const { total = 0, completed = 0 } = stats[key] ?? { + total: 0, + completed: 0, + }; + const percentage = roundPercentage(completed, total); + const label = `${key} ${completed}/${total} ${percentage}%`; + acc.options.push(label); + acc.valueToDisplay[key] = label; + acc.displayToValue[label] = key; + return acc; + }, + { options: [], valueToDisplay: {}, displayToValue: {} } as LabelMaps + ); export let syncTasksWithTwAndDb: () => any; export const Tasks = ( @@ -136,6 +205,75 @@ export const Tasks = ( const [debouncedTerm, setDebouncedTerm] = useState(''); const [lastSyncTime, setLastSyncTime] = useState(null); + const projectStats = useMemo(() => aggregateProjectStats(tasks), [tasks]); + const tagStats = useMemo(() => aggregateTagStats(tasks), [tasks]); + + const { + options: projectDisplayOptions, + valueToDisplay: projectValueToDisplay, + displayToValue: projectDisplayToValue, + } = useMemo( + () => buildLabelMaps(uniqueProjects, projectStats), + [uniqueProjects, projectStats] + ); + + const { + options: tagDisplayOptions, + valueToDisplay: tagValueToDisplay, + displayToValue: tagDisplayToValue, + } = useMemo( + () => buildLabelMaps(uniqueTags, tagStats), + [uniqueTags, tagStats] + ); + + const selectedProjectDisplayValues = useMemo( + () => + selectedProjects + .map((project) => projectValueToDisplay[project]) + .filter((label): label is string => Boolean(label)), + [selectedProjects, projectValueToDisplay] + ); + + const selectedTagDisplayValues = useMemo( + () => + selectedTags + .map((tag) => tagValueToDisplay[tag]) + .filter((label): label is string => Boolean(label)), + [selectedTags, tagValueToDisplay] + ); + + const handleProjectSelectionChange = useCallback< + Dispatch> + >( + (valueOrUpdater) => { + if (typeof valueOrUpdater === 'function') { + setSelectedProjects(valueOrUpdater); + return; + } + const rawValues = valueOrUpdater + .map((label) => projectDisplayToValue[label]) + .filter((value): value is string => Boolean(value)); + setSelectedProjects(rawValues); + }, + [projectDisplayToValue, setSelectedProjects] + ); + + const handleTagSelectionChange = useCallback< + Dispatch> + >( + (valueOrUpdater) => { + if (typeof valueOrUpdater === 'function') { + setSelectedTags(valueOrUpdater); + return; + } + const rawValues = valueOrUpdater + .map((label) => tagDisplayToValue[label]) + .filter((value): value is string => Boolean(value)); + setSelectedTags(rawValues); + }, + [tagDisplayToValue, setSelectedTags] + ); + const isOverdue = (due?: string) => { if (!due) return false; @@ -735,15 +873,15 @@ export const Tasks = ( className="container py-24 pl-1 pr-1 md:pr-4 md:pl-4 sm:py-32" >

- + handleProjectSelectionChange(values) + } className="flex-1 min-w-[140px]" /> - + handleTagSelectionChange(values) + } className="flex-1 min-w-[140px]" />
diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx b/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx index e4622342..f73997fc 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx @@ -38,6 +38,17 @@ jest.mock('@/components/ui/multiSelect', () => ({ )), })); +jest.mock('../StatsMultiSelectFilter', () => ({ + StatsMultiSelectFilter: jest.fn(({ title, options }) => ( +
+ Mocked Stats MultiSelect: {title} +
+ {JSON.stringify(options)} +
+
+ )), +})); + jest.mock('../../BottomBar/BottomBar', () => { return jest.fn(() =>
Mocked BottomBar
); }); @@ -135,6 +146,37 @@ describe('Tasks Component', () => { expect(screen.getByText('Mocked BottomBar')).toBeInTheDocument(); }); + test('displays completion stats inside filter dropdown options', async () => { + const { StatsMultiSelectFilter } = require('../StatsMultiSelectFilter'); + render(); + + expect(await screen.findByText('Task 1')).toBeInTheDocument(); + + const projectCall = StatsMultiSelectFilter.mock.calls.find( + ([props]: [{ title: string }]) => props.title === 'Projects' + ); + expect(projectCall).toBeTruthy(); + const projectOptions = projectCall![0].options; + expect( + projectOptions.some( + (opt: string) => + opt.includes('ProjectA') && opt.includes('0/6') && opt.includes('0%') + ) + ).toBe(true); + + const tagCall = StatsMultiSelectFilter.mock.calls.find( + ([props]: [{ title: string }]) => props.title === 'Tags' + ); + expect(tagCall).toBeTruthy(); + const tagOptions = tagCall![0].options; + expect( + tagOptions.some( + (opt: string) => + opt.includes('tag1') && opt.includes('0/4') && opt.includes('0%') + ) + ).toBe(true); + }); + test('renders the "Tasks per Page" dropdown with default value', async () => { render(); From 34c275de079b16b7888f5b3e0a468c874478b93b Mon Sep 17 00:00:00 2001 From: ANIR1604 Date: Tue, 25 Nov 2025 22:00:26 +0530 Subject: [PATCH 2/5] Test Cases Added --- .../BottomBar/__tests__/BottomBar.test.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frontend/src/components/HomeComponents/BottomBar/__tests__/BottomBar.test.tsx b/frontend/src/components/HomeComponents/BottomBar/__tests__/BottomBar.test.tsx index 44878bac..77ccaf61 100644 --- a/frontend/src/components/HomeComponents/BottomBar/__tests__/BottomBar.test.tsx +++ b/frontend/src/components/HomeComponents/BottomBar/__tests__/BottomBar.test.tsx @@ -17,6 +17,20 @@ jest.mock('@/components/ui/multiSelect', () => ({ )), })); +// Mock the StatsMultiSelectFilter component +jest.mock('../../Tasks/StatsMultiSelectFilter', () => ({ + StatsMultiSelectFilter: jest.fn(({ title, selectedValues }) => ( +
+ + {title} + + + {selectedValues.length} + +
+ )), +})); + const mockProps: BottomBarProps = { projects: ['Project A', 'Project B'], selectedProjects: ['Project A'], From 1e1831a53e75949850a73f2ca494fb5eecc354e9 Mon Sep 17 00:00:00 2001 From: ANIR1604 Date: Fri, 28 Nov 2025 23:10:53 +0530 Subject: [PATCH 3/5] Make the Types and Constant --- .../components/HomeComponents/Tasks/Tasks.tsx | 65 ++----------------- .../HomeComponents/Tasks/constants.ts | 1 + .../HomeComponents/Tasks/tasks-utils.ts | 57 ++++++++++++++++ .../components/HomeComponents/Tasks/types.ts | 10 +++ 4 files changed, 72 insertions(+), 61 deletions(-) create mode 100644 frontend/src/components/HomeComponents/Tasks/constants.ts create mode 100644 frontend/src/components/HomeComponents/Tasks/types.ts diff --git a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx index 6ec12a80..684e2167 100644 --- a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx +++ b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx @@ -3,6 +3,7 @@ import { useState, useCallback, useMemo, + useRef, Dispatch, SetStateAction, } from 'react'; @@ -64,6 +65,9 @@ import { sortTasksById, getTimeSinceLastSync, hashKey, + aggregateProjectStats, + aggregateTagStats, + buildLabelMaps, } from './tasks-utils'; import Pagination from './Pagination'; import { url } from '@/components/utils/URLs'; @@ -84,67 +88,6 @@ import { Taskskeleton } from './TaskSkeleton'; import { Key } from '@/components/ui/key-button'; const db = new TasksDatabase(); -type CompletionSummary = Record; - -const COMPLETED_STATUS = 'completed'; - -const roundPercentage = (completed: number, total: number) => - total === 0 ? 0 : Math.round((completed / total) * 100); - -const aggregateProjectStats = (tasks: Task[]): CompletionSummary => - tasks.reduce((acc, task) => { - if (!task.project) { - return acc; - } - if (!acc[task.project]) { - acc[task.project] = { total: 0, completed: 0 }; - } - acc[task.project].total += 1; - if (task.status === COMPLETED_STATUS) { - acc[task.project].completed += 1; - } - return acc; - }, {} as CompletionSummary); - -const aggregateTagStats = (tasks: Task[]): CompletionSummary => - tasks.reduce((acc, task) => { - (task.tags || []).forEach((tag) => { - if (!tag) { - return; - } - if (!acc[tag]) { - acc[tag] = { total: 0, completed: 0 }; - } - acc[tag].total += 1; - if (task.status === COMPLETED_STATUS) { - acc[tag].completed += 1; - } - }); - return acc; - }, {} as CompletionSummary); - -type LabelMaps = { - options: string[]; - valueToDisplay: Record; - displayToValue: Record; -}; - -const buildLabelMaps = (keys: string[], stats: CompletionSummary): LabelMaps => - keys.reduce( - (acc, key) => { - const { total = 0, completed = 0 } = stats[key] ?? { - total: 0, - completed: 0, - }; - const percentage = roundPercentage(completed, total); - const label = `${key} ${completed}/${total} ${percentage}%`; - acc.options.push(label); - acc.valueToDisplay[key] = label; - acc.displayToValue[label] = key; - return acc; - }, - { options: [], valueToDisplay: {}, displayToValue: {} } as LabelMaps - ); export let syncTasksWithTwAndDb: () => any; export const Tasks = ( diff --git a/frontend/src/components/HomeComponents/Tasks/constants.ts b/frontend/src/components/HomeComponents/Tasks/constants.ts new file mode 100644 index 00000000..1a5f034a --- /dev/null +++ b/frontend/src/components/HomeComponents/Tasks/constants.ts @@ -0,0 +1 @@ +export const COMPLETED_STATUS = 'completed'; diff --git a/frontend/src/components/HomeComponents/Tasks/tasks-utils.ts b/frontend/src/components/HomeComponents/Tasks/tasks-utils.ts index dcfde5fe..85e140ec 100644 --- a/frontend/src/components/HomeComponents/Tasks/tasks-utils.ts +++ b/frontend/src/components/HomeComponents/Tasks/tasks-utils.ts @@ -2,6 +2,8 @@ import { Task } from '@/components/utils/types'; import { url } from '@/components/utils/URLs'; import { format, parseISO } from 'date-fns'; import { toast } from 'react-toastify'; +import { CompletionSummary, LabelMaps } from './types'; +import { COMPLETED_STATUS } from './constants'; export type Props = { email: string; @@ -183,3 +185,58 @@ export const hashKey = (key: string, email: string): string => { } return Math.abs(hash).toString(36); }; + +export const roundPercentage = (completed: number, total: number) => + total === 0 ? 0 : Math.round((completed / total) * 100); + +export const aggregateProjectStats = (tasks: Task[]): CompletionSummary => + tasks.reduce((acc, task) => { + if (!task.project) { + return acc; + } + if (!acc[task.project]) { + acc[task.project] = { total: 0, completed: 0 }; + } + acc[task.project].total += 1; + if (task.status === COMPLETED_STATUS) { + acc[task.project].completed += 1; + } + return acc; + }, {} as CompletionSummary); + +export const aggregateTagStats = (tasks: Task[]): CompletionSummary => + tasks.reduce((acc, task) => { + (task.tags || []).forEach((tag) => { + if (!tag) { + return; + } + if (!acc[tag]) { + acc[tag] = { total: 0, completed: 0 }; + } + acc[tag].total += 1; + if (task.status === COMPLETED_STATUS) { + acc[tag].completed += 1; + } + }); + return acc; + }, {} as CompletionSummary); + +export const buildLabelMaps = ( + keys: string[], + stats: CompletionSummary +): LabelMaps => + keys.reduce( + (acc, key) => { + const { total = 0, completed = 0 } = stats[key] ?? { + total: 0, + completed: 0, + }; + const percentage = roundPercentage(completed, total); + const label = `${key} ${completed}/${total} ${percentage}%`; + acc.options.push(label); + acc.valueToDisplay[key] = label; + acc.displayToValue[label] = key; + return acc; + }, + { options: [], valueToDisplay: {}, displayToValue: {} } as LabelMaps + ); diff --git a/frontend/src/components/HomeComponents/Tasks/types.ts b/frontend/src/components/HomeComponents/Tasks/types.ts new file mode 100644 index 00000000..89ca7ed3 --- /dev/null +++ b/frontend/src/components/HomeComponents/Tasks/types.ts @@ -0,0 +1,10 @@ +export type CompletionSummary = Record< + string, + { total: number; completed: number } +>; + +export type LabelMaps = { + options: string[]; + valueToDisplay: Record; + displayToValue: Record; +}; From 174704e22375e44dc47c30b419dec5dbf237c002 Mon Sep 17 00:00:00 2001 From: ANIR1604 Date: Sat, 29 Nov 2025 16:49:43 +0530 Subject: [PATCH 4/5] Resolve Conflicts --- .../HomeComponents/Tasks/StatsMultiSelectFilter.tsx | 10 +++++++++- frontend/src/components/HomeComponents/Tasks/Tasks.tsx | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/HomeComponents/Tasks/StatsMultiSelectFilter.tsx b/frontend/src/components/HomeComponents/Tasks/StatsMultiSelectFilter.tsx index 44d171ec..007d9077 100644 --- a/frontend/src/components/HomeComponents/Tasks/StatsMultiSelectFilter.tsx +++ b/frontend/src/components/HomeComponents/Tasks/StatsMultiSelectFilter.tsx @@ -20,19 +20,23 @@ import { const ALL_ITEMS_VALUE = '__ALL__'; interface StatsMultiSelectFilterProps { + id?: string; title: string; options: string[]; selectedValues: string[]; onSelectionChange: (values: string[]) => void; className?: string; + icon?: React.ReactNode; } export function StatsMultiSelectFilter({ + id, title, options, selectedValues, onSelectionChange, className, + icon, }: StatsMultiSelectFilterProps) { const [open, setOpen] = React.useState(false); @@ -52,6 +56,7 @@ export function StatsMultiSelectFilter({
- +
+ + {icon && {icon}} +
diff --git a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx index 37b45616..005a0ac9 100644 --- a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx +++ b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx @@ -1053,6 +1053,7 @@ export const Tasks = ( icon={} /> } /> Date: Sat, 29 Nov 2025 22:36:44 +0530 Subject: [PATCH 5/5] Rebase to the Basic Version of the CHange --- .../HomeComponents/BottomBar/BottomBar.tsx | 5 +- .../BottomBar/__tests__/BottomBar.test.tsx | 14 -- .../Tasks/StatsMultiSelectFilter.tsx | 128 ------------------ .../components/HomeComponents/Tasks/Tasks.tsx | 5 +- .../Tasks/__tests__/Tasks.test.tsx | 20 +-- frontend/src/components/ui/multi-select.tsx | 16 ++- 6 files changed, 26 insertions(+), 162 deletions(-) delete mode 100644 frontend/src/components/HomeComponents/Tasks/StatsMultiSelectFilter.tsx diff --git a/frontend/src/components/HomeComponents/BottomBar/BottomBar.tsx b/frontend/src/components/HomeComponents/BottomBar/BottomBar.tsx index e93db751..4931a51a 100644 --- a/frontend/src/components/HomeComponents/BottomBar/BottomBar.tsx +++ b/frontend/src/components/HomeComponents/BottomBar/BottomBar.tsx @@ -4,7 +4,6 @@ import { buttonVariants } from '@/components/ui/button'; import { BottomBarProps } from './bottom-bar-utils'; import { Icons } from '@/components/ui/icons'; import { MultiSelectFilter } from '@/components/ui/multi-select'; -import { StatsMultiSelectFilter } from '../Tasks/StatsMultiSelectFilter'; import { Popover, PopoverTrigger, @@ -60,7 +59,7 @@ const BottomBar: React.FC = ({ - = ({ onSelectionChange={setSelectedStatus} className="min-w-[200px]" /> - ({ )), })); -// Mock the StatsMultiSelectFilter component -jest.mock('../../Tasks/StatsMultiSelectFilter', () => ({ - StatsMultiSelectFilter: jest.fn(({ title, selectedValues }) => ( -
- - {title} - - - {selectedValues.length} - -
- )), -})); - const mockProps: BottomBarProps = { projects: ['Project A', 'Project B'], selectedProjects: ['Project A'], diff --git a/frontend/src/components/HomeComponents/Tasks/StatsMultiSelectFilter.tsx b/frontend/src/components/HomeComponents/Tasks/StatsMultiSelectFilter.tsx deleted file mode 100644 index 007d9077..00000000 --- a/frontend/src/components/HomeComponents/Tasks/StatsMultiSelectFilter.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import * as React from 'react'; -import { Check, ChevronsUpDown } from 'lucide-react'; - -import { cn } from '@/components/utils/utils'; -import { Button } from '@/components/ui/button'; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from '@/components/ui/command'; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover'; - -const ALL_ITEMS_VALUE = '__ALL__'; - -interface StatsMultiSelectFilterProps { - id?: string; - title: string; - options: string[]; - selectedValues: string[]; - onSelectionChange: (values: string[]) => void; - className?: string; - icon?: React.ReactNode; -} - -export function StatsMultiSelectFilter({ - id, - title, - options, - selectedValues, - onSelectionChange, - className, - icon, -}: StatsMultiSelectFilterProps) { - const [open, setOpen] = React.useState(false); - - const handleSelect = (value: string) => { - if (value === ALL_ITEMS_VALUE) { - onSelectionChange([]); - setOpen(false); - return; - } - const newSelectedValues = selectedValues.includes(value) - ? selectedValues.filter((item) => item !== value) - : [...selectedValues, value]; - onSelectionChange(newSelectedValues); - }; - - return ( - - - - - - - - - - No results found. - - handleSelect(ALL_ITEMS_VALUE)} - className="text-muted-foreground cursor-pointer" - > - All {title} - - {options.map((option) => { - const isSelected = selectedValues.includes(option); - - // Parse option for stats formatting - const parts = option.split(' '); - const hasStats = parts.length === 3; - - return ( - handleSelect(option)} - > - - {hasStats ? ( - <> - {parts[0]} - - {parts[1]} {parts[2]} - - - ) : ( - option - )} - - ); - })} - - - - - - ); -} diff --git a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx index 005a0ac9..489da6e0 100644 --- a/frontend/src/components/HomeComponents/Tasks/Tasks.tsx +++ b/frontend/src/components/HomeComponents/Tasks/Tasks.tsx @@ -72,7 +72,6 @@ import { import Pagination from './Pagination'; import { url } from '@/components/utils/URLs'; import { MultiSelectFilter } from '@/components/ui/multi-select'; -import { StatsMultiSelectFilter } from './StatsMultiSelectFilter'; import BottomBar from '../BottomBar/BottomBar'; import { addTaskToBackend, @@ -1052,7 +1051,7 @@ export const Tasks = ( data-testid="task-search-bar" icon={} /> - } /> - { }); jest.mock('@/components/ui/multi-select', () => ({ - MultiSelectFilter: jest.fn(({ title }) => ( -
Mocked MultiSelect: {title}
- )), -})); - -jest.mock('../StatsMultiSelectFilter', () => ({ - StatsMultiSelectFilter: jest.fn(({ title, options }) => ( -
- Mocked Stats MultiSelect: {title} + MultiSelectFilter: jest.fn(({ title, options }) => ( +
+ Mocked MultiSelect: {title}
- {JSON.stringify(options)} + {options ? JSON.stringify(options) : ''}
)), @@ -154,12 +148,12 @@ describe('Tasks Component', () => { }); test('displays completion stats inside filter dropdown options', async () => { - const { StatsMultiSelectFilter } = require('../StatsMultiSelectFilter'); + const { MultiSelectFilter } = require('@/components/ui/multi-select'); render(); expect(await screen.findByText('Task 1')).toBeInTheDocument(); - const projectCall = StatsMultiSelectFilter.mock.calls.find( + const projectCall = MultiSelectFilter.mock.calls.find( ([props]: [{ title: string }]) => props.title === 'Projects' ); expect(projectCall).toBeTruthy(); @@ -171,7 +165,7 @@ describe('Tasks Component', () => { ) ).toBe(true); - const tagCall = StatsMultiSelectFilter.mock.calls.find( + const tagCall = MultiSelectFilter.mock.calls.find( ([props]: [{ title: string }]) => props.title === 'Tags' ); expect(tagCall).toBeTruthy(); diff --git a/frontend/src/components/ui/multi-select.tsx b/frontend/src/components/ui/multi-select.tsx index 5089635a..83b10bde 100644 --- a/frontend/src/components/ui/multi-select.tsx +++ b/frontend/src/components/ui/multi-select.tsx @@ -90,6 +90,11 @@ export function MultiSelectFilter({ {options.map((option) => { const isSelected = selectedValues.includes(option); + + // Parse option for stats formatting + const parts = option.split(' '); + const hasStats = parts.length === 3; + return ( - {option} + {hasStats ? ( + <> + {parts[0]} + + {parts[1]} {parts[2]} + + + ) : ( + option + )} ); })}