From 67d8f9e0ddc67aa504d4d1d788ba37a3d72931e2 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 9 Dec 2025 12:45:01 +0200 Subject: [PATCH] Add universal filter to tag details page (#690) * Add universal filter to tag details page * fixup! Add universal filter to tag details page --- e2e/tests/today-view.spec.ts | 6 - frontend/components/Tag/TagDetails.tsx | 314 ++++++++++++++++++++----- 2 files changed, 249 insertions(+), 71 deletions(-) diff --git a/e2e/tests/today-view.spec.ts b/e2e/tests/today-view.spec.ts index 70b2e84..a5c4d76 100644 --- a/e2e/tests/today-view.spec.ts +++ b/e2e/tests/today-view.spec.ts @@ -135,9 +135,6 @@ test.describe('Today', () => { } else { // If section not visible, the settings might be hiding it // Skip this assertion but don't fail the test - console.log( - 'Overdue section not visible - may be hidden by settings' - ); } // Clean up @@ -195,9 +192,6 @@ test.describe('Today', () => { await expect(dueTodayTask).toBeVisible(); } else { // If section not visible, the settings might be hiding it - console.log( - 'Due Today section not visible - may be hidden by settings' - ); } // Clean up diff --git a/frontend/components/Tag/TagDetails.tsx b/frontend/components/Tag/TagDetails.tsx index 5cd1a07..f85dfd9 100644 --- a/frontend/components/Tag/TagDetails.tsx +++ b/frontend/components/Tag/TagDetails.tsx @@ -15,6 +15,7 @@ import { Task } from '../../entities/Task'; import { Note } from '../../entities/Note'; import { Project } from '../../entities/Project'; import TaskList from '../Task/TaskList'; +import GroupedTaskList from '../Task/GroupedTaskList'; import ProjectItem from '../Project/ProjectItem'; import ProjectShareModal from '../Project/ProjectShareModal'; import TagModal from './TagModal'; @@ -40,7 +41,10 @@ const TagDetails: React.FC = () => { // Search, filter, and sort state const [taskSearchQuery, setTaskSearchQuery] = useState(''); const [isSearchExpanded, setIsSearchExpanded] = useState(false); - const [showCompleted, setShowCompleted] = useState(false); + const [taskStatusFilter, setTaskStatusFilter] = useState< + 'all' | 'active' | 'completed' + >('active'); + const [groupBy, setGroupBy] = useState<'none' | 'project'>('none'); const [orderBy, setOrderBy] = useState('created_at:desc'); // Filter projects by current tag @@ -52,6 +56,27 @@ const TagDetails: React.FC = () => { ) ); + const projectLookupList = useMemo(() => { + const map = new Map(); + const addProject = (project?: Project | null) => { + if (!project) return; + const key = + (project.uid && `uid-${project.uid}`) ?? + (project.id !== undefined && project.id !== null + ? `id-${project.id}` + : undefined); + if (!key) return; + if (!map.has(key)) { + map.set(key, project); + } + }; + + allProjects.forEach(addProject); + projects.forEach(addProject); + + return Array.from(map.values()); + }, [allProjects, projects]); + // State for ProjectItem components const [activeDropdown, setActiveDropdown] = useState(null); const [hoveredNoteId, setHoveredNoteId] = useState(null); @@ -89,7 +114,7 @@ const TagDetails: React.FC = () => { let filteredTasks: Task[]; // Filter by completion status - if (showCompleted) { + if (taskStatusFilter === 'completed') { filteredTasks = tasks.filter( (task: Task) => task.status === 'done' || @@ -97,7 +122,7 @@ const TagDetails: React.FC = () => { task.status === 2 || task.status === 3 ); - } else { + } else if (taskStatusFilter === 'active') { filteredTasks = tasks.filter( (task: Task) => task.status !== 'done' && @@ -105,6 +130,8 @@ const TagDetails: React.FC = () => { task.status !== 2 && task.status !== 3 ); + } else { + filteredTasks = tasks; } // Filter by search query @@ -169,7 +196,7 @@ const TagDetails: React.FC = () => { }); return sortedTasks; - }, [tasks, showCompleted, taskSearchQuery, orderBy, t]); + }, [tasks, taskStatusFilter, taskSearchQuery, orderBy, t]); useEffect(() => { const fetchTagData = async () => { @@ -215,6 +242,16 @@ const TagDetails: React.FC = () => { fetchTagData(); }, [uidSlug, t]); + useEffect(() => { + const savedOrderBy = + localStorage.getItem('order_by') || 'created_at:desc'; + setOrderBy(savedOrderBy); + const savedGroupBy = + (localStorage.getItem('tasks_group_by') as 'none' | 'project') || + 'none'; + setGroupBy(savedGroupBy); + }, []); + // Setup native event listener for edit button to avoid React event system conflicts useEffect(() => { const button = editButtonRef.current; @@ -299,6 +336,14 @@ const TagDetails: React.FC = () => { } }; + const handleTaskCompletionToggle = (updatedTask: Task) => { + setTasks((prevTasks) => + prevTasks.map((task) => + task.id === updatedTask.id ? updatedTask : task + ) + ); + }; + const getCompletionPercentage = (project: Project) => { return (project as any).completion_percentage || 0; }; @@ -340,6 +385,22 @@ const TagDetails: React.FC = () => { } }; + const handleSortChange = (order: string) => { + setOrderBy(order); + localStorage.setItem('order_by', order); + }; + + const handleGroupByChange = (value: 'none' | 'project') => { + setGroupBy(value); + localStorage.setItem('tasks_group_by', value); + }; + + const handleStatusChange = (value: 'all' | 'active' | 'completed') => { + setTaskStatusFilter(value); + }; + + const showCompletedTasks = taskStatusFilter !== 'active'; + if (loading) { return (
@@ -499,74 +560,197 @@ const TagDetails: React.FC = () => { setShowCompleted((v) => !v)} - className="w-full flex items-center justify-between text-sm text-gray-700 dark:text-gray-300" - aria-pressed={showCompleted} - aria-label={ - showCompleted - ? t( - 'tasks.hideCompleted', - 'Hide completed tasks' - ) - : t( - 'tasks.showCompleted', - 'Show completed tasks' - ) - } - title={ - showCompleted - ? t( - 'tasks.hideCompleted', - 'Hide completed tasks' - ) - : t( - 'tasks.showCompleted', - 'Show completed tasks' - ) - } - > - - {t( - 'tasks.showCompleted', - 'Show completed' - )} - - - - - + footerContent={ +
+
+
+ {t('tasks.groupBy', 'Group by')} +
+
+ {['none', 'project'].map((val) => ( + + ))} +
+
+
+
+ {t('tasks.show', 'Show')} +
+
+ {[ + { + key: 'active', + label: t( + 'tasks.open', + 'Open' + ), + }, + { + key: 'all', + label: t( + 'tasks.all', + 'All' + ), + }, + { + key: 'completed', + label: t( + 'tasks.completed', + 'Completed' + ), + }, + ].map((opt) => { + const isActive = + taskStatusFilter === + opt.key; + return ( + + ); + })} +
+
+
+
+ {t('tasks.direction', 'Direction')} +
+
+ {[ + { + key: 'asc', + label: t( + 'tasks.ascending', + 'Ascending' + ), + }, + { + key: 'desc', + label: t( + 'tasks.descending', + 'Descending' + ), + }, + ].map((dir) => { + const currentDirection = + orderBy.split(':')[1] || + 'asc'; + const isActive = + currentDirection === + dir.key; + return ( + + ); + })} +
+
+
} />
{displayTasks.length > 0 ? ( - + groupBy === 'project' ? ( + + ) : ( + + ) ) : (

{t('tasks.noTasksAvailable', 'No tasks available.')}