Split screen
This commit is contained in:
parent
1f70be8387
commit
5694df4adf
5 changed files with 659 additions and 175 deletions
|
|
@ -17,7 +17,7 @@ import About from './components/About';
|
|||
import Layout from './Layout';
|
||||
import { User } from './entities/User';
|
||||
import TasksToday from './components/Task/TasksToday';
|
||||
import TaskView from './components/Task/TaskView';
|
||||
import TaskDetails from './components/Task/TaskDetails';
|
||||
import LoadingScreen from './components/Shared/LoadingScreen';
|
||||
import InboxItems from './components/Inbox/InboxItems';
|
||||
// Lazy load Tasks component to prevent issues with tags loading
|
||||
|
|
@ -25,14 +25,13 @@ const Tasks = lazy(() => import('./components/Tasks'));
|
|||
|
||||
const App: React.FC = () => {
|
||||
const { i18n } = useTranslation();
|
||||
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
if (!i18n.isInitialized) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fetchCurrentUser = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/current_user', {
|
||||
|
|
@ -176,7 +175,10 @@ const App: React.FC = () => {
|
|||
element={<Navigate to="/today" replace />}
|
||||
/>
|
||||
<Route path="/today" element={<TasksToday />} />
|
||||
<Route path="/task/:uuid" element={<TaskView />} />
|
||||
<Route
|
||||
path="/task/:uuid"
|
||||
element={<TaskDetails />}
|
||||
/>
|
||||
<Route
|
||||
path="/tasks"
|
||||
element={
|
||||
|
|
|
|||
544
frontend/components/Task/TaskDetails.tsx
Normal file
544
frontend/components/Task/TaskDetails.tsx
Normal file
|
|
@ -0,0 +1,544 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams, Link, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
PencilSquareIcon,
|
||||
TrashIcon,
|
||||
TagIcon,
|
||||
FolderIcon,
|
||||
CalendarIcon,
|
||||
ExclamationTriangleIcon,
|
||||
ArrowPathIcon,
|
||||
ClockIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import ConfirmDialog from '../Shared/ConfirmDialog';
|
||||
import TaskModal from './TaskModal';
|
||||
import { Task } from '../../entities/Task';
|
||||
import { Project } from '../../entities/Project';
|
||||
import {
|
||||
fetchTaskByUuid,
|
||||
updateTask,
|
||||
deleteTask,
|
||||
fetchSubtasks,
|
||||
toggleTaskCompletion,
|
||||
} from '../../utils/tasksService';
|
||||
import { createProject } from '../../utils/projectsService';
|
||||
import { useStore } from '../../store/useStore';
|
||||
import { useToast } from '../Shared/ToastContext';
|
||||
import TaskPriorityIcon from './TaskPriorityIcon';
|
||||
import LoadingScreen from '../Shared/LoadingScreen';
|
||||
import MarkdownRenderer from '../Shared/MarkdownRenderer';
|
||||
import TimelinePanel from './TimelinePanel';
|
||||
|
||||
const TaskDetails: React.FC = () => {
|
||||
const { uuid } = useParams<{ uuid: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const { showSuccessToast, showErrorToast } = useToast();
|
||||
|
||||
const projects = useStore((state) => state.projectsStore.projects);
|
||||
|
||||
// Local state
|
||||
const [task, setTask] = useState<Task | null>(null);
|
||||
const [subtasks, setSubtasks] = useState<Task[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isTaskModalOpen, setIsTaskModalOpen] = useState(false);
|
||||
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false);
|
||||
const [taskToDelete, setTaskToDelete] = useState<Task | null>(null);
|
||||
const [isTimelineExpanded, setIsTimelineExpanded] = useState(false);
|
||||
|
||||
// Date and recurrence formatting functions (from TaskHeader)
|
||||
const formatDueDate = (dueDate: string) => {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000)
|
||||
.toISOString()
|
||||
.split('T')[0];
|
||||
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000)
|
||||
.toISOString()
|
||||
.split('T')[0];
|
||||
if (dueDate === today) return t('dateIndicators.today', 'TODAY');
|
||||
if (dueDate === tomorrow)
|
||||
return t('dateIndicators.tomorrow', 'TOMORROW');
|
||||
if (dueDate === yesterday)
|
||||
return t('dateIndicators.yesterday', 'YESTERDAY');
|
||||
|
||||
return new Date(dueDate).toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
const formatRecurrence = (recurrenceType: string) => {
|
||||
switch (recurrenceType) {
|
||||
case 'daily':
|
||||
return t('recurrence.daily', 'Daily');
|
||||
case 'weekly':
|
||||
return t('recurrence.weekly', 'Weekly');
|
||||
case 'monthly':
|
||||
return t('recurrence.monthly', 'Monthly');
|
||||
case 'monthly_weekday':
|
||||
return t('recurrence.monthlyWeekday', 'Monthly');
|
||||
case 'monthly_last_day':
|
||||
return t('recurrence.monthlyLastDay', 'Monthly');
|
||||
default:
|
||||
return t('recurrence.recurring', 'Recurring');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTaskData = async () => {
|
||||
if (!uuid) {
|
||||
setError('No task UUID provided');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const taskData = await fetchTaskByUuid(uuid);
|
||||
setTask(taskData);
|
||||
|
||||
// Load subtasks if this task has any
|
||||
if (taskData.id) {
|
||||
try {
|
||||
const subtasksData = await fetchSubtasks(taskData.id);
|
||||
setSubtasks(subtasksData);
|
||||
} catch (subtaskError) {
|
||||
console.warn('Error loading subtasks:', subtaskError);
|
||||
// Don't fail the whole page if subtasks fail to load
|
||||
}
|
||||
}
|
||||
} catch (fetchError) {
|
||||
setError('Task not found');
|
||||
console.error('Error fetching task:', fetchError);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTaskData();
|
||||
}, [uuid]);
|
||||
|
||||
const handleEdit = () => {
|
||||
setIsTaskModalOpen(true);
|
||||
};
|
||||
|
||||
const handleToggleCompletion = async () => {
|
||||
if (!task?.id) return;
|
||||
|
||||
try {
|
||||
const updatedTask = await toggleTaskCompletion(task.id);
|
||||
setTask(updatedTask);
|
||||
|
||||
const statusMessage =
|
||||
updatedTask.status === 'done' || updatedTask.status === 2
|
||||
? t('task.completedSuccess', 'Task marked as completed')
|
||||
: t('task.reopenedSuccess', 'Task reopened');
|
||||
|
||||
showSuccessToast(statusMessage);
|
||||
} catch (error) {
|
||||
console.error('Error toggling task completion:', error);
|
||||
showErrorToast(
|
||||
t('task.toggleError', 'Failed to update task status')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTaskUpdate = async (updatedTask: Task) => {
|
||||
try {
|
||||
if (task?.id) {
|
||||
const updated = await updateTask(task.id, updatedTask);
|
||||
setTask(updated);
|
||||
showSuccessToast(
|
||||
t('task.updateSuccess', 'Task updated successfully')
|
||||
);
|
||||
|
||||
// Reload subtasks in case they changed
|
||||
if (updated.id) {
|
||||
try {
|
||||
const subtasksData = await fetchSubtasks(updated.id);
|
||||
setSubtasks(subtasksData);
|
||||
} catch (subtaskError) {
|
||||
console.warn('Error reloading subtasks:', subtaskError);
|
||||
}
|
||||
}
|
||||
}
|
||||
setIsTaskModalOpen(false);
|
||||
} catch (error) {
|
||||
console.error('Error updating task:', error);
|
||||
showErrorToast(t('task.updateError', 'Failed to update task'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = () => {
|
||||
if (task) {
|
||||
setTaskToDelete(task);
|
||||
setIsConfirmDialogOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteConfirm = async () => {
|
||||
if (taskToDelete?.id) {
|
||||
try {
|
||||
await deleteTask(taskToDelete.id);
|
||||
showSuccessToast(
|
||||
t('task.deleteSuccess', 'Task deleted successfully')
|
||||
);
|
||||
navigate('/today'); // Navigate back to today view after deletion
|
||||
} catch (error) {
|
||||
console.error('Error deleting task:', error);
|
||||
showErrorToast(t('task.deleteError', 'Failed to delete task'));
|
||||
}
|
||||
}
|
||||
setIsConfirmDialogOpen(false);
|
||||
setTaskToDelete(null);
|
||||
};
|
||||
|
||||
const handleCreateProject = async (name: string): Promise<Project> => {
|
||||
try {
|
||||
return await createProject({ name });
|
||||
} catch (error) {
|
||||
console.error('Error creating project:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubtaskClick = (subtask: Task) => {
|
||||
if (subtask.uuid) {
|
||||
navigate(`/task/${subtask.uuid}`);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
if (error || !task) {
|
||||
return (
|
||||
<div className="flex justify-center px-4 lg:px-2">
|
||||
<div className="w-full max-w-5xl">
|
||||
<div className="min-h-[60vh] flex flex-col items-center justify-center text-center">
|
||||
<ExclamationTriangleIcon className="h-24 w-24 text-gray-400 dark:text-gray-500 mx-auto mb-8" />
|
||||
<h1 className="text-2xl font-medium text-gray-700 dark:text-gray-300 mb-4">
|
||||
{error || t('task.notFound', 'Task Not Found')}
|
||||
</h1>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400 mb-8">
|
||||
{t(
|
||||
'task.notFoundDescription',
|
||||
'The task you are looking for does not exist or has been deleted.'
|
||||
)}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate('/today')}
|
||||
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 transition-colors duration-200"
|
||||
>
|
||||
{t('common.goToToday', 'Go to Today')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center px-4 lg:px-2">
|
||||
<div className="w-full max-w-5xl">
|
||||
{/* Header Section with Title and Action Buttons */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<TaskPriorityIcon
|
||||
priority={task.priority}
|
||||
status={task.status}
|
||||
onToggleCompletion={handleToggleCompletion}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<h2 className="text-2xl font-light text-gray-900 dark:text-gray-100">
|
||||
{task.name}
|
||||
</h2>
|
||||
{/* Project, tags, due date, and recurrence under title */}
|
||||
{(task.Project ||
|
||||
(task.tags && task.tags.length > 0) ||
|
||||
task.due_date ||
|
||||
(task.recurrence_type &&
|
||||
task.recurrence_type !== 'none')) && (
|
||||
<div className="flex items-center text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{task.Project && (
|
||||
<div className="flex items-center">
|
||||
<FolderIcon className="h-3 w-3 mr-1" />
|
||||
<Link
|
||||
to={`/project/${task.Project.id}`}
|
||||
className="text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
|
||||
>
|
||||
{task.Project.name}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{task.Project &&
|
||||
task.tags &&
|
||||
task.tags.length > 0 && (
|
||||
<span className="mx-2">•</span>
|
||||
)}
|
||||
{task.tags && task.tags.length > 0 && (
|
||||
<div className="flex items-center">
|
||||
<TagIcon className="h-3 w-3 mr-1" />
|
||||
<span>
|
||||
{task.tags.map((tag, index) => (
|
||||
<React.Fragment
|
||||
key={tag.id || tag.name}
|
||||
>
|
||||
<Link
|
||||
to={`/tag/${encodeURIComponent(tag.name)}`}
|
||||
className="text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
|
||||
>
|
||||
{tag.name}
|
||||
</Link>
|
||||
{index <
|
||||
task.tags!.length -
|
||||
1 && ', '}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{(task.Project ||
|
||||
(task.tags && task.tags.length > 0)) &&
|
||||
task.due_date && (
|
||||
<span className="mx-2">•</span>
|
||||
)}
|
||||
{task.due_date && (
|
||||
<div className="flex items-center">
|
||||
<CalendarIcon className="h-3 w-3 mr-1" />
|
||||
<span>
|
||||
{formatDueDate(task.due_date)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{(task.Project ||
|
||||
(task.tags && task.tags.length > 0) ||
|
||||
task.due_date) &&
|
||||
task.recurrence_type &&
|
||||
task.recurrence_type !== 'none' && (
|
||||
<span className="mx-2">•</span>
|
||||
)}
|
||||
{task.recurrence_type &&
|
||||
task.recurrence_type !== 'none' && (
|
||||
<div className="flex items-center">
|
||||
<ArrowPathIcon className="h-3 w-3 mr-1" />
|
||||
<span>
|
||||
{formatRecurrence(
|
||||
task.recurrence_type
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() =>
|
||||
setIsTimelineExpanded(!isTimelineExpanded)
|
||||
}
|
||||
className={`p-2 rounded-full transition-colors duration-200 ${
|
||||
isTimelineExpanded
|
||||
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400'
|
||||
}`}
|
||||
title={
|
||||
isTimelineExpanded
|
||||
? 'Hide Activity Timeline'
|
||||
: 'Show Activity Timeline'
|
||||
}
|
||||
>
|
||||
<ClockIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleEdit}
|
||||
className="p-2 text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400 rounded-full transition-colors duration-200"
|
||||
>
|
||||
<PencilSquareIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleDeleteClick();
|
||||
}}
|
||||
className="p-2 text-gray-600 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400 rounded-full transition-colors duration-200"
|
||||
>
|
||||
<TrashIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content - Two column layout for notes and subtasks */}
|
||||
{(task.note || subtasks.length > 0) && (
|
||||
<div className="mb-8 mt-8">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* Notes Column */}
|
||||
{task.note && (
|
||||
<div>
|
||||
<h4 className="text-base font-light text-gray-900 dark:text-gray-100 mb-4">
|
||||
{t('task.notes', 'Notes')}
|
||||
</h4>
|
||||
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-6">
|
||||
<MarkdownRenderer
|
||||
content={task.note}
|
||||
className="prose dark:prose-invert max-w-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Subtasks Column */}
|
||||
{subtasks.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-sm font-light text-gray-900 dark:text-gray-100 mb-4">
|
||||
{t('task.subtasks', 'Subtasks')}
|
||||
</h4>
|
||||
<div className="space-y-1">
|
||||
{subtasks.map((subtask) => (
|
||||
<div
|
||||
key={subtask.id}
|
||||
className="group"
|
||||
>
|
||||
<div
|
||||
onClick={() =>
|
||||
handleSubtaskClick(
|
||||
subtask
|
||||
)
|
||||
}
|
||||
className={`rounded-lg shadow-sm bg-white dark:bg-gray-900 border-2 cursor-pointer transition-all duration-200 ${
|
||||
subtask.status ===
|
||||
'in_progress' ||
|
||||
subtask.status === 1
|
||||
? 'border-green-400/60 dark:border-green-500/60'
|
||||
: 'border-gray-50 dark:border-gray-800'
|
||||
}`}
|
||||
>
|
||||
<div className="px-4 py-2.5 flex items-center space-x-3">
|
||||
<div className="flex-shrink-0">
|
||||
<TaskPriorityIcon
|
||||
priority={
|
||||
subtask.priority
|
||||
}
|
||||
status={
|
||||
subtask.status
|
||||
}
|
||||
onToggleCompletion={async () => {
|
||||
if (
|
||||
subtask.id
|
||||
) {
|
||||
try {
|
||||
await toggleTaskCompletion(
|
||||
subtask.id
|
||||
);
|
||||
// Reload subtasks after toggling completion
|
||||
if (
|
||||
task?.id
|
||||
) {
|
||||
const subtasksData =
|
||||
await fetchSubtasks(
|
||||
task.id
|
||||
);
|
||||
setSubtasks(
|
||||
subtasksData
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error toggling subtask completion:',
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className={`text-base flex-1 truncate ${
|
||||
subtask.status ===
|
||||
'done' ||
|
||||
subtask.status ===
|
||||
2 ||
|
||||
subtask.status ===
|
||||
'archived' ||
|
||||
subtask.status ===
|
||||
3
|
||||
? 'text-gray-500 dark:text-gray-400'
|
||||
: 'text-gray-900 dark:text-gray-100'
|
||||
}`}
|
||||
>
|
||||
{subtask.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Activity Timeline */}
|
||||
{isTimelineExpanded && task?.id && (
|
||||
<div className="mb-8 mt-8">
|
||||
<TimelinePanel
|
||||
taskId={task.id}
|
||||
isExpanded={isTimelineExpanded}
|
||||
onToggle={() =>
|
||||
setIsTimelineExpanded(!isTimelineExpanded)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Task Modal for Editing */}
|
||||
<TaskModal
|
||||
isOpen={isTaskModalOpen}
|
||||
task={
|
||||
task || {
|
||||
name: '',
|
||||
status: 'not_started',
|
||||
priority: 'medium',
|
||||
completed_at: null,
|
||||
}
|
||||
}
|
||||
onClose={() => setIsTaskModalOpen(false)}
|
||||
onSave={handleTaskUpdate}
|
||||
onDelete={async (taskId: number) => {
|
||||
await deleteTask(taskId);
|
||||
navigate('/today');
|
||||
}}
|
||||
projects={projects}
|
||||
onCreateProject={handleCreateProject}
|
||||
showToast={false}
|
||||
initialSubtasks={subtasks}
|
||||
/>
|
||||
|
||||
{/* Confirm Delete Dialog */}
|
||||
{isConfirmDialogOpen && taskToDelete && (
|
||||
<ConfirmDialog
|
||||
title={t('task.deleteConfirmTitle', 'Delete Task')}
|
||||
message={t(
|
||||
'task.deleteConfirmMessage',
|
||||
'Are you sure you want to delete this task? This action cannot be undone.'
|
||||
)}
|
||||
onConfirm={handleDeleteConfirm}
|
||||
onCancel={() => {
|
||||
setIsConfirmDialogOpen(false);
|
||||
setTaskToDelete(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskDetails;
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { EyeIcon, PencilIcon } from '@heroicons/react/24/outline';
|
||||
import MarkdownRenderer from '../../Shared/MarkdownRenderer';
|
||||
|
||||
interface TaskContentSectionProps {
|
||||
taskId: number | undefined;
|
||||
|
|
@ -13,20 +15,65 @@ const TaskContentSection: React.FC<TaskContentSectionProps> = ({
|
|||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState<'edit' | 'preview'>('edit');
|
||||
|
||||
return (
|
||||
<div className="px-4 py-4 border-b border-gray-200 dark:border-gray-700 flex-1 flex flex-col mb-2">
|
||||
<textarea
|
||||
id={`task_note_${taskId}`}
|
||||
name="note"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className="block w-full border-0 focus:outline-none focus:ring-0 p-3 text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 resize-none flex-1 min-h-0"
|
||||
placeholder={t(
|
||||
'forms.noteContentPlaceholder',
|
||||
'Add task description...'
|
||||
{/* Content area with floating buttons */}
|
||||
<div className="relative flex-1 flex flex-col">
|
||||
{/* Floating toggle buttons */}
|
||||
<div className="absolute top-2 right-2 z-10 flex space-x-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab('edit')}
|
||||
className={`p-1.5 rounded-md transition-colors ${
|
||||
activeTab === 'edit'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
title="Edit"
|
||||
>
|
||||
<PencilIcon className="h-3 w-3" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab('preview')}
|
||||
className={`p-1.5 rounded-md transition-colors ${
|
||||
activeTab === 'preview'
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600'
|
||||
}`}
|
||||
title="Preview"
|
||||
>
|
||||
<EyeIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeTab === 'edit' ? (
|
||||
<textarea
|
||||
id={`task_note_${taskId}`}
|
||||
name="note"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className="block w-full border border-gray-300 dark:border-gray-600 rounded-md shadow-sm p-3 pr-20 text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 resize-none flex-1 min-h-0 focus:ring-2 focus:ring-blue-500 transition duration-150 ease-in-out"
|
||||
placeholder={t(
|
||||
'forms.noteContentPlaceholder',
|
||||
'Add task description using Markdown formatting...\n\nExamples:\n# Heading\n**Bold text**\n*Italic text*\n- List item\n```code```'
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div className="block w-full border border-gray-300 dark:border-gray-600 rounded-md shadow-sm p-3 pr-20 text-sm bg-gray-50 dark:bg-gray-800 flex-1 min-h-0 overflow-y-auto">
|
||||
{value ? (
|
||||
<MarkdownRenderer content={value} />
|
||||
) : (
|
||||
<p className="text-gray-500 dark:text-gray-400 italic">
|
||||
No content to preview. Switch to Edit mode to
|
||||
add content.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Task } from '../../entities/Task';
|
||||
import { Project } from '../../entities/Project';
|
||||
import TaskHeader from './TaskHeader';
|
||||
|
|
@ -124,11 +125,7 @@ const SubtasksDisplay: React.FC<SubtasksDisplayProps> = ({
|
|||
);
|
||||
};
|
||||
import TaskModal from './TaskModal';
|
||||
import {
|
||||
toggleTaskCompletion,
|
||||
fetchSubtasks,
|
||||
fetchTaskById,
|
||||
} from '../../utils/tasksService';
|
||||
import { toggleTaskCompletion, fetchSubtasks } from '../../utils/tasksService';
|
||||
import { isTaskOverdue } from '../../utils/dateUtils';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
|
@ -151,23 +148,8 @@ const TaskItem: React.FC<TaskItemProps> = ({
|
|||
hideProjectName = false,
|
||||
onToggleToday,
|
||||
}) => {
|
||||
// Use task ID as key for modal state to persist across task prop changes
|
||||
const [modalOpenTaskId, setModalOpenTaskId] = useState<number | null>(null);
|
||||
const isModalOpen = modalOpenTaskId === task.id;
|
||||
|
||||
// Wrapper function for setting modal state
|
||||
const setIsModalOpen = (value: boolean) => {
|
||||
if (value) {
|
||||
setModalOpenTaskId(task.id || null);
|
||||
} else {
|
||||
setModalOpenTaskId(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Update projectList when projects prop changes
|
||||
useEffect(() => {
|
||||
setProjectList(projects);
|
||||
}, [projects]);
|
||||
const navigate = useNavigate();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [subtaskModalOpen, setSubtaskModalOpen] = useState(false);
|
||||
const [selectedSubtask, setSelectedSubtask] = useState<Task | null>(null);
|
||||
const [projectList, setProjectList] = useState<Project[]>(projects);
|
||||
|
|
@ -180,6 +162,11 @@ const TaskItem: React.FC<TaskItemProps> = ({
|
|||
const [loadingSubtasks, setLoadingSubtasks] = useState(false);
|
||||
const [hasSubtasks, setHasSubtasks] = useState(false);
|
||||
|
||||
// Update projectList when projects prop changes
|
||||
useEffect(() => {
|
||||
setProjectList(projects);
|
||||
}, [projects]);
|
||||
|
||||
// Calculate completion percentage
|
||||
const calculateCompletionPercentage = () => {
|
||||
if (subtasks.length === 0) return 0;
|
||||
|
|
@ -248,34 +235,16 @@ const TaskItem: React.FC<TaskItemProps> = ({
|
|||
setShowSubtasks(!showSubtasks);
|
||||
};
|
||||
|
||||
const handleTaskClick = (e?: React.MouseEvent) => {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const handleTaskClick = () => {
|
||||
if (task.uuid) {
|
||||
navigate(`/task/${task.uuid}`);
|
||||
}
|
||||
// Use setTimeout to ensure state update happens after any other processing
|
||||
setTimeout(() => {
|
||||
setIsModalOpen(true);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const handleSubtaskClick = async (subtask: Task) => {
|
||||
// If subtask has a parent_task_id, open the parent task with subtasks focus
|
||||
if (subtask.parent_task_id) {
|
||||
try {
|
||||
const parentTask = await fetchTaskById(subtask.parent_task_id);
|
||||
setParentTask(parentTask);
|
||||
setParentTaskModalOpen(true);
|
||||
} catch (error) {
|
||||
console.error('Error fetching parent task:', error);
|
||||
// Fall back to opening the subtask itself
|
||||
setSelectedSubtask(subtask);
|
||||
setSubtaskModalOpen(true);
|
||||
}
|
||||
} else {
|
||||
// If no parent_task_id, open the subtask itself
|
||||
setSelectedSubtask(subtask);
|
||||
setSubtaskModalOpen(true);
|
||||
// Navigate directly to the subtask URL
|
||||
if (subtask.uuid) {
|
||||
navigate(`/task/${subtask.uuid}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,18 +3,15 @@ import { createPortal } from 'react-dom';
|
|||
import { PriorityType, Task } from '../../entities/Task';
|
||||
import ConfirmDialog from '../Shared/ConfirmDialog';
|
||||
import { useToast } from '../Shared/ToastContext';
|
||||
import TimelinePanel from './TimelinePanel';
|
||||
import { Project } from '../../entities/Project';
|
||||
import { useStore } from '../../store/useStore';
|
||||
import { fetchTaskById, fetchSubtasks } from '../../utils/tasksService';
|
||||
import { getTaskIntelligenceEnabled } from '../../utils/profileService';
|
||||
import { fetchTaskById } from '../../utils/tasksService';
|
||||
import {
|
||||
analyzeTaskName,
|
||||
TaskAnalysis,
|
||||
} from '../../utils/taskIntelligenceService';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ClockIcon,
|
||||
TagIcon,
|
||||
FolderIcon,
|
||||
ArrowPathIcon,
|
||||
|
|
@ -45,6 +42,7 @@ interface TaskModalProps {
|
|||
onEditParentTask?: (parentTask: Task) => void;
|
||||
autoFocusSubtasks?: boolean;
|
||||
showToast?: boolean;
|
||||
initialSubtasks?: Task[];
|
||||
}
|
||||
|
||||
const TaskModal: React.FC<TaskModalProps> = ({
|
||||
|
|
@ -58,9 +56,11 @@ const TaskModal: React.FC<TaskModalProps> = ({
|
|||
onEditParentTask,
|
||||
autoFocusSubtasks,
|
||||
showToast = true,
|
||||
initialSubtasks = [],
|
||||
}) => {
|
||||
const { tagsStore } = useStore();
|
||||
const availableTags = tagsStore.getTags();
|
||||
// Avoid calling getTags() during component initialization to prevent remounting
|
||||
const availableTags = tagsStore.tags;
|
||||
const { addNewTags } = tagsStore;
|
||||
const [formData, setFormData] = useState<Task>(task);
|
||||
const [tags, setTags] = useState<string[]>(
|
||||
|
|
@ -78,11 +78,8 @@ const TaskModal: React.FC<TaskModalProps> = ({
|
|||
const [parentTask, setParentTask] = useState<Task | null>(null);
|
||||
const [parentTaskLoading, setParentTaskLoading] = useState(false);
|
||||
const [taskAnalysis, setTaskAnalysis] = useState<TaskAnalysis | null>(null);
|
||||
const [taskIntelligenceEnabled, setTaskIntelligenceEnabled] =
|
||||
useState(true);
|
||||
const [isTimelineExpanded, setIsTimelineExpanded] = useState(false);
|
||||
const [taskIntelligenceEnabled] = useState(true);
|
||||
const [subtasks, setSubtasks] = useState<Task[]>([]);
|
||||
const [subtasksLoaded, setSubtasksLoaded] = useState(false);
|
||||
|
||||
// Collapsible section states
|
||||
const [expandedSections, setExpandedSections] = useState({
|
||||
|
|
@ -163,21 +160,24 @@ const TaskModal: React.FC<TaskModalProps> = ({
|
|||
);
|
||||
setNewProjectName(currentProject ? currentProject.name : '');
|
||||
|
||||
// Fetch parent task if this is a child task
|
||||
// Fetch parent task if this is a child task - but don't block rendering
|
||||
const fetchParentTask = async () => {
|
||||
if (task.recurring_parent_id && isOpen) {
|
||||
setParentTaskLoading(true);
|
||||
try {
|
||||
const parent = await fetchTaskById(
|
||||
task.recurring_parent_id
|
||||
);
|
||||
setParentTask(parent);
|
||||
} catch (error) {
|
||||
console.error('Error fetching parent task:', error);
|
||||
setParentTask(null);
|
||||
} finally {
|
||||
setParentTaskLoading(false);
|
||||
}
|
||||
// Use setTimeout to not block initial render
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const parent = await fetchTaskById(
|
||||
task.recurring_parent_id
|
||||
);
|
||||
setParentTask(parent);
|
||||
} catch (error) {
|
||||
console.error('Error fetching parent task:', error);
|
||||
setParentTask(null);
|
||||
} finally {
|
||||
setParentTaskLoading(false);
|
||||
}
|
||||
}, 0);
|
||||
} else {
|
||||
setParentTask(null);
|
||||
}
|
||||
|
|
@ -186,25 +186,8 @@ const TaskModal: React.FC<TaskModalProps> = ({
|
|||
fetchParentTask();
|
||||
}, [task, projects, isOpen, taskIntelligenceEnabled]);
|
||||
|
||||
// Fetch task intelligence setting when modal opens
|
||||
useEffect(() => {
|
||||
const fetchTaskIntelligenceSetting = async () => {
|
||||
if (isOpen) {
|
||||
try {
|
||||
const enabled = await getTaskIntelligenceEnabled();
|
||||
setTaskIntelligenceEnabled(enabled);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error fetching task intelligence setting:',
|
||||
error
|
||||
);
|
||||
setTaskIntelligenceEnabled(true); // Default to enabled
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchTaskIntelligenceSetting();
|
||||
}, [isOpen]);
|
||||
// Don't fetch task intelligence setting - use default enabled state
|
||||
// This prevents unnecessary API calls when opening the modal
|
||||
|
||||
// Auto-focus on subtasks section when modal opens
|
||||
useEffect(() => {
|
||||
|
|
@ -220,8 +203,12 @@ const TaskModal: React.FC<TaskModalProps> = ({
|
|||
}
|
||||
}, [isOpen, autoFocusSubtasks]);
|
||||
|
||||
// Tags refreshing removed to prevent modal closing issues
|
||||
// Tags will be loaded automatically when accessed via getTags() in the store
|
||||
// Load tags when modal opens if not already loaded
|
||||
useEffect(() => {
|
||||
if (isOpen && !tagsStore.hasLoaded && !tagsStore.isLoading) {
|
||||
tagsStore.loadTags();
|
||||
}
|
||||
}, [isOpen, tagsStore.hasLoaded, tagsStore.isLoading, tagsStore.loadTags]);
|
||||
|
||||
const handleEditParent = () => {
|
||||
if (parentTask && onEditParentTask) {
|
||||
|
|
@ -436,28 +423,16 @@ const TaskModal: React.FC<TaskModalProps> = ({
|
|||
};
|
||||
}, [isOpen]);
|
||||
|
||||
// Load existing subtasks when modal opens (only if not already loaded)
|
||||
// Load existing subtasks when modal opens - use initialSubtasks if provided, no fetching
|
||||
useEffect(() => {
|
||||
if (isOpen && task.id && !subtasksLoaded) {
|
||||
const loadExistingSubtasks = async () => {
|
||||
try {
|
||||
const existingSubtasks = await fetchSubtasks(task.id!);
|
||||
setSubtasks(existingSubtasks);
|
||||
setSubtasksLoaded(true);
|
||||
} catch {
|
||||
// Handle silently - don't show error for this
|
||||
setSubtasks([]);
|
||||
setSubtasksLoaded(true);
|
||||
}
|
||||
};
|
||||
|
||||
loadExistingSubtasks();
|
||||
if (isOpen && task.id) {
|
||||
// Always use provided initial subtasks (from parent component) or empty array
|
||||
setSubtasks(initialSubtasks);
|
||||
} else if (!isOpen) {
|
||||
// Reset subtasks when modal closes
|
||||
setSubtasks([]);
|
||||
setSubtasksLoaded(false);
|
||||
}
|
||||
}, [isOpen, task.id, subtasksLoaded]);
|
||||
}, [isOpen, task.id, initialSubtasks]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
|
|
@ -491,11 +466,7 @@ const TaskModal: React.FC<TaskModalProps> = ({
|
|||
>
|
||||
<div className="flex flex-col lg:flex-row h-full sm:min-h-[600px] sm:max-h-[90vh]">
|
||||
{/* Main Form Section */}
|
||||
<div
|
||||
className={`flex-1 flex flex-col transition-all duration-300 bg-white dark:bg-gray-800 sm:rounded-l-lg ${
|
||||
isTimelineExpanded ? 'lg:pr-2' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1 flex flex-col transition-all duration-300 bg-white dark:bg-gray-800 sm:rounded-lg">
|
||||
<div className="flex-1 relative">
|
||||
<div
|
||||
className="absolute inset-0 overflow-y-auto overflow-x-hidden"
|
||||
|
|
@ -724,21 +695,6 @@ const TaskModal: React.FC<TaskModalProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timeline Panel - Show when expanded on mobile only */}
|
||||
{isTimelineExpanded && (
|
||||
<div className="lg:hidden border-t border-gray-200 dark:border-gray-700">
|
||||
<TimelinePanel
|
||||
taskId={task.id}
|
||||
isExpanded={isTimelineExpanded}
|
||||
onToggle={() =>
|
||||
setIsTimelineExpanded(
|
||||
!isTimelineExpanded
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Section Icons - Above border, split layout */}
|
||||
<div className="flex-shrink-0 bg-white dark:bg-gray-800 px-3 py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
@ -875,31 +831,6 @@ const TaskModal: React.FC<TaskModalProps> = ({
|
|||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Right side: Timeline Toggle Button */}
|
||||
<button
|
||||
onClick={() =>
|
||||
setIsTimelineExpanded(
|
||||
!isTimelineExpanded
|
||||
)
|
||||
}
|
||||
className={`p-2 rounded-full transition-colors ${
|
||||
isTimelineExpanded
|
||||
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
title={
|
||||
isTimelineExpanded
|
||||
? t(
|
||||
'timeline.hideActivityTimeline'
|
||||
)
|
||||
: t(
|
||||
'timeline.showActivityTimeline'
|
||||
)
|
||||
}
|
||||
>
|
||||
<ClockIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -939,15 +870,6 @@ const TaskModal: React.FC<TaskModalProps> = ({
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timeline Panel - Desktop Sidebar */}
|
||||
<TimelinePanel
|
||||
taskId={task.id}
|
||||
isExpanded={isTimelineExpanded}
|
||||
onToggle={() =>
|
||||
setIsTimelineExpanded(!isTimelineExpanded)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue