mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 07:53:09 +00:00
feat(todo): sort the task list by due date (#2205)
The list held its manual order, so an item due tomorrow sat behind one created earlier but due later. The sort-by section gains a due-date toggle next to the priority one, on desktop and in the phone tab: nearest deadline first, undated tasks after all dated ones, ties keep the manual order. One sort at a time, and manual drag-reorder pauses while either is active, exactly like the priority sort.
This commit is contained in:
@@ -256,6 +256,24 @@ describe('TodoListPanel', () => {
|
||||
expect(html.indexOf('High Prio')).toBeLessThan(html.indexOf('Low Prio'));
|
||||
});
|
||||
|
||||
it('FE-COMP-TODO-073: Sort by due date puts the nearest deadline first, undated tasks last (#2205)', async () => {
|
||||
const user = userEvent.setup();
|
||||
const items = [
|
||||
buildTodoItem({ name: 'Due Later', due_date: '2099-07-17', checked: 0 }),
|
||||
buildTodoItem({ name: 'Due Tomorrow', due_date: '2099-07-16', checked: 0 }),
|
||||
buildTodoItem({ name: 'No Due Date', checked: 0 }),
|
||||
];
|
||||
render(<TodoListPanel tripId={1} items={items} />);
|
||||
const sortBtn = screen.getAllByRole('button').find(
|
||||
b => b.textContent?.includes('Due date') || b.getAttribute('title') === 'Due date'
|
||||
);
|
||||
expect(sortBtn).toBeTruthy();
|
||||
await user.click(sortBtn!);
|
||||
const html = document.body.innerHTML;
|
||||
expect(html.indexOf('Due Tomorrow')).toBeLessThan(html.indexOf('Due Later'));
|
||||
expect(html.indexOf('Due Later')).toBeLessThan(html.indexOf('No Due Date'));
|
||||
});
|
||||
|
||||
it('FE-COMP-TODO-019: Detail pane shows task name and allows editing', async () => {
|
||||
const user = userEvent.setup();
|
||||
const items = [buildTodoItem({ id: 11, name: 'Edit Me', checked: 0 })];
|
||||
|
||||
@@ -74,7 +74,7 @@ export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tr
|
||||
const {
|
||||
canEdit, t, formatDate, toggleTodoItem, reorderTodoItems,
|
||||
isMobile, filter, setFilter, selectedId, setSelectedId,
|
||||
isAddingNew, setIsAddingNew, sortByPrio, setSortByPrio,
|
||||
isAddingNew, setIsAddingNew, sortByPrio, setSortByPrio, sortByDue, setSortByDue,
|
||||
addingCategory, setAddingCategory, newCategoryName, setNewCategoryName,
|
||||
members, categories, today, filtered, selectedItem,
|
||||
totalCount, doneCount, overdueCount, myCount,
|
||||
@@ -89,7 +89,7 @@ export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tr
|
||||
// full item order so unfiltered tasks keep their place.
|
||||
const [dragId, setDragId] = useState<number | null>(null)
|
||||
const [overId, setOverId] = useState<number | null>(null)
|
||||
const canReorder = canEdit && !sortByPrio
|
||||
const canReorder = canEdit && !sortByPrio && !sortByDue
|
||||
|
||||
const handleReorderDrop = (targetId: number) => {
|
||||
const from = dragId
|
||||
@@ -162,7 +162,7 @@ export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tr
|
||||
{!isMobile && <div style={{ fontSize: 'calc(10px * var(--fs-scale-caption, 1))', fontWeight: 600, color: 'var(--text-faint)', padding: '16px 12px 4px', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
||||
{t('todo.sidebar.sortBy')}
|
||||
</div>}
|
||||
<button type="button" onClick={() => setSortByPrio(v => !v)}
|
||||
<button type="button" onClick={() => { setSortByDue(false); setSortByPrio(v => !v) }}
|
||||
title={isMobile ? t('todo.priority') : undefined}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: isMobile ? 'center' : 'flex-start',
|
||||
@@ -177,6 +177,21 @@ export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tr
|
||||
<Flag size={isMobile ? 18 : 15} style={{ flexShrink: 0, opacity: 0.7 }} />
|
||||
{!isMobile && <span style={{ flex: 1, textAlign: 'left' }}>{t('todo.priority')}</span>}
|
||||
</button>
|
||||
<button type="button" onClick={() => { setSortByPrio(false); setSortByDue(v => !v) }}
|
||||
title={isMobile ? t('todo.detail.dueDate') : undefined}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: isMobile ? 'center' : 'flex-start',
|
||||
gap: isMobile ? 0 : 8, width: '100%', padding: isMobile ? '8px 0' : '7px 12px',
|
||||
border: 'none', borderRadius: 8, cursor: 'pointer', fontFamily: 'inherit', fontSize: 'calc(13px * var(--fs-scale-body, 1))',
|
||||
background: sortByDue ? '#f59e0b12' : 'transparent',
|
||||
color: sortByDue ? '#f59e0b' : 'var(--text-secondary)',
|
||||
fontWeight: sortByDue ? 600 : 400, transition: 'all 0.1s',
|
||||
}}
|
||||
onMouseEnter={e => { if (!sortByDue) e.currentTarget.style.background = 'var(--bg-hover)' }}
|
||||
onMouseLeave={e => { if (!sortByDue) e.currentTarget.style.background = 'transparent' }}>
|
||||
<Calendar size={isMobile ? 18 : 15} style={{ flexShrink: 0, opacity: 0.7 }} />
|
||||
{!isMobile && <span style={{ flex: 1, textAlign: 'left' }}>{t('todo.detail.dueDate')}</span>}
|
||||
</button>
|
||||
|
||||
{/* Categories */}
|
||||
{!isMobile && <div style={{ fontSize: 'calc(10px * var(--fs-scale-caption, 1))', fontWeight: 600, color: 'var(--text-faint)', padding: '16px 12px 4px', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
||||
|
||||
@@ -45,6 +45,7 @@ export function useTodoList(tripId: number, items: TodoItem[], addItemSignal: nu
|
||||
lastHandledAddSignal.current = addItemSignal
|
||||
}, [addItemSignal])
|
||||
const [sortByPrio, setSortByPrio] = useState(false)
|
||||
const [sortByDue, setSortByDue] = useState(false)
|
||||
const [addingCategory, setAddingCategory] = useState(false)
|
||||
const [newCategoryName, setNewCategoryName] = useState('')
|
||||
const [members, setMembers] = useState<Member[]>([])
|
||||
@@ -86,8 +87,15 @@ export function useTodoList(tripId: number, items: TodoItem[], addItemSignal: nu
|
||||
const bp = b.priority || 99
|
||||
return ap - bp
|
||||
})
|
||||
// Nearest deadline first, undated tasks after all dated ones; ties keep the
|
||||
// manual order, the stable sort leaves them untouched (#2205).
|
||||
else if (sortByDue) result = [...result].sort((a, b) => {
|
||||
if (!a.due_date) return b.due_date ? 1 : 0
|
||||
if (!b.due_date) return -1
|
||||
return a.due_date < b.due_date ? -1 : a.due_date > b.due_date ? 1 : 0
|
||||
})
|
||||
return result
|
||||
}, [items, filter, currentUserId, today, sortByPrio])
|
||||
}, [items, filter, currentUserId, today, sortByPrio, sortByDue])
|
||||
|
||||
const selectedItem = items.find(i => i.id === selectedId) || null
|
||||
const totalCount = items.length
|
||||
@@ -109,7 +117,7 @@ export function useTodoList(tripId: number, items: TodoItem[], addItemSignal: nu
|
||||
return {
|
||||
canEdit, t, formatDate, toggleTodoItem, reorderTodoItems,
|
||||
isMobile, filter, setFilter, selectedId, setSelectedId,
|
||||
isAddingNew, setIsAddingNew, sortByPrio, setSortByPrio,
|
||||
isAddingNew, setIsAddingNew, sortByPrio, setSortByPrio, sortByDue, setSortByDue,
|
||||
addingCategory, setAddingCategory, newCategoryName, setNewCategoryName,
|
||||
members, categories, today, filtered, selectedItem,
|
||||
totalCount, doneCount, overdueCount, myCount,
|
||||
|
||||
Reference in New Issue
Block a user