Skip to content
This repository was archived by the owner on Sep 18, 2025. It is now read-only.

Commit e5c8deb

Browse files
authored
Merge pull request #104 from birdup000/feature/task-panel-redesign-tests
Feature/task panel redesign tests
2 parents 1f289e9 + 1b40c19 commit e5c8deb

23 files changed

Lines changed: 10082 additions & 3136 deletions

__mocks__/fileMock.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = 'test-file-stub';

app/components/TaskManager.tsx

Lines changed: 64 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import React from 'react';
44
import TaskList from './tasks/TaskList';
55
import BoardView from './tasks/BoardView';
66
import MindMapView from './tasks/MindMapView';
7-
import TaskDetailPanel from './tasks/TaskDetailPanel';
7+
// import TaskDetailPanel from './tasks/TaskDetailPanel'; // To be replaced
8+
import NewTaskView from './tasks/NewTaskView'; // Import the new task view
89
import { Task } from '../types/task';
910
import { useTasks } from '../hooks/useTasks';
1011

@@ -36,74 +37,73 @@ const TaskManager: React.FC = () => {
3637
}
3738

3839
return (
39-
<div className="h-full flex">
40-
{/* Main Content */}
41-
<div className="flex-1 flex flex-col">
42-
{/* View Toggle */}
43-
<div className="p-4 border-b border-border-default">
44-
<div className="flex items-center gap-4">
45-
<button
46-
onClick={() => setViewMode('list')}
47-
className={`px-4 py-2 rounded-md ${
48-
viewMode === 'list'
49-
? 'bg-accent-primary text-white'
50-
: 'text-text-secondary hover:bg-hover'
51-
}`}
52-
>
53-
List View
54-
</button>
55-
<button
56-
onClick={() => setViewMode('board')}
57-
className={`px-4 py-2 rounded-md ${
58-
viewMode === 'board'
59-
? 'bg-accent-primary text-white'
60-
: 'text-text-secondary hover:bg-hover'
61-
}`}
62-
>
63-
Board View
64-
</button>
65-
<button
66-
onClick={() => setViewMode('mindmap')}
67-
className={`px-4 py-2 rounded-md ${
68-
viewMode === 'mindmap'
69-
? 'bg-accent-primary text-white'
70-
: 'text-text-secondary hover:bg-hover'
71-
}`}
72-
>
73-
Mind Map
74-
</button>
40+
<div className="h-full flex flex-col md:flex-row overflow-hidden"> {/* Ensure overflow is handled */}
41+
{/* Main Task List / Board / MindMap Area */}
42+
{/* This part will dynamically take less space if a task is selected, or full width if not */}
43+
<div
44+
className={`flex-1 flex flex-col transition-all duration-300 ease-in-out overflow-y-auto custom-scrollbar
45+
${selectedTaskId ? 'md:w-[calc(100%-360px)] lg:w-[calc(100%-400px)]' : 'w-full'}`}
46+
>
47+
{/* View Toggle Buttons */}
48+
<div className="p-4 border-b border-border-default sticky top-0 bg-bg-primary z-10">
49+
<div className="flex items-center gap-2 md:gap-4">
50+
{(['list', 'board', 'mindmap'] as ViewMode[]).map((mode) => (
51+
<button
52+
key={mode}
53+
onClick={() => setViewMode(mode)}
54+
className={`btn ${viewMode === mode ? 'btn-primary' : 'btn-ghost'} btn-sm capitalize`}
55+
aria-pressed={viewMode === mode}
56+
>
57+
{mode} View
58+
</button>
59+
))}
7560
</div>
7661
</div>
7762

78-
{/* Task Views */}
79-
{viewMode === 'list' ? (
80-
<TaskList
81-
tasks={tasks}
82-
onTaskSelect={setSelectedTaskId}
83-
onTaskStatusChange={updateTaskStatus}
84-
/>
85-
) : viewMode === 'board' ? (
86-
<BoardView
87-
tasks={tasks}
88-
onTaskSelect={setSelectedTaskId}
89-
onTaskStatusChange={updateTaskStatus}
90-
/>
91-
) : (
92-
<MindMapView
93-
tasks={tasks}
94-
onTaskSelect={setSelectedTaskId}
95-
onTaskCreate={addTask}
96-
/>
97-
)}
63+
{/* Conditional Task Views */}
64+
<div className="flex-1 p-2 md:p-4"> {/* Added padding for content within scrollable area */}
65+
{viewMode === 'list' && (
66+
<TaskList
67+
tasks={tasks}
68+
onTaskSelect={setSelectedTaskId}
69+
onTaskStatusChange={updateTaskStatus}
70+
selectedTaskId={selectedTaskId} // Pass selectedTaskId for highlighting
71+
/>
72+
)}
73+
{viewMode === 'board' && (
74+
<BoardView
75+
tasks={tasks}
76+
onTaskSelect={setSelectedTaskId}
77+
onTaskStatusChange={updateTaskStatus}
78+
/>
79+
)}
80+
{viewMode === 'mindmap' && (
81+
<MindMapView
82+
tasks={tasks}
83+
onTaskSelect={setSelectedTaskId}
84+
onTaskCreate={addTask}
85+
/>
86+
)}
87+
</div>
9888
</div>
9989

100-
{/* Task Detail Panel */}
101-
<div className="w-96 border-l border-border-default">
102-
<TaskDetailPanel
103-
task={selectedTask}
104-
onClose={() => setSelectedTaskId(null)}
105-
onSave={updateTask}
106-
/>
90+
{/* New Task View (Task Detail Panel) - slides in or is present */}
91+
{/* The NewTaskView itself handles its width (md:w-[320px] lg:w-[360px]) */}
92+
{/* We control its visibility and animation here */}
93+
<div
94+
className={`fixed inset-y-0 right-0 md:relative md:inset-auto bg-bg-secondary border-l border-border-default shadow-xl md:shadow-none
95+
transform transition-transform duration-300 ease-in-out z-20
96+
${selectedTaskId ? 'translate-x-0' : 'translate-x-full md:translate-x-0 md:hidden'}`} // Control visibility and slide-in
97+
style={{ width: selectedTaskId ? 'clamp(320px, 90vw, 420px)' : '0px' }} // Dynamic width for mobile overlay, fixed for desktop
98+
>
99+
{selectedTaskId && ( // Only render if a task is selected
100+
<NewTaskView
101+
taskId={selectedTaskId}
102+
onClose={() => setSelectedTaskId(null)}
103+
// onSave={updateTask} // NewTaskView handles its own saving via its sub-components & useTasks hook
104+
// onDelete={deleteTask} // Similarly, can be handled within or passed if preferred
105+
/>
106+
)}
107107
</div>
108108
</div>
109109
);

