fix: prevent sub-subtask creation, limit nesting to 2 levels (#62)
Web: hide "Add subtask" button when depth >= 1 in TaskItem. API: reject task creation if parentId points to a task that already has a parentId (max depth validation). Mobile: hide subtask section in task detail when viewing a subtask. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
894ac03072
commit
21020406b2
3 changed files with 77 additions and 66 deletions
|
|
@ -54,6 +54,7 @@ type TaskData = {
|
|||
priority: number;
|
||||
dueDate: Date | null;
|
||||
listId: string;
|
||||
parentId: string | null;
|
||||
recurrence: string | null;
|
||||
};
|
||||
|
||||
|
|
@ -400,67 +401,71 @@ export default function TaskDetailScreen() {
|
|||
</>
|
||||
)}
|
||||
|
||||
{/* Subtasks */}
|
||||
<Text className={`mb-2 mt-6 text-xs uppercase tracking-wide ${isDark ? 'text-[#A0A0A0]' : 'text-[#6B6B6B]'}`} style={{ fontFamily: 'Inter_600SemiBold' }}>
|
||||
{t('task.subtasks')}
|
||||
</Text>
|
||||
{subtasks.map((sub) => (
|
||||
<Pressable
|
||||
key={sub.id}
|
||||
onPress={() => editingSubtaskId === sub.id ? undefined : handleToggleSubtask(sub.id)}
|
||||
onLongPress={() => handleEditSubtask(sub)}
|
||||
className={`flex-row items-center border-b py-2.5 ${isDark ? 'border-[#3A3A3A]' : 'border-[#E5E7EB]'}`}
|
||||
>
|
||||
<View
|
||||
className="mr-3 h-5 w-5 items-center justify-center rounded-full border-2"
|
||||
style={{
|
||||
borderColor: sub.completed ? colors.bleu.DEFAULT : colors.priority.none,
|
||||
backgroundColor: sub.completed ? colors.bleu.DEFAULT : 'transparent',
|
||||
}}
|
||||
>
|
||||
{sub.completed && <Text className="text-xs text-white" style={{ fontFamily: 'Inter_700Bold' }}>✓</Text>}
|
||||
</View>
|
||||
{editingSubtaskId === sub.id ? (
|
||||
{/* Subtasks — only for root tasks (not subtasks themselves) */}
|
||||
{!task?.parentId && (
|
||||
<>
|
||||
<Text className={`mb-2 mt-6 text-xs uppercase tracking-wide ${isDark ? 'text-[#A0A0A0]' : 'text-[#6B6B6B]'}`} style={{ fontFamily: 'Inter_600SemiBold' }}>
|
||||
{t('task.subtasks')}
|
||||
</Text>
|
||||
{subtasks.map((sub) => (
|
||||
<Pressable
|
||||
key={sub.id}
|
||||
onPress={() => editingSubtaskId === sub.id ? undefined : handleToggleSubtask(sub.id)}
|
||||
onLongPress={() => handleEditSubtask(sub)}
|
||||
className={`flex-row items-center border-b py-2.5 ${isDark ? 'border-[#3A3A3A]' : 'border-[#E5E7EB]'}`}
|
||||
>
|
||||
<View
|
||||
className="mr-3 h-5 w-5 items-center justify-center rounded-full border-2"
|
||||
style={{
|
||||
borderColor: sub.completed ? colors.bleu.DEFAULT : colors.priority.none,
|
||||
backgroundColor: sub.completed ? colors.bleu.DEFAULT : 'transparent',
|
||||
}}
|
||||
>
|
||||
{sub.completed && <Text className="text-xs text-white" style={{ fontFamily: 'Inter_700Bold' }}>✓</Text>}
|
||||
</View>
|
||||
{editingSubtaskId === sub.id ? (
|
||||
<TextInput
|
||||
value={editingTitle}
|
||||
onChangeText={setEditingTitle}
|
||||
onSubmitEditing={handleSaveSubtaskEdit}
|
||||
onBlur={handleSaveSubtaskEdit}
|
||||
autoFocus
|
||||
className={`flex-1 text-base ${isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
|
||||
style={{ fontFamily: 'Inter_400Regular' }}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
className={`flex-1 text-base ${sub.completed ? 'line-through ' + (isDark ? 'text-[#A0A0A0]' : 'text-[#9CA3AF]') : isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
|
||||
style={{ fontFamily: 'Inter_400Regular' }}
|
||||
>
|
||||
{sub.title}
|
||||
</Text>
|
||||
)}
|
||||
<Pressable
|
||||
onPress={() => handleDeleteSubtask(sub.id)}
|
||||
className="ml-2 p-1.5"
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||
>
|
||||
<X size={16} color={isDark ? '#A0A0A0' : '#9CA3AF'} />
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
))}
|
||||
|
||||
{/* Add subtask */}
|
||||
<View className="mt-2 flex-row items-center">
|
||||
<Plus size={18} color={colors.bleu.DEFAULT} />
|
||||
<TextInput
|
||||
value={editingTitle}
|
||||
onChangeText={setEditingTitle}
|
||||
onSubmitEditing={handleSaveSubtaskEdit}
|
||||
onBlur={handleSaveSubtaskEdit}
|
||||
autoFocus
|
||||
className={`flex-1 text-base ${isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
|
||||
value={newSubtask}
|
||||
onChangeText={setNewSubtask}
|
||||
onSubmitEditing={handleAddSubtask}
|
||||
placeholder={t('task.addSubtask')}
|
||||
placeholderTextColor={isDark ? '#A0A0A0' : '#6B6B6B'}
|
||||
className={`ml-2 flex-1 text-base ${isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
|
||||
style={{ fontFamily: 'Inter_400Regular' }}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
className={`flex-1 text-base ${sub.completed ? 'line-through ' + (isDark ? 'text-[#A0A0A0]' : 'text-[#9CA3AF]') : isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
|
||||
style={{ fontFamily: 'Inter_400Regular' }}
|
||||
>
|
||||
{sub.title}
|
||||
</Text>
|
||||
)}
|
||||
<Pressable
|
||||
onPress={() => handleDeleteSubtask(sub.id)}
|
||||
className="ml-2 p-1.5"
|
||||
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
||||
>
|
||||
<X size={16} color={isDark ? '#A0A0A0' : '#9CA3AF'} />
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
))}
|
||||
|
||||
{/* Add subtask */}
|
||||
<View className="mt-2 flex-row items-center">
|
||||
<Plus size={18} color={colors.bleu.DEFAULT} />
|
||||
<TextInput
|
||||
value={newSubtask}
|
||||
onChangeText={setNewSubtask}
|
||||
onSubmitEditing={handleAddSubtask}
|
||||
placeholder={t('task.addSubtask')}
|
||||
placeholderTextColor={isDark ? '#A0A0A0' : '#6B6B6B'}
|
||||
className={`ml-2 flex-1 text-base ${isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
|
||||
style={{ fontFamily: 'Inter_400Regular' }}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
<View style={{ height: 32 }} />
|
||||
</KeyboardAwareScrollView>
|
||||
|
|
|
|||
|
|
@ -25,16 +25,20 @@ export async function POST(request: Request) {
|
|||
return NextResponse.json({ error: 'List not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// If parentId, verify parent task belongs to user
|
||||
// If parentId, verify parent task belongs to user and is not itself a subtask
|
||||
if (body.data.parentId) {
|
||||
const [parent] = await db
|
||||
.select({ id: slTasks.id })
|
||||
.select({ id: slTasks.id, parentId: slTasks.parentId })
|
||||
.from(slTasks)
|
||||
.where(and(eq(slTasks.id, body.data.parentId), eq(slTasks.userId, auth.userId)));
|
||||
|
||||
if (!parent) {
|
||||
return NextResponse.json({ error: 'Parent task not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (parent.parentId) {
|
||||
return NextResponse.json({ error: 'Cannot create sub-subtasks (max 2 levels)' }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
const [task] = await db
|
||||
|
|
|
|||
|
|
@ -261,12 +261,14 @@ export function TaskItem({ task, subtasks = [], depth = 0 }: TaskItemProps) {
|
|||
>
|
||||
{t("task.edit")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowSubtaskForm(!showSubtaskForm)}
|
||||
className="text-xs text-bleu hover:underline"
|
||||
>
|
||||
{t("task.addSubtask")}
|
||||
</button>
|
||||
{depth < 1 && (
|
||||
<button
|
||||
onClick={() => setShowSubtaskForm(!showSubtaskForm)}
|
||||
className="text-xs text-bleu hover:underline"
|
||||
>
|
||||
{t("task.addSubtask")}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={deleteTask}
|
||||
className="text-xs text-rouge hover:underline flex items-center gap-1"
|
||||
|
|
|
|||
Loading…
Reference in a new issue