Fix saving subtasks on edit (#806)

This commit is contained in:
Chris 2026-01-31 08:29:44 +02:00 committed by GitHub
parent e7f23bcff8
commit 2f13d0d4a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 84 additions and 196 deletions

View file

@ -10,6 +10,7 @@ import {
deleteTask,
fetchTaskByUid,
fetchTaskNextIterations,
fetchSubtasks,
TaskIteration,
toggleTaskCompletion,
} from '../../utils/tasksService';
@ -57,8 +58,7 @@ const TaskDetails: React.FC = () => {
const [loadingIterations, setLoadingIterations] = useState(false);
const [parentTask, setParentTask] = useState<Task | null>(null);
const [loadingParent, setLoadingParent] = useState(false);
const [isEditingSubtasks, setIsEditingSubtasks] = useState(false);
const [editedSubtasks, setEditedSubtasks] = useState<Task[]>([]);
const [pendingSubtasks, setPendingSubtasks] = useState<Task[]>([]);
const [actionsMenuOpen, setActionsMenuOpen] = useState(false);
const actionsMenuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@ -116,6 +116,7 @@ const TaskDetails: React.FC = () => {
});
const [activePill, setActivePill] = useState('overview');
const [attachmentCount, setAttachmentCount] = useState(0);
const [hasLoadedSubtasks, setHasLoadedSubtasks] = useState(false);
useEffect(() => {
setEditedDueDate(task?.due_date || '');
@ -187,7 +188,6 @@ const TaskDetails: React.FC = () => {
setRecurrenceForm((prev) => {
const updated = { ...prev, [field]: value };
// Set default values when switching to monthly recurrence
if (
field === 'recurrence_type' &&
value === 'monthly' &&
@ -307,7 +307,6 @@ const TaskDetails: React.FC = () => {
}
}
// Check if due date is in the past
if (editedDueDate) {
const dueDate = new Date(editedDueDate);
const today = new Date();
@ -463,7 +462,6 @@ const TaskDetails: React.FC = () => {
fetchTaskData();
}, [uid, task, tasksStore]);
// Load attachment count when task is loaded
useEffect(() => {
const loadAttachmentCount = async () => {
if (task?.uid) {
@ -479,6 +477,49 @@ const TaskDetails: React.FC = () => {
loadAttachmentCount();
}, [task?.uid]);
useEffect(() => {
setHasLoadedSubtasks(false);
}, [uid]);
useEffect(() => {
const loadSubtasks = async () => {
const subtasksAlreadyLoaded = task?.subtasks && task.subtasks.length > 0;
if (
activePill === 'subtasks' &&
task?.uid &&
!hasLoadedSubtasks &&
!subtasksAlreadyLoaded
) {
try {
const fetchedSubtasks = await fetchSubtasks(task.uid);
setHasLoadedSubtasks(true);
const existingIndex = tasksStore.tasks.findIndex(
(t: Task) => t.uid === task.uid
);
if (existingIndex >= 0) {
const updatedTasks = [...tasksStore.tasks];
updatedTasks[existingIndex] = {
...task,
subtasks: fetchedSubtasks,
};
tasksStore.setTasks(updatedTasks);
}
} catch (error) {
console.error('Error loading subtasks:', error);
setHasLoadedSubtasks(true);
}
}
};
loadSubtasks();
}, [activePill, task?.uid, task?.subtasks, hasLoadedSubtasks, tasksStore]);
useEffect(() => {
setPendingSubtasks(subtasks);
}, [subtasks]);
useEffect(() => {
const loadNextIterations = async () => {
if (
@ -488,7 +529,6 @@ const TaskDetails: React.FC = () => {
) {
try {
setLoadingIterations(true);
// Don't pass startFromDate - let backend default to today
const iterations = await fetchTaskNextIterations(
task.uid!
);
@ -508,7 +548,6 @@ const TaskDetails: React.FC = () => {
try {
setLoadingIterations(true);
// Don't pass startFromDate - let backend default to today
const iterations = await fetchTaskNextIterations(
parentTask.uid
);
@ -559,20 +598,29 @@ const TaskDetails: React.FC = () => {
loadParentTask();
}, [task?.recurring_parent_uid]);
const handleStartSubtasksEdit = () => {
setIsEditingSubtasks(true);
setEditedSubtasks([...subtasks]);
};
const handleSaveSubtasks = async () => {
const handleSaveSubtasks = async (subtasksToSave: Task[]) => {
if (!task?.uid) {
setIsEditingSubtasks(false);
setEditedSubtasks([]);
return;
}
const hasChanges =
subtasksToSave.length !== subtasks.length ||
subtasksToSave.some(
(ps, i) =>
!subtasks[i] ||
ps.name !== subtasks[i].name ||
ps.status !== subtasks[i].status ||
(ps as any)._isNew ||
(ps as any)._isEdited ||
(ps as any)._statusChanged
);
if (!hasChanges) {
return;
}
try {
await updateTask(task.uid, { ...task, subtasks: editedSubtasks });
await updateTask(task.uid, { ...task, subtasks: subtasksToSave });
if (uid) {
const updatedTask = await fetchTaskByUid(uid);
@ -586,45 +634,13 @@ const TaskDetails: React.FC = () => {
}
}
showSuccessToast(
t('task.subtasksUpdated', 'Subtasks updated successfully')
);
setIsEditingSubtasks(false);
setTimelineRefreshKey((prev) => prev + 1);
} catch (error) {
console.error('Error updating subtasks:', error);
showErrorToast(
t('task.subtasksUpdateError', 'Failed to update subtasks')
);
setEditedSubtasks([...subtasks]);
setIsEditingSubtasks(false);
}
};
const handleCancelSubtasksEdit = () => {
setIsEditingSubtasks(false);
setEditedSubtasks([]);
};
const handleToggleSubtaskCompletion = async (subtask: Task) => {
if (!subtask.uid) return;
try {
await toggleTaskCompletion(subtask.uid, subtask);
if (uid) {
const updatedTask = await fetchTaskByUid(uid);
const existingIndex = tasksStore.tasks.findIndex(
(t: Task) => t.uid === uid
);
if (existingIndex >= 0) {
const updatedTasks = [...tasksStore.tasks];
updatedTasks[existingIndex] = updatedTask;
tasksStore.setTasks(updatedTasks);
}
}
setTimelineRefreshKey((prev) => prev + 1);
} catch (error) {
console.error('Error toggling subtask completion:', error);
setPendingSubtasks([...subtasks]);
}
};
@ -715,13 +731,11 @@ const TaskDetails: React.FC = () => {
try {
setLoadingIterations(true);
if (isTemplateTask) {
// Don't pass startFromDate - let backend default to today
const iterations = await fetchTaskNextIterations(
latestTask.uid!
);
setNextIterations(iterations);
} else if (canUseParentIterations && parentTask?.uid) {
// Don't pass startFromDate - let backend default to today
const iterations = await fetchTaskNextIterations(
parentTask.uid
);
@ -1071,7 +1085,6 @@ const TaskDetails: React.FC = () => {
return (
<div className="px-4 lg:px-6 pt-4">
<div className="w-full">
{/* Header Section with Title and Action Buttons */}
<TaskDetailsHeader
task={task}
onTitleUpdate={handleTitleUpdate}
@ -1092,12 +1105,9 @@ const TaskDetails: React.FC = () => {
subtasksCount={subtasks.length}
/>
{/* Content - Full width layout */}
<div className="mb-6 mt-6">
{/* Overview Pill */}
{activePill === 'overview' && (
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
{/* Left Column - Main Content */}
<div className="lg:col-span-3 space-y-8">
<TaskContentCard
content={task.note || ''}
@ -1105,7 +1115,6 @@ const TaskDetails: React.FC = () => {
/>
</div>
{/* Right Column - Project and Tags */}
<div className="space-y-6">
<TaskProjectCard
task={task}
@ -1151,7 +1160,6 @@ const TaskDetails: React.FC = () => {
</div>
)}
{/* Recurrence Pill */}
{activePill === 'recurrence' && (
<div className="grid grid-cols-1">
<TaskRecurrenceCard
@ -1171,26 +1179,17 @@ const TaskDetails: React.FC = () => {
</div>
)}
{/* Subtasks Pill */}
{activePill === 'subtasks' && (
<div className="grid grid-cols-1">
<TaskSubtasksCard
task={task}
subtasks={subtasks}
isEditing={isEditingSubtasks}
editedSubtasks={editedSubtasks}
onSubtasksChange={setEditedSubtasks}
onStartEdit={handleStartSubtasksEdit}
subtasks={pendingSubtasks}
onSubtasksChange={setPendingSubtasks}
onSave={handleSaveSubtasks}
onCancel={handleCancelSubtasksEdit}
onToggleSubtaskCompletion={
handleToggleSubtaskCompletion
}
/>
</div>
)}
{/* Attachments Pill */}
{activePill === 'attachments' && (
<div className="grid grid-cols-1">
<TaskAttachmentsCard
@ -1200,7 +1199,6 @@ const TaskDetails: React.FC = () => {
</div>
)}
{/* Activity Pill */}
{activePill === 'activity' && (
<div className="rounded-lg shadow-sm bg-white dark:bg-gray-900 border-2 border-gray-50 dark:border-gray-800 p-6">
<TaskTimeline
@ -1210,9 +1208,7 @@ const TaskDetails: React.FC = () => {
</div>
)}
</div>
{/* End of main content sections */}
{/* Confirm Delete Dialog */}
{isConfirmDialogOpen && taskToDelete && (
<ConfirmDialog
title={t('task.deleteConfirmTitle', 'Delete Task')}

View file

@ -1,119 +1,28 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { ListBulletIcon } from '@heroicons/react/24/outline';
import TaskSubtasksSection from '../TaskForm/TaskSubtasksSection';
import TaskPriorityIcon from '../../Shared/Icons/TaskPriorityIcon';
import { Task } from '../../../entities/Task';
interface TaskSubtasksCardProps {
task: Task;
subtasks: Task[];
isEditing: boolean;
editedSubtasks: Task[];
onSubtasksChange: (subtasks: Task[]) => void;
onStartEdit: () => void;
onSave: () => void;
onCancel: () => void;
onToggleSubtaskCompletion: (subtask: Task) => Promise<void>;
onSave: (subtasks: Task[]) => void;
}
const TaskSubtasksCard: React.FC<TaskSubtasksCardProps> = ({
task,
subtasks,
isEditing,
editedSubtasks,
onSubtasksChange,
onStartEdit,
onSave,
onCancel,
onToggleSubtaskCompletion,
}) => {
const { t } = useTranslation();
return (
<div className="space-y-2">
{isEditing ? (
<div className="rounded-lg shadow-sm bg-white dark:bg-gray-900 border-2 border-blue-500 dark:border-blue-400 p-6">
<TaskSubtasksSection
parentTaskId={task.id!}
subtasks={editedSubtasks}
onSubtasksChange={onSubtasksChange}
/>
<div className="flex items-center justify-end mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<div className="flex space-x-2">
<button
onClick={onSave}
className="px-4 py-2 text-sm bg-green-600 dark:bg-green-500 text-white rounded hover:bg-green-700 dark:hover:bg-green-600 transition-colors"
>
{t('common.save', 'Save')}
</button>
<button
onClick={onCancel}
className="px-4 py-2 text-sm bg-gray-200 dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded hover:bg-gray-300 dark:hover:bg-gray-700 transition-colors"
>
{t('common.cancel', 'Cancel')}
</button>
</div>
</div>
</div>
) : subtasks.length > 0 ? (
<div className="space-y-0.5">
{subtasks.map((subtask: Task) => (
<div
key={subtask.id}
className={`rounded-lg shadow-sm bg-white dark:bg-gray-900 border transition-all duration-200 ${
subtask.status === 'in_progress' ||
subtask.status === 1
? 'border-blue-500/60 dark:border-blue-600/60'
: 'border-gray-50 dark:border-gray-800'
}`}
>
<div className="px-3 py-3 flex items-center space-x-3">
<TaskPriorityIcon
priority={subtask.priority}
status={subtask.status}
onToggleCompletion={() =>
onToggleSubtaskCompletion(subtask)
}
/>
<span
onClick={onStartEdit}
className={`text-base flex-1 truncate cursor-pointer hover:text-blue-600 dark:hover:text-blue-400 transition-colors ${
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'
}`}
title={t(
'task.clickToEditSubtasks',
'Click to edit subtasks'
)}
>
{subtask.name}
</span>
</div>
</div>
))}
</div>
) : (
<div
onClick={onStartEdit}
className="rounded-lg shadow-sm bg-white dark:bg-gray-900 border-2 border-gray-50 dark:border-gray-800 hover:border-gray-200 dark:hover:border-gray-700 p-6 cursor-pointer transition-colors"
title={t(
'task.clickToEditSubtasks',
'Click to add or edit subtasks'
)}
>
<div className="flex flex-col items-center justify-center py-8 text-gray-500 dark:text-gray-400">
<ListBulletIcon className="h-12 w-12 mb-3 opacity-50" />
<span className="text-sm text-center">
{t('task.noSubtasksClickToAdd', 'Add subtasks')}
</span>
</div>
</div>
)}
<div className="rounded-lg shadow-sm bg-white dark:bg-gray-900 border-2 border-gray-50 dark:border-gray-800 p-6">
<TaskSubtasksSection
parentTaskId={task.id!}
subtasks={subtasks}
onSubtasksChange={onSubtasksChange}
onSave={onSave}
/>
</div>
);
};

View file

@ -10,6 +10,7 @@ interface TaskSubtasksSectionProps {
subtasks: Task[];
onSubtasksChange: (subtasks: Task[]) => void;
onSubtaskUpdate?: (subtask: Task) => Promise<void>;
onSave?: (subtasks: Task[]) => void;
}
const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
@ -17,6 +18,7 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
subtasks,
onSubtasksChange,
onSubtaskUpdate,
onSave,
}) => {
const [newSubtaskName, setNewSubtaskName] = useState('');
const [isLoading] = useState(false);
@ -28,7 +30,6 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
const scrollToBottom = () => {
setTimeout(() => {
// Find the modal's scrollable container
const modalScrollContainer = document.querySelector(
'.absolute.inset-0.overflow-y-auto'
);
@ -49,24 +50,24 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
status: 'not_started',
priority: 'low',
today: false,
parent_task_id: parentTaskId, // Set the parent task ID immediately
// Mark as new for backend processing
parent_task_id: parentTaskId,
isNew: true,
// Also keep for UI purposes
_isNew: true,
completed_at: null,
} as Task;
onSubtasksChange([...subtasks, newSubtask]);
const updatedSubtasks = [...subtasks, newSubtask];
onSubtasksChange(updatedSubtasks);
setNewSubtaskName('');
// Only scroll when adding new subtask, not when toggling completion
scrollToBottom();
onSave?.(updatedSubtasks);
};
const handleDeleteSubtask = (index: number) => {
const updatedSubtasks = subtasks.filter((_, i) => i !== index);
onSubtasksChange(updatedSubtasks);
onSave?.(updatedSubtasks);
};
const handleKeyPress = (e: React.KeyboardEvent) => {
@ -93,10 +94,8 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
return {
...subtask,
name: editingName.trim(),
// Backend flags
isNew: isNew,
isEdited: isEdited,
// UI flags
_isNew: isNew,
_isEdited: isEdited,
};
@ -107,6 +106,7 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
onSubtasksChange(updatedSubtasks);
setEditingIndex(null);
setEditingName('');
onSave?.(updatedSubtasks);
};
const handleCancelEdit = () => {
@ -130,7 +130,6 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
...subtask,
status: newStatus,
completed_at: isDone ? null : new Date().toISOString(),
// Mark for backend update if it has an ID (existing subtask)
_statusChanged: hasId,
};
}
@ -141,7 +140,6 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
return (
<div ref={subtasksSectionRef} className="space-y-3">
{/* Existing Subtasks */}
{isLoading ? (
<div className="text-sm text-gray-500 dark:text-gray-400">
{t('loading.subtasks', 'Loading subtasks...')}
@ -171,7 +169,6 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
(subtask as any).isNew
)
) {
// Existing subtask - use API for immediate toggle, then update callback
try {
const updatedSubtask =
await toggleTaskCompletion(
@ -187,7 +184,6 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
);
}
} else {
// New subtask or no callback - handle locally
handleToggleNewSubtaskCompletion(
index
);
@ -245,7 +241,6 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
.isNew
)
) {
// Existing subtask - use API for immediate toggle, then update callback
try {
const updatedSubtask =
await toggleTaskCompletion(
@ -261,7 +256,6 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
);
}
} else {
// New subtask or no callback - handle locally
handleToggleNewSubtaskCompletion(
index
);
@ -287,16 +281,6 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
)}
>
{subtask.name}
{(subtask as any)._isNew && (
<span className="ml-2 text-xs text-blue-500 dark:text-blue-400">
(new)
</span>
)}
{(subtask as any)._isEdited && (
<span className="ml-2 text-xs text-orange-500 dark:text-orange-400">
(edited)
</span>
)}
</span>
</div>
<button
@ -320,7 +304,6 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
</div>
)}
{/* Add New Subtask */}
<div className="flex items-center space-x-2">
<input
ref={addInputRef}