diff --git a/.gitignore b/.gitignore index 5145d4dc35..c1a9df02fa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,15 @@ __pycache__ .env .serena -.claude/settings.local.json PRPs/local PRPs/completed/ /logs/ +.DS_Store +*.log + + +# Claude +.claude/ +claude.local.md +.mcp.json +.next-session.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 4668891611..c40ea8d5e8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -172,6 +172,14 @@ uv run pytest tests/test_service_integration.py -v - `GET /api/projects/{id}/tasks` - Get project tasks - `POST /api/projects/{id}/tasks` - Create task +### Documents + +- `GET /api/projects/{id}/docs` - List project documents (light mode) +- `POST /api/projects/{id}/docs` - Create document (returns full document with created_at/updated_at timestamps) +- `GET /api/projects/{id}/docs/{doc_id}` - Get specific document (full mode with complete content) +- `PUT /api/projects/{id}/docs/{doc_id}` - Update document +- `DELETE /api/projects/{id}/docs/{doc_id}` - Delete document + ## Socket.IO Events Real-time updates via Socket.IO on port 8181: diff --git a/archon-ui-main/src/App.tsx b/archon-ui-main/src/App.tsx index 42af02ac0b..9d8ab267ee 100644 --- a/archon-ui-main/src/App.tsx +++ b/archon-ui-main/src/App.tsx @@ -25,9 +25,19 @@ const AppRoutes = () => { } /> } /> {projectsEnabled ? ( - } /> + <> + } /> + } /> + } /> + } /> + } /> + } /> + ) : ( - } /> + <> + } /> + } /> + )} ); @@ -59,17 +69,17 @@ const AppContent = () => { } }, onReconnected: () => { + console.log('🏥 [Health] onReconnected called - clearing disconnect screen'); setDisconnectScreenActive(false); setDisconnectScreenDismissed(false); - // Refresh the page to ensure all data is fresh - window.location.reload(); + // Don't auto-reload - let the user stay on the current page } }); return () => { serverHealthService.stopMonitoring(); }; - }, [disconnectScreenDismissed]); + }, []); // Only run once on mount, not when disconnectScreenDismissed changes const handleDismissDisconnectScreen = () => { setDisconnectScreenActive(false); diff --git a/archon-ui-main/src/components/project-tasks/DocsTab.tsx b/archon-ui-main/src/components/project-tasks/DocsTab.tsx index 55aebebb6e..ff4295ab52 100644 --- a/archon-ui-main/src/components/project-tasks/DocsTab.tsx +++ b/archon-ui-main/src/components/project-tasks/DocsTab.tsx @@ -1,5 +1,5 @@ -import React, { useState, useEffect } from 'react'; -import { Plus, X, Search, Upload, Link as LinkIcon, Check, Brain, Save, History, Eye, Edit3, Sparkles } from 'lucide-react'; +import React, { useState, useEffect, useCallback } from 'react'; +import { Plus, X, Search, Upload, Link as LinkIcon, Check, Brain, Save, History, Eye, Edit3, Sparkles, Loader2 } from 'lucide-react'; import { Button } from '../ui/Button'; import { knowledgeBaseService, KnowledgeItem } from '../../services/knowledgeBaseService'; import { projectService } from '../../services/projectService'; @@ -501,7 +501,9 @@ Add your content here... /* ——————————————————————————————————————————— */ export const DocsTab = ({ tasks, - project + project, + selectedDocumentId, + onDocumentSelect }: { tasks: Task[]; project?: { @@ -510,6 +512,8 @@ export const DocsTab = ({ created_at?: string; updated_at?: string; } | null; + selectedDocumentId?: string; + onDocumentSelect?: (documentId: string) => void; }) => { // Document state const [documents, setDocuments] = useState([]); @@ -558,31 +562,35 @@ export const DocsTab = ({ const [progressItems, setProgressItems] = useState([]); const { showToast } = useToast(); - // Load project documents from the project data + // Load project documents using light mode for performance const loadProjectDocuments = async () => { - if (!project?.id || !project.docs) return; + if (!project?.id) return; try { setLoading(true); - // Use the docs directly from the project data - const projectDocuments: ProjectDoc[] = project.docs.map((doc: any) => ({ + // Use light mode to get document metadata only (for document cards) + const documentsResponse = await projectService.listDocuments(project.id, false); + + if (!documentsResponse || documentsResponse.length === 0) { + setDocuments([]); + setLoading(false); + return; + } + + // Map to ProjectDoc format for document cards + const projectDocuments: ProjectDoc[] = documentsResponse.map((doc: any) => ({ id: doc.id, title: doc.title || 'Untitled Document', created_at: doc.created_at, updated_at: doc.updated_at, - content: doc.content, + content: doc.content || {}, // May be empty in light mode document_type: doc.document_type || 'document' })); setDocuments(projectDocuments); - // Auto-select first document if available and no document is currently selected - if (projectDocuments.length > 0 && !selectedDocument) { - setSelectedDocument(projectDocuments[0]); - } - - console.log(`Loaded ${projectDocuments.length} documents from project data`); + console.log(`Loaded ${projectDocuments.length} documents in light mode`); } catch (error) { console.error('Failed to load documents:', error); showToast('Failed to load documents', 'error'); @@ -591,6 +599,40 @@ export const DocsTab = ({ } }; + // Load full document content when selecting a specific document + const loadFullDocument = async (docId: string) => { + if (!project?.id || !docId) return; + + // Check if document already has content loaded + const existingDoc = documents.find(d => d.id === docId); + if (existingDoc && existingDoc.content && Object.keys(existingDoc.content).length > 0) { + console.log(`Document ${existingDoc.title} already has content loaded - skipping API call`); + setSelectedDocument(existingDoc); + return; + } + + try { + console.log(`Loading full content for document: ${docId}`); + const fullDoc = await projectService.getDocument(project.id, docId); + + // Update the documents array with the full content + setDocuments(prev => prev.map(doc => + doc.id === docId ? { ...doc, content: fullDoc.content || {} } : doc + )); + + // Set as selected document with full content + const enrichedDoc = documents.find(d => d.id === docId); + if (enrichedDoc) { + setSelectedDocument({ ...enrichedDoc, content: fullDoc.content || {} }); + } + + console.log(`✅ Loaded full content for document: ${fullDoc.title}`); + } catch (error) { + console.error('Failed to load full document:', error); + showToast('Failed to load document content', 'error'); + } + }; + // Create new document from template const createDocumentFromTemplate = async (templateKey: string) => { if (!project?.id) return; @@ -713,6 +755,27 @@ export const DocsTab = ({ setSelectedDocument(null); }, [project?.id]); + // Handle selectedDocumentId from URL - load full content for deep linking + useEffect(() => { + if (selectedDocumentId && documents.length > 0) { + const targetDoc = documents.find(doc => doc.id === selectedDocumentId); + if (targetDoc && targetDoc !== selectedDocument) { + console.log(`🔗 URL specified document: ${targetDoc.title} - loading full content`); + loadFullDocument(selectedDocumentId); + setIsEditing(false); + } + } + }, [selectedDocumentId, documents, selectedDocument]); + + // Handle document selection with URL callback - load full content + const handleDocumentSelect = useCallback((document: ProjectDoc) => { + // Load full document content when selected + loadFullDocument(document.id); + if (onDocumentSelect) { + onDocumentSelect(document.id); + } + }, [onDocumentSelect]); + // Existing knowledge loading function const loadKnowledgeItems = async (knowledgeType?: 'technical' | 'business') => { try { @@ -935,35 +998,59 @@ export const DocsTab = ({ {/* Document Cards Container */}
-
- {documents.map(doc => ( - { - try { - // Call API to delete from database first - await projectService.deleteDocument(project.id, docId); - - // Then remove from local state - setDocuments(prev => prev.filter(d => d.id !== docId)); - if (selectedDocument?.id === docId) { - setSelectedDocument(documents.find(d => d.id !== docId) || null); - } - showToast('Document deleted', 'success'); - } catch (error) { - console.error('Failed to delete document:', error); - showToast('Failed to delete document', 'error'); - } - }} - isDarkMode={isDarkMode} - /> - ))} - - {/* Add New Document Card */} - setShowTemplateModal(true)} /> +
+ {loading ? ( + // Single loading card following the same pattern as projects + <> +
+
+ + + Loading documents... + +
+
+ {/* Skeleton placeholders */} +
+
+
+
+
+ setShowTemplateModal(true)} /> + + ) : ( + <> + {documents.map(doc => ( + { + try { + // Call API to delete from database first + await projectService.deleteDocument(project.id, docId); + + // Then remove from local state + setDocuments(prev => prev.filter(d => d.id !== docId)); + if (selectedDocument?.id === docId) { + setSelectedDocument(documents.find(d => d.id !== docId) || null); + } + showToast('Document deleted', 'success'); + } catch (error) { + console.error('Failed to delete document:', error); + showToast('Failed to delete document', 'error'); + } + }} + isDarkMode={isDarkMode} + projectId={project.id} + /> + ))} + + {/* Add New Document Card */} + setShowTemplateModal(true)} /> + + )}
@@ -977,7 +1064,11 @@ export const DocsTab = ({ viewMode === 'beautiful' ? (
diff --git a/archon-ui-main/src/components/project-tasks/DocumentCard.tsx b/archon-ui-main/src/components/project-tasks/DocumentCard.tsx index e6ec5b9b70..b8784b3278 100644 --- a/archon-ui-main/src/components/project-tasks/DocumentCard.tsx +++ b/archon-ui-main/src/components/project-tasks/DocumentCard.tsx @@ -1,6 +1,8 @@ import React, { useState } from 'react'; -import { Rocket, Code, Briefcase, Users, FileText, X, Plus, Clipboard } from 'lucide-react'; +import { Rocket, Code, Briefcase, Users, FileText, X, Plus, Clipboard, ExternalLink } from 'lucide-react'; import { useToast } from '../../contexts/ToastContext'; +import { handleCopyClick, copyUrlToClipboard } from '../../utils/copyHelpers'; +import { needsCopyLinkButton } from '../../utils/platformDetection'; export interface ProjectDoc { id: string; @@ -17,6 +19,7 @@ interface DocumentCardProps { onSelect: (doc: ProjectDoc) => void; onDelete: (docId: string) => void; isDarkMode: boolean; + projectId: string; } export const DocumentCard: React.FC = ({ @@ -24,7 +27,8 @@ export const DocumentCard: React.FC = ({ isActive, onSelect, onDelete, - isDarkMode + isDarkMode, + projectId }) => { const [showDelete, setShowDelete] = useState(false); const { showToast } = useToast(); @@ -49,18 +53,34 @@ export const DocumentCard: React.FC = ({ } }; - const handleCopyId = (e: React.MouseEvent) => { + const handleCopyId = async (e: React.MouseEvent) => { e.stopPropagation(); - navigator.clipboard.writeText(document.id); - showToast('Document ID copied to clipboard', 'success'); - // Visual feedback - const button = e.currentTarget; + // Capture button reference before async call + const button = e.currentTarget as HTMLButtonElement; const originalHTML = button.innerHTML; - button.innerHTML = '
Copied
'; - setTimeout(() => { - button.innerHTML = originalHTML; - }, 2000); + + try { + const result = await handleCopyClick(e, 'document', projectId, document.id); + + if (result.success) { + const message = result.copied === 'url' + ? 'Document URL copied to clipboard' + : 'Document ID copied to clipboard'; + showToast(message, 'success'); + + // Visual feedback + button.innerHTML = '
Copied
'; + setTimeout(() => { + button.innerHTML = originalHTML; + }, 2000); + } else { + showToast('Failed to copy to clipboard', 'error'); + } + } catch (error) { + console.error('Exception in copy handler:', error); + showToast('Failed to copy to clipboard', 'error'); + } }; return ( @@ -93,20 +113,52 @@ export const DocumentCard: React.FC = ({ {new Date(document.updated_at || document.created_at || Date.now()).toLocaleDateString()}

- {/* ID Display Section - Always visible for active, hover for others */} -
+ {/* ID Display Section - Always visible for active, hover for others, and always visible on mobile */} +
{document.id.slice(0, 8)}... - +
+ {/* Enhanced Copy Document ID Button with shift-click support */} + + + {/* Mobile Copy Link Button - shown on iOS/Android */} + {needsCopyLinkButton() && ( + + )} +
{/* Delete Button */} diff --git a/archon-ui-main/src/components/project-tasks/DraggableTaskCard.tsx b/archon-ui-main/src/components/project-tasks/DraggableTaskCard.tsx index a610030ff1..d53be1a916 100644 --- a/archon-ui-main/src/components/project-tasks/DraggableTaskCard.tsx +++ b/archon-ui-main/src/components/project-tasks/DraggableTaskCard.tsx @@ -1,8 +1,11 @@ import React, { useRef, useState } from 'react'; import { useDrag, useDrop } from 'react-dnd'; -import { Edit, Trash2, RefreshCw, Tag, User, Bot, Clipboard } from 'lucide-react'; +import { Edit, Trash2, RefreshCw, Tag, User, Bot, Clipboard, ExternalLink } from 'lucide-react'; import { Task } from './TaskTableView'; import { ItemTypes, getAssigneeIcon, getAssigneeGlow, getOrderColor, getOrderGlow } from '../../lib/task-utils'; +import { handleCopyClick, copyUrlToClipboard } from '../../utils/copyHelpers'; +import { needsCopyLinkButton } from '../../utils/platformDetection'; +import { useToast } from '../../contexts/ToastContext'; export interface DraggableTaskCardProps { task: Task; @@ -15,6 +18,9 @@ export interface DraggableTaskCardProps { allTasks?: Task[]; hoveredTaskId?: string | null; onTaskHover?: (taskId: string | null) => void; + selectedTaskId?: string; + projectId: string; + currentView?: 'table' | 'board'; } export const DraggableTaskCard = ({ @@ -26,7 +32,11 @@ export const DraggableTaskCard = ({ allTasks = [], hoveredTaskId, onTaskHover, + selectedTaskId, + projectId, + currentView = 'board' }: DraggableTaskCardProps) => { + const { showToast } = useToast(); const [{ isDragging }, drag] = useDrag({ type: ItemTypes.TASK, @@ -90,6 +100,11 @@ export const DraggableTaskCard = ({ const cardScale = 'scale-100'; const cardOpacity = 'opacity-100'; + // Highlight for selected task (blue) and related tasks (cyan) + const selectedHighlight = selectedTaskId === task.id + ? 'bg-gradient-to-br from-blue-100/90 to-purple-100/90 dark:from-blue-900/50 dark:to-purple-900/50 border-blue-400/70 dark:border-blue-500/60 shadow-[0_0_15px_rgba(59,130,246,0.3)]' + : ''; + // Subtle highlight effect for related tasks - applied to the card, not parent const highlightGlow = isHighlighted ? 'border-cyan-400/50 shadow-[0_0_8px_rgba(34,211,238,0.2)]' @@ -107,6 +122,7 @@ export const DraggableTaskCard = ({ return (
drag(drop(node))} + data-task-id={task.id} style={{ perspective: '1000px', transformStyle: 'preserve-3d' @@ -119,7 +135,7 @@ export const DraggableTaskCard = ({ className={`relative w-full min-h-[140px] transform-style-preserve-3d ${isFlipped ? 'rotate-y-180' : ''}`} > {/* Front side with subtle hover effect */} -
+
{/* Priority indicator */}
@@ -196,33 +212,83 @@ export const DraggableTaskCard = ({
{task.assignee?.name || 'User'}
- + +
+ {/* Enhanced Copy Task ID Button with shift-click support */} + + + {/* Mobile Copy Link Button - shown on iOS/Android */} + {needsCopyLinkButton() && ( + + )} +
{/* Back side */} {/* Back side with same hover effect */} -
+
{/* Priority indicator */}
diff --git a/archon-ui-main/src/components/project-tasks/TaskBoardView.tsx b/archon-ui-main/src/components/project-tasks/TaskBoardView.tsx index 0e37c98897..cb0ebefdff 100644 --- a/archon-ui-main/src/components/project-tasks/TaskBoardView.tsx +++ b/archon-ui-main/src/components/project-tasks/TaskBoardView.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState, useCallback } from 'react'; +import React, { useRef, useState, useCallback, useEffect } from 'react'; import { useDrag, useDrop } from 'react-dnd'; import { useToast } from '../../contexts/ToastContext'; import { DeleteConfirmModal } from '../../pages/ProjectPage'; @@ -15,6 +15,9 @@ interface TaskBoardViewProps { onTaskDelete: (task: Task) => void; onTaskMove: (taskId: string, newStatus: Task['status']) => void; onTaskReorder: (taskId: string, targetIndex: number, status: Task['status']) => void; + selectedTaskId?: string; + projectId: string; + currentView?: 'table' | 'board'; } interface ColumnDropZoneProps { @@ -28,9 +31,12 @@ interface ColumnDropZoneProps { onTaskReorder: (taskId: string, targetIndex: number, status: Task['status']) => void; allTasks: Task[]; hoveredTaskId: string | null; + selectedTaskId?: string; onTaskHover: (taskId: string | null) => void; selectedTasks: Set; onTaskSelect: (taskId: string) => void; + projectId: string; + currentView?: 'table' | 'board'; } const ColumnDropZone = ({ @@ -44,9 +50,12 @@ const ColumnDropZone = ({ onTaskReorder, allTasks, hoveredTaskId, + selectedTaskId, onTaskHover, selectedTasks, - onTaskSelect + onTaskSelect, + projectId, + currentView }: ColumnDropZoneProps) => { const ref = useRef(null); @@ -107,7 +116,7 @@ const ColumnDropZone = ({
-
+
{organizedTasks.map((task, index) => ( ))}
@@ -134,7 +146,10 @@ export const TaskBoardView = ({ onTaskComplete, onTaskDelete, onTaskMove, - onTaskReorder + onTaskReorder, + selectedTaskId, + projectId, + currentView = 'board' }: TaskBoardViewProps) => { const [hoveredTaskId, setHoveredTaskId] = useState(null); const [selectedTasks, setSelectedTasks] = useState>(new Set()); @@ -145,6 +160,56 @@ export const TaskBoardView = ({ const { showToast } = useToast(); + // Auto-scroll selected task into view (following projects pattern) + useEffect(() => { + if (selectedTaskId && tasks.length > 0) { + // Small delay to ensure DOM is updated + setTimeout(() => { + const taskCard = document.querySelector(`[data-task-id="${selectedTaskId}"]`); + + if (taskCard) { + // Find the column scroll container (the parent with overflow-y-auto) + let scrollContainer = taskCard.parentElement; + while (scrollContainer && !scrollContainer.classList.contains('overflow-y-auto')) { + scrollContainer = scrollContainer.parentElement; + } + + + if (scrollContainer) { + // Get the position of the card relative to the scroll container + const containerScrollTop = scrollContainer.scrollTop; + const containerHeight = scrollContainer.clientHeight; + + // Get card position relative to scroll container using getBoundingClientRect + const containerRect = scrollContainer.getBoundingClientRect(); + const cardRect = taskCard.getBoundingClientRect(); + const cardOffsetTop = cardRect.top - containerRect.top + containerScrollTop; + const cardHeight = taskCard.clientHeight; + + // Calculate the scroll position to center the card + const targetScrollTop = Math.max(0, cardOffsetTop - (containerHeight / 2) + (cardHeight / 2)); + + + // Store initial scroll position to verify movement + const initialScrollTop = scrollContainer.scrollTop; + + // Check if scroll is actually needed + if (Math.abs(targetScrollTop - initialScrollTop) < 5) { + return; + } + + // Smooth scroll to center the selected task + scrollContainer.scrollTo({ + top: targetScrollTop, + behavior: 'smooth' + }); + + } + } + }, 300); // Small delay to ensure DOM is updated + } + }, [selectedTaskId, tasks]); + // Multi-select handlers const toggleTaskSelection = useCallback((taskId: string) => { setSelectedTasks(prev => { @@ -326,9 +391,12 @@ export const TaskBoardView = ({ onTaskReorder={onTaskReorder} allTasks={tasks} hoveredTaskId={hoveredTaskId} + selectedTaskId={selectedTaskId} onTaskHover={setHoveredTaskId} selectedTasks={selectedTasks} onTaskSelect={toggleTaskSelection} + projectId={projectId} + currentView={currentView} /> {/* In Progress Column */} @@ -343,9 +411,12 @@ export const TaskBoardView = ({ onTaskReorder={onTaskReorder} allTasks={tasks} hoveredTaskId={hoveredTaskId} + selectedTaskId={selectedTaskId} onTaskHover={setHoveredTaskId} selectedTasks={selectedTasks} onTaskSelect={toggleTaskSelection} + projectId={projectId} + currentView={currentView} /> {/* Review Column */} @@ -360,9 +431,12 @@ export const TaskBoardView = ({ onTaskReorder={onTaskReorder} allTasks={tasks} hoveredTaskId={hoveredTaskId} + selectedTaskId={selectedTaskId} onTaskHover={setHoveredTaskId} selectedTasks={selectedTasks} onTaskSelect={toggleTaskSelection} + projectId={projectId} + currentView={currentView} /> {/* Complete Column */} @@ -377,9 +451,12 @@ export const TaskBoardView = ({ onTaskReorder={onTaskReorder} allTasks={tasks} hoveredTaskId={hoveredTaskId} + selectedTaskId={selectedTaskId} onTaskHover={setHoveredTaskId} selectedTasks={selectedTasks} onTaskSelect={toggleTaskSelection} + projectId={projectId} + currentView={currentView} />
diff --git a/archon-ui-main/src/components/project-tasks/TaskTableView.tsx b/archon-ui-main/src/components/project-tasks/TaskTableView.tsx index 795a758aab..464d50cec2 100644 --- a/archon-ui-main/src/components/project-tasks/TaskTableView.tsx +++ b/archon-ui-main/src/components/project-tasks/TaskTableView.tsx @@ -1,9 +1,11 @@ import React, { useState, useCallback, useRef, useEffect } from 'react'; import { useDrag, useDrop } from 'react-dnd'; -import { Check, Trash2, Edit, Tag, User, Bot, Clipboard, Save, Plus } from 'lucide-react'; +import { Check, Trash2, Edit, Tag, User, Bot, Clipboard, Save, Plus, ExternalLink } from 'lucide-react'; import { useToast } from '../../contexts/ToastContext'; import { DeleteConfirmModal } from '../../pages/ProjectPage'; import { projectService } from '../../services/projectService'; +import { handleCopyClick, copyUrlToClipboard } from '../../utils/copyHelpers'; +import { needsCopyLinkButton } from '../../utils/platformDetection'; import { ItemTypes, getAssigneeIcon, getAssigneeGlow, getOrderColor, getOrderGlow } from '../../lib/task-utils'; import { DraggableTaskCard } from './DraggableTaskCard'; @@ -29,6 +31,9 @@ interface TaskTableViewProps { onTaskReorder: (taskId: string, newOrder: number, status: Task['status']) => void; onTaskCreate?: (task: Omit) => Promise; onTaskUpdate?: (taskId: string, updates: Partial) => Promise; + selectedTaskId?: string; + projectId: string; + currentView?: 'table' | 'board'; } const getAssigneeGlassStyle = (assigneeName: 'User' | 'Archon' | 'AI IDE Agent') => { @@ -193,6 +198,7 @@ interface DraggableTaskRowProps { onTaskUpdate?: (taskId: string, updates: Partial) => Promise; tasksInStatus: Task[]; style?: React.CSSProperties; + selectedTaskId?: string; } const DraggableTaskRow = ({ @@ -204,7 +210,8 @@ const DraggableTaskRow = ({ onTaskReorder, onTaskUpdate, tasksInStatus, - style + style, + selectedTaskId }: DraggableTaskRowProps) => { const [editingField, setEditingField] = useState(null); const [isHovering, setIsHovering] = useState(false); @@ -266,12 +273,18 @@ const DraggableTaskRow = ({ } }; + const isHighlighted = task.id === selectedTaskId; + return ( drag(drop(node))} + data-task-id={task.id} className={` group transition-all duration-200 cursor-move - ${index % 2 === 0 ? 'bg-white/50 dark:bg-black/50' : 'bg-gray-50/80 dark:bg-gray-900/30'} + ${isHighlighted + ? 'bg-gradient-to-r from-cyan-100/90 to-cyan-50/90 dark:from-cyan-900/50 dark:to-cyan-800/50 ring-2 ring-cyan-400 dark:ring-cyan-400 shadow-[0_0_15px_rgba(34,211,238,0.4)]' + : index % 2 === 0 ? 'bg-white/50 dark:bg-black/50' : 'bg-gray-50/80 dark:bg-gray-900/30' + } hover:bg-gradient-to-r hover:from-cyan-50/70 hover:to-purple-50/70 dark:hover:from-cyan-900/20 dark:hover:to-purple-900/20 border-b border-gray-200 dark:border-gray-800 last:border-b-0 ${isDragging ? 'opacity-50 scale-105 shadow-lg z-50' : ''} @@ -392,26 +405,71 @@ const DraggableTaskRow = ({ >
@@ -565,9 +623,12 @@ export const TaskTableView = ({ onTaskDelete, onTaskReorder, onTaskCreate, - onTaskUpdate + onTaskUpdate, + selectedTaskId, + projectId, + currentView = 'table' }: TaskTableViewProps) => { - const [statusFilter, setStatusFilter] = useState('backlog'); + const [statusFilter, setStatusFilter] = useState('all'); // State for delete confirmation modal const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); @@ -575,6 +636,63 @@ export const TaskTableView = ({ const { showToast } = useToast(); + // Auto-select status filter when a task is selected via deep URL + useEffect(() => { + if (selectedTaskId && tasks.length > 0) { + const selectedTask = tasks.find(task => task.id === selectedTaskId); + if (selectedTask) { + // If the selected task is not visible in the current filter, switch to its status or 'all' + if (statusFilter !== 'all' && statusFilter !== selectedTask.status) { + setStatusFilter(selectedTask.status); + } + } + } + }, [selectedTaskId, tasks, statusFilter]); + + // Auto-scroll selected task into view (following projects pattern) + useEffect(() => { + if (selectedTaskId && tasks.length > 0) { + // Small delay to ensure DOM is updated and status filter has been applied + setTimeout(() => { + const taskRow = document.querySelector(`[data-task-id="${selectedTaskId}"]`); + const scrollContainer = tableContainerRef.current; + + + if (taskRow && scrollContainer) { + // Get the position of the row relative to the scroll container + const containerScrollTop = scrollContainer.scrollTop; + const containerHeight = scrollContainer.clientHeight; + + // Get row position relative to scroll container using getBoundingClientRect + const containerRect = scrollContainer.getBoundingClientRect(); + const rowRect = taskRow.getBoundingClientRect(); + const rowOffsetTop = rowRect.top - containerRect.top + containerScrollTop; + const rowHeight = taskRow.clientHeight; + + // Calculate the scroll position to center the row + const targetScrollTop = Math.max(0, rowOffsetTop - (containerHeight / 2) + (rowHeight / 2)); + + + // Store initial scroll position to verify movement + const initialScrollTop = scrollContainer.scrollTop; + + + // Check if scroll is actually needed + if (Math.abs(targetScrollTop - initialScrollTop) < 5) { + return; + } + + // Smooth scroll to center the selected task + scrollContainer.scrollTo({ + top: targetScrollTop, + behavior: 'smooth' + }); + + } + }, 300); // Slightly longer delay to ensure status filter changes are applied + } + }, [selectedTaskId, tasks, statusFilter]); // Include statusFilter so it re-runs after filter changes + // Refs for scroll fade effect const tableContainerRef = useRef(null); const tableRef = useRef(null); @@ -783,7 +901,7 @@ export const TaskTableView = ({ {/* Scrollable table container */}
diff --git a/archon-ui-main/src/components/project-tasks/TasksTab.tsx b/archon-ui-main/src/components/project-tasks/TasksTab.tsx index 03a6a61ed6..03c2747826 100644 --- a/archon-ui-main/src/components/project-tasks/TasksTab.tsx +++ b/archon-ui-main/src/components/project-tasks/TasksTab.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Table, LayoutGrid, Plus, Wifi, WifiOff, List } from 'lucide-react'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; +import { useSearchParams, useNavigate } from 'react-router-dom'; import { Toggle } from '../ui/Toggle'; import { projectService } from '../../services/projectService'; @@ -55,18 +56,44 @@ const mapDatabaseTaskToUITask = (dbTask: any): Task => { export const TasksTab = ({ initialTasks, onTasksChange, - projectId + projectId, + selectedTaskId, + onTaskSelect }: { initialTasks: Task[]; onTasksChange: (tasks: Task[]) => void; projectId: string; + selectedTaskId?: string; + onTaskSelect?: (taskId: string) => void; }) => { - const [viewMode, setViewMode] = useState<'table' | 'board'>('board'); + const [searchParams, setSearchParams] = useSearchParams(); + const navigate = useNavigate(); + // Initialize view mode from URL parameters, defaulting to 'board' + const [viewMode, setViewMode] = useState<'table' | 'board'>(() => { + const viewParam = searchParams.get('view'); + return (viewParam === 'table' || viewParam === 'board') ? viewParam : 'board'; + }); const [tasks, setTasks] = useState([]); const [editingTask, setEditingTask] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [projectFeatures, setProjectFeatures] = useState([]); const [isLoadingFeatures, setIsLoadingFeatures] = useState(false); + + // Update URL parameters when view mode changes + const updateViewMode = useCallback((newViewMode: 'table' | 'board') => { + const newSearchParams = new URLSearchParams(searchParams); + newSearchParams.set('view', newViewMode); + setSearchParams(newSearchParams, { replace: true }); + setViewMode(newViewMode); + }, [searchParams, setSearchParams]); + + // Update view mode when URL parameters change + useEffect(() => { + const viewParam = searchParams.get('view'); + if (viewParam === 'table' || viewParam === 'board') { + setViewMode(viewParam); + } + }, [searchParams]); const [isSavingTask, setIsSavingTask] = useState(false); const [isWebSocketConnected, setIsWebSocketConnected] = useState(false); @@ -587,6 +614,9 @@ export const TasksTab = ({ onTaskReorder={handleTaskReorder} onTaskCreate={createTaskInline} onTaskUpdate={updateTaskInline} + selectedTaskId={selectedTaskId} + projectId={projectId} + currentView={viewMode} /> ) : ( )}
@@ -644,7 +677,7 @@ export const TasksTab = ({ {/* View Toggle Controls */}