app/components/layout/TaskPanelLayout.tsx

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,14 @@ const TaskPanelLayout: React.FC<TaskPanelLayoutProps> = ({
1717
}) => {
1818
const [isLeftPanelOpen, setIsLeftPanelOpen] = React.useState(true);
1919
const [isRightPanelOpen, setIsRightPanelOpen] = React.useState(false);
20-
const [isFocusMode, setIsFocusMode] = React.useState(false);
2120

2221
// Update right panel state when visibility changes
2322
React.useEffect(() => {
2423
setIsRightPanelOpen(isRightPanelVisible);
2524
}, [isRightPanelVisible]);
2625

2726
return (
28-
<div className={`flex h-[100dvh] bg-bg-primary text-text-primary overflow-hidden ${isFocusMode ? 'focus-mode' : ''}`} role="application" aria-label="Task management panel">
27+
<div className="flex h-[100dvh] bg-bg-primary text-text-primary overflow-hidden" role="application" aria-label="Task management panel">
2928
{/* Mobile Navigation Toggle */}
3029
<div className="fixed top-4 left-4 md:hidden z-40">
3130
<button
@@ -53,31 +52,6 @@ const TaskPanelLayout: React.FC<TaskPanelLayoutProps> = ({
5352
</button>
5453
</div>
5554

56-
{/* Focus Mode Toggle */}
57-
<div className="fixed top-4 right-4 z-40">
58-
<button
59-
onClick={() => setIsFocusMode(!isFocusMode)}
60-
className="p-3 bg-bg-secondary rounded-md hover:bg-accent-muted border border-border-default
61-
shadow-md focus:outline-none focus:ring-2 focus:ring-accent-focus focus:ring-offset-1 focus:ring-offset-bg-primary
62-
transition-colors duration-200"
63-
aria-label={isFocusMode ? 'Exit focus mode' : 'Enter focus mode'}
64-
aria-pressed={isFocusMode}
65-
>
66-
<svg
67-
className="w-6 h-6"
68-
fill="none"
69-
stroke="currentColor"
70-
viewBox="0 0 24 24"
71-
aria-hidden="true"
72-
>
73-
{isFocusMode ? (
74-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
75-
) : (
76-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10a3 3 0 11-6 0 3 3 0 016 0z" />
77-
)}
78-
</svg>
79-
</button>
80-
</div>
8155

8256
{/* Mobile Navigation Backdrop */}
8357
{isLeftPanelOpen && (
@@ -94,7 +68,7 @@ const TaskPanelLayout: React.FC<TaskPanelLayoutProps> = ({
9468
className={`fixed md:relative left-0 top-0 h-full bg-bg-secondary
9569
w-80 md:w-72 lg:w-80 border-r border-border-default transform md:translate-x-0
9670
panel-transition duration-300 ease-in-out z-30 md:z-10 ${
97-
isLeftPanelOpen && !isFocusMode ? 'translate-x-0 shadow-xl md:shadow-none' : '-translate-x-full md:translate-x-0'
71+
isLeftPanelOpen ? 'translate-x-0 shadow-xl md:shadow-none' : '-translate-x-full md:translate-x-0'
9872
}`}
9973
role="navigation"
10074
aria-label="Main navigation"
@@ -120,7 +94,7 @@ const TaskPanelLayout: React.FC<TaskPanelLayoutProps> = ({
12094
{/* Main Task List Panel */}
12195
<main
12296
className={`flex-1 min-w-0 bg-bg-primary overflow-hidden transition-all duration-300
123-
${isRightPanelOpen && !isFocusMode ? 'md:mr-[400px] lg:mr-[420px]' : 'md:mr-0'}`}
97+
${isRightPanelOpen ? 'md:mr-[400px] lg:mr-[420px]' : 'md:mr-0'}`}
12498
role="main"
12599
aria-label="Task list"
126100
>
@@ -135,7 +109,7 @@ const TaskPanelLayout: React.FC<TaskPanelLayoutProps> = ({
135109
w-[90vw] max-w-[400px] md:w-[400px] lg:w-[420px]
136110
border-l border-border-default transform panel-transition z-20
137111
shadow-xl md:shadow-none ${
138-
isRightPanelOpen && !isFocusMode ? 'translate-x-0' : 'translate-x-full'
112+
isRightPanelOpen ? 'translate-x-0' : 'translate-x-full'
139113
}`}
140114
role="complementary"
141115
aria-label="Task details panel"
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
"use client";
2+
3+
import React, { useEffect, useState } from 'react';
4+
import { Task } from '../../types/task';
5+
// import { User } from '../../types/user'; // Assuming a User type might be needed later
6+
import { useTasks } from '../../hooks/useTasks'; // To interact with tasks
7+
8+
// Placeholder for sub-components that will be created in later steps
9+
import TaskHeader from './TaskHeader';
10+
import TaskDescription from './TaskDescription';
11+
import SubTasksList from './SubTasksList';
12+
import TaskComments from './TaskComments';
13+
// import TaskAssignment from './TaskAssignment'; // TaskAssignment is now part of TaskAttributesSidebar
14+
import TaskAttributesSidebar from './TaskAttributesSidebar';
15+
16+
interface NewTaskViewProps {
17+
taskId: string | null;
18+
onClose: () => void;
19+
}
20+
21+
const NewTaskView: React.FC<NewTaskViewProps> = ({ taskId, onClose }) => {
22+
const { tasks, updateTask, getTaskById } = useTasks(); // Assuming getTaskById is available or can be added
23+
const [task, setTask] = useState<Task | null>(null);
24+
const [loading, setLoading] = useState<boolean>(false);
25+
const [error, setError] = useState<string | null>(null);
26+
27+
// Ref for the main scrollable container to manage focus and scroll for navigation.
28+
const mainContentRef = React.useRef<HTMLDivElement>(null);
29+
30+
31+
useEffect(() => {
32+
if (taskId) {
33+
setLoading(true);
34+
setError(null);
35+
const currentTask = tasks.find(t => t.id === taskId);
36+
if (currentTask) {
37+
setTask(currentTask);
38+
// Focus the main content area when a new task is loaded for keyboard navigation
39+
mainContentRef.current?.focus();
40+
} else {
41+
setError('Task not found.');
42+
}
43+
setLoading(false);
44+
} else {
45+
setTask(null);
46+
}
47+
}, [taskId, tasks]);
48+
49+
const handleUpdateTask = async (updatedFields: Partial<Task>) => {
50+
if (task) {
51+
const updatedTaskData = { ...task, ...updatedFields };
52+
// Optimistic update
53+
setTask(updatedTaskData);
54+
try {
55+
await updateTask(updatedTaskData);
56+
// Optionally re-fetch or confirm save
57+
} catch (e) {
58+
setError('Failed to update task.');
59+
// Revert optimistic update if necessary
60+
setTask(tasks.find(t => t.id === task.id) || null);
61+
}
62+
}
63+
};
64+
65+
// Keyboard navigation handler
66+
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
67+
if (!mainContentRef.current) return;
68+
69+
// Using J for down, K for up (like Vim)
70+
// This is a basic implementation. A more robust solution would involve
71+
// identifying focusable elements within sections and moving between them.
72+
if (event.key === 'j') {
73+
event.preventDefault();
74+
mainContentRef.current.scrollBy({ top: 100, behavior: 'smooth' }); // Scroll down
75+
} else if (event.key === 'k') {
76+
event.preventDefault();
77+
mainContentRef.current.scrollBy({ top: -100, behavior: 'smooth' }); // Scroll up
78+
} else if (event.key === 'Escape') {
79+
// Check if any input/textarea is focused, if so, let them handle Esc first (e.g. to blur)
80+
const activeElement = document.activeElement;
81+
if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {
82+
(activeElement as HTMLElement).blur();
83+
} else {
84+
onClose(); // Close panel if no input is focused
85+
}
86+
}
87+
// Other shortcuts can be added here:
88+
// 'e' to edit description/title, 'c' to comment, etc.
89+
};
90+
91+
if (!taskId) {
92+
return null; // Or a message indicating no task is selected
93+
}
94+
95+
if (loading) {
96+
return <div className="p-6 text-center">Loading task details...</div>;
97+
}
98+
99+
if (error) {
100+
return <div className="p-6 text-center text-red-500">{error}</div>;
101+
}
102+
103+
if (!task) {
104+
return <div className="p-6 text-center">Select a task to view its details.</div>;
105+
}
106+
107+
return (
108+
// Add onKeyDown to the outermost div of the task view that should capture these events.
109+
// It might be better on the `main` element if the sidebar shouldn't capture these.
110+
<div
111+
className="flex flex-col md:flex-row h-full bg-bg-primary text-text-primary focus:outline-none" // Added focus:outline-none
112+
role="main"
113+
onKeyDown={handleKeyDown}
114+
tabIndex={-1}
115+
ref={mainContentRef}
116+
>
117+
{/* Main Content Area (Left/Center) */}
118+
<div className="flex-1 flex flex-col min-w-0 max-h-full"> {/* Ensure max-h-full for proper scrolling */}
119+
<TaskHeader task={task} onUpdateTask={handleUpdateTask} onClosePanel={onClose} />
120+
121+
<main
122+
className="flex-1 overflow-y-auto p-4 md:p-6 space-y-6 divide-y divide-border-default/50 focus:outline-none custom-scrollbar" // Softer divider, custom scrollbar
123+
>
124+
<TaskDescription task={task} onUpdateTask={handleUpdateTask} />
125+
<SubTasksList parentTask={task} onUpdateParentTask={handleUpdateTask} />
126+
<TaskComments taskId={task.id} currentUser={{id: 'user-1', name: 'Current User', avatarUrl: 'https://i.pravatar.cc/150?u=current'}} />
127+
</main>
128+
129+
{/* Removed redundant Quick Actions bar, as they are now contextual */}
130+
</div>
131+
132+
{/* Attributes Sidebar (Right) */}
133+
<TaskAttributesSidebar task={task} onUpdateTask={handleUpdateTask} />
134+
{/*
135+
Original structure idea:
136+
<div className="flex flex-1 overflow-hidden">
137+
<main className="flex-1 overflow-y-auto p-6 space-y-6">
138+
<TaskDescription task={task} onUpdateTask={handleUpdateTask} />
139+
<SubTasksList task={task} onUpdateTask={handleUpdateTask} />
140+
<TaskComments taskId={task.id} />
141+
</main>
142+
<TaskAttributesSidebar task={task} onUpdateTask={handleUpdateTask} />
143+
</div>
144+
// Quick actions could be part of the header or a separate bar
145+
*/}
146+
</div>
147+
);
148+
};
149+
150+
export default NewTaskView;

0 commit comments

Comments
 (0)