import React, { useState, useEffect, useRef } from 'react'; import { useParams, useNavigate, Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { TrashIcon, TagIcon, QueueListIcon, StarIcon, InformationCircleIcon, PencilSquareIcon, } from '@heroicons/react/24/outline'; import { StarIcon as StarIconSolid } from '@heroicons/react/24/solid'; import { Task } from '../entities/Task'; import { Note } from '../entities/Note'; import { Project } from '../entities/Project'; import TaskList from './Task/TaskList'; import ProjectItem from './Project/ProjectItem'; import ConfirmDialog from './Shared/ConfirmDialog'; import { searchUniversal } from '../utils/searchService'; import { getApiPath } from '../config/paths'; interface View { id: number; uid: string; name: string; search_query: string | null; filters: string[]; priority: string | null; due: string | null; tags: string[]; is_pinned: boolean; } const ViewDetail: React.FC = () => { const { t } = useTranslation(); const { uid } = useParams<{ uid: string }>(); const navigate = useNavigate(); const [view, setView] = useState(null); const [tasks, setTasks] = useState([]); const [notes, setNotes] = useState([]); const [projects, setProjects] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isEditingName, setIsEditingName] = useState(false); const [editedName, setEditedName] = useState(''); const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false); const [showCriteriaDropdown, setShowCriteriaDropdown] = useState(false); // Pagination state const [offset, setOffset] = useState(0); const [hasMore, setHasMore] = useState(false); const [isLoadingMore, setIsLoadingMore] = useState(false); const [totalCount, setTotalCount] = useState(0); const limit = 20; // State for ProjectItem and Note components const [activeDropdown, setActiveDropdown] = useState(null); const [hoveredNoteId, setHoveredNoteId] = useState(null); const [, setProjectToDelete] = useState(null); // Ref for dropdown and title edit const criteriaDropdownRef = useRef(null); const titleInputRef = useRef(null); useEffect(() => { fetchViewAndResults(); }, [uid]); // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( criteriaDropdownRef.current && !criteriaDropdownRef.current.contains(event.target as Node) ) { setShowCriteriaDropdown(false); } }; if (showCriteriaDropdown) { document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; } }, [showCriteriaDropdown]); // Save title when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( titleInputRef.current && !titleInputRef.current.contains(event.target as Node) ) { handleSaveName(); } }; if (isEditingName) { document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; } }, [isEditingName, editedName]); const fetchViewAndResults = async (resetPagination = true) => { if (!uid) return; try { // Fetch view details const viewResponse = await fetch(getApiPath(`views/${uid}`), { credentials: 'include', }); if (!viewResponse.ok) { navigate('/views'); return; } const viewData = await viewResponse.json(); setView(viewData); const currentOffset = resetPagination ? 0 : offset; // Fetch search results with pagination and exclude subtasks const response = await searchUniversal({ query: viewData.search_query || '', filters: viewData.filters, priority: viewData.priority || undefined, due: viewData.due || undefined, tags: viewData.tags && viewData.tags.length > 0 ? viewData.tags : undefined, limit: limit, offset: currentOffset, excludeSubtasks: true, }); // Separate results by type const taskResults: Task[] = []; const noteResults: Note[] = []; const projectResults: Project[] = []; response.results.forEach((result) => { if (result.type === 'Task') { taskResults.push(result as any); } else if (result.type === 'Note') { noteResults.push(result as any); } else if (result.type === 'Project') { projectResults.push(result as any); } }); if (resetPagination) { setTasks(taskResults); setNotes(noteResults); setProjects(projectResults); setOffset(limit); } else { setTasks((prev) => [...prev, ...taskResults]); setNotes((prev) => [...prev, ...noteResults]); setProjects((prev) => [...prev, ...projectResults]); setOffset((prev) => prev + limit); } setHasMore(response.pagination?.hasMore || false); if (response.pagination) { setTotalCount(response.pagination.total); } } catch (error) { console.error('Error fetching view:', error); navigate('/views'); } finally { setIsLoading(false); setIsLoadingMore(false); } }; const loadMore = async () => { if (!hasMore || isLoadingMore) return; setIsLoadingMore(true); await fetchViewAndResults(false); }; // Task handlers const handleTaskUpdate = async (updatedTask: Task) => { try { const response = await fetch(getApiPath(`task/${updatedTask.id}`), { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updatedTask), }); if (response.ok) { setTasks((prevTasks) => prevTasks.map((task) => task.id === updatedTask.id ? updatedTask : task ) ); } } catch (error) { console.error('Error updating task:', error); } }; const handleTaskDelete = async (taskId: number) => { try { const response = await fetch(getApiPath(`task/${taskId}`), { method: 'DELETE', }); if (response.ok) { setTasks((prevTasks) => prevTasks.filter((task) => task.id !== taskId) ); } } catch (error) { console.error('Error deleting task:', error); } }; const handleToggleToday = async (taskId: number, task?: Task) => { try { const { toggleTaskToday } = await import('../utils/tasksService'); const updatedTask = await toggleTaskToday(taskId, task); setTasks((prevTasks) => prevTasks.map((task) => task.id === taskId ? { ...task, today: updatedTask.today, today_move_count: updatedTask.today_move_count, } : task ) ); } catch (error) { console.error('Error toggling today status:', error); } }; const getCompletionPercentage = (project: Project) => { return (project as any).completion_percentage || 0; }; const handleEditProject = (project: Project) => { if (project.uid) { const slug = project.name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, ''); navigate(`/project/${project.uid}-${slug}/edit`); } else { navigate(`/project/${project.id}/edit`); } }; const handleEditName = () => { if (view) { setEditedName(view.name); setIsEditingName(true); } }; const handleSaveName = async () => { if (!view || !editedName.trim()) return; try { const response = await fetch(getApiPath(`views/${view.uid}`), { method: 'PATCH', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ name: editedName.trim(), }), }); if (response.ok) { setView({ ...view, name: editedName.trim() }); setIsEditingName(false); window.dispatchEvent(new CustomEvent('viewUpdated')); } } catch (error) { console.error('Error updating view name:', error); } }; const handleCancelEdit = () => { setIsEditingName(false); setEditedName(''); }; const togglePin = async () => { if (!view) return; try { const response = await fetch(getApiPath(`views/${view.uid}`), { method: 'PATCH', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify({ is_pinned: !view.is_pinned, }), }); if (response.ok) { setView({ ...view, is_pinned: !view.is_pinned }); window.dispatchEvent(new CustomEvent('viewUpdated')); } } catch (error) { console.error('Error toggling pin:', error); } }; const handleDeleteView = async () => { if (!view) return; try { const response = await fetch(getApiPath(`views/${view.uid}`), { method: 'DELETE', credentials: 'include', }); if (response.ok) { window.dispatchEvent(new CustomEvent('viewUpdated')); navigate('/views'); } } catch (error) { console.error('Error deleting view:', error); } finally { setIsConfirmDialogOpen(false); } }; const openConfirmDialog = () => { setIsConfirmDialogOpen(true); }; const closeConfirmDialog = () => { setIsConfirmDialogOpen(false); }; if (isLoading) { return (
Loading view...
); } if (!view) { return null; } return (
{/* Header */}
{isEditingName ? ( setEditedName(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { handleSaveName(); } else if (e.key === 'Escape') { handleCancelEdit(); } }} className="text-2xl font-light text-gray-900 dark:text-white bg-transparent border-b-2 border-blue-500 focus:outline-none w-full max-w-2xl" autoFocus /> ) : (

{view.name}

)}
{showCriteriaDropdown && (

{t('views.searchCriteria')}

{view.filters.length > 0 && (

{t('views.entityTypes')}

{view.filters.map( (filter) => ( {filter} ) )}
)} {view.search_query && (

{t('views.searchText')}

" {view.search_query} "

)} {view.priority && (

{t('views.priority')}

{view.priority}
)} {view.due && (

{t('views.dueDate')}

{view.due.replace( /_/g, ' ' )}
)} {view.tags && view.tags.length > 0 && (

{t('views.tags')}

{view.tags.map( (tag) => ( {tag} ) )}
)} {!view.filters.length && !view.search_query && !view.priority && !view.due && (!view.tags || view.tags.length === 0) && (

{t( 'views.noCriteriaSet' )}

)}
)}
{/* Tasks Section */} {tasks.length > 0 && (

{t('tasks.title')} ({totalCount})

{/* Load more button */} {hasMore && (
)} {/* Pagination info */} {tasks.length > 0 && (
{t( 'tasks.showingItems', 'Showing {{current}} of {{total}} items', { current: tasks.length, total: totalCount, } )}
)}
)} {/* Notes Section */} {notes.length > 0 && (

{t('notes.title')} ({notes.length})

    {notes.map((note) => (
  • setHoveredNoteId(note.uid || null) } onMouseLeave={() => setHoveredNoteId(null)} >
    {note.title} {/* Tags */} {((note.tags && note.tags.length > 0) || (note.Tags && note.Tags.length > 0)) && ( <> {( note.tags || note.Tags || [] ).map((noteTag) => ( ))} )}
  • ))}
)} {/* Projects Section */} {projects.length > 0 && (

{t('projects.title')} ({projects.length})

{projects.map((project) => { return ( getCompletionPercentage(project) } activeDropdown={activeDropdown} setActiveDropdown={setActiveDropdown} handleEditProject={handleEditProject} setProjectToDelete={setProjectToDelete} setIsConfirmDialogOpen={() => {}} onOpenShare={() => { /* noop in view detail */ }} /> ); })}
)} {/* Empty State */} {tasks.length === 0 && notes.length === 0 && projects.length === 0 && (

No items found matching the view criteria

)} {/* Confirm Delete Dialog */} {isConfirmDialogOpen && ( )}
); }; export default ViewDetail;