Keep completed todos at the end (#1430)

Co-authored-by: Konstantin Zolin <zolin_ka@vk.com>
This commit is contained in:
kostazol
2026-05-26 11:49:30 +03:00
committed by GitHub
co-authored by Konstantin Zolin
parent db2464d94d
commit 4c34031f8d
@@ -101,6 +101,19 @@ const createTodoId = (): string => {
return `todo_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
};
const sortTodosWithCompletedLast = (items: OpenChamberProjectTodoItem[]): OpenChamberProjectTodoItem[] => [
...items.filter((todo) => !todo.completed),
...items.filter((todo) => todo.completed),
];
const insertTodoBeforeCompleted = (items: OpenChamberProjectTodoItem[], item: OpenChamberProjectTodoItem): OpenChamberProjectTodoItem[] => {
const firstCompletedIndex = items.findIndex((todo) => todo.completed);
if (firstCompletedIndex === -1) {
return [...items, item];
}
return [...items.slice(0, firstCompletedIndex), item, ...items.slice(firstCompletedIndex)];
};
type SortableTodoHandleProps = {
attributes: ReturnType<typeof useSortable>['attributes'];
listeners: ReturnType<typeof useSortable>['listeners'];
@@ -216,7 +229,7 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
return;
}
setNotes(data.notes);
setTodos(data.todos);
setTodos(sortTodosWithCompletedLast(data.todos));
setPlans(nextPlans);
lastSavedNotesRef.current = data.notes;
notesHydratedRef.current = true;
@@ -341,15 +354,12 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
return;
}
const nextTodos = [
...todos,
{
id: createTodoId(),
text: trimmed.slice(0, OPENCHAMBER_PROJECT_TODO_TEXT_MAX_LENGTH),
completed: false,
createdAt: Date.now(),
},
];
const nextTodos = insertTodoBeforeCompleted(todos, {
id: createTodoId(),
text: trimmed.slice(0, OPENCHAMBER_PROJECT_TODO_TEXT_MAX_LENGTH),
completed: false,
createdAt: Date.now(),
});
setTodos(nextTodos);
setNewTodoText('');
void persistProjectData(notes, nextTodos);
@@ -369,7 +379,15 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
const handleToggleTodo = React.useCallback(
(id: string, completed: boolean) => {
const nextTodos = todos.map((todo) => (todo.id === id ? { ...todo, completed } : todo));
const todo = todos.find((item) => item.id === id);
if (!todo || todo.completed === completed) {
return;
}
const remainingTodos = todos.filter((item) => item.id !== id);
const updatedTodo = { ...todo, completed };
const nextTodos = completed
? [...remainingTodos, updatedTodo]
: insertTodoBeforeCompleted(remainingTodos, updatedTodo);
setTodos(nextTodos);
void persistProjectData(notes, nextTodos);
},
@@ -405,7 +423,7 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
if (oldIndex === -1 || newIndex === -1) {
return;
}
const nextTodos = arrayMove(todos, oldIndex, newIndex);
const nextTodos = sortTodosWithCompletedLast(arrayMove(todos, oldIndex, newIndex));
setTodos(nextTodos);
void persistProjectData(notes, nextTodos);
},
@@ -799,7 +817,7 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
return (
<SortableTodoItem key={todo.id} id={todo.id}>
{(dragHandleProps) => (
<div className="flex items-start gap-1.5 px-2.5 py-1.5">
<div className={cn('flex gap-1.5 px-2.5 py-1.5', isExpandedTodo ? 'items-start' : 'items-center')}>
<button
type="button"
ref={dragHandleProps.setActivatorNodeRef}
@@ -826,7 +844,7 @@ export const ProjectNotesTodoPanel: React.FC<ProjectNotesTodoPanelProps> = ({
type="button"
onClick={() => handleToggleTodoExpanded(todo.id)}
className={cn(
'block min-h-6 min-w-0 flex-1 bg-transparent p-0 text-left typography-ui-label leading-6 text-foreground',
'block min-h-6 min-w-0 flex-1 bg-transparent p-0 text-left typography-ui-label leading-normal text-foreground',
isExpandedTodo ? 'whitespace-normal break-words' : 'overflow-hidden text-ellipsis whitespace-nowrap',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
todo.completed && 'text-muted-foreground line-through'