feat: sort tasks by due time
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildTaskRrule, defaultTaskDueAt, filterTasks, groupTaskTree, moveItemWithinScope, parseTaskRrule, renderMarkdown, toDateTimeLocal } from './task-utils'
|
||||
import { buildTaskRrule, classifyTaskForToday, defaultTaskDueAt, filterTasks, groupTaskTree, isSameTaskSortTier, moveItemWithinScope, parseTaskRrule, renderMarkdown, toDateTimeLocal } from './task-utils'
|
||||
|
||||
type SearchTask = {
|
||||
id: string
|
||||
@@ -33,6 +33,25 @@ describe('task utilities', () => {
|
||||
expect(groupTaskTree([parent])).toEqual([{ task: parent, subtasks: [child] }])
|
||||
})
|
||||
|
||||
it('classifies due edits for Today membership', () => {
|
||||
const start = new Date('2026-09-10T00:00:00+08:00')
|
||||
const end = new Date('2026-09-11T00:00:00+08:00')
|
||||
expect(classifyTaskForToday({ completed: false, due_at: '2026-09-09T12:00:00+08:00' }, start, end)).toBe('overdue')
|
||||
expect(classifyTaskForToday({ completed: false, due_at: '2026-09-10T12:00:00+08:00' }, start, end)).toBe('today')
|
||||
expect(classifyTaskForToday({ completed: false, due_at: '2026-09-11T12:00:00+08:00' }, start, end)).toBe('outside')
|
||||
expect(classifyTaskForToday({ completed: true, due_at: '2026-09-09T12:00:00+08:00' }, start, end)).toBe('outside')
|
||||
})
|
||||
|
||||
it('only allows manual movement within the same completion and deadline tier', () => {
|
||||
const noDueOpen = { completed: false, due_at: null }
|
||||
expect(isSameTaskSortTier(noDueOpen, { completed: false, due_at: null })).toBe(true)
|
||||
expect(isSameTaskSortTier({ completed: false, due_at: '2026-09-10T08:00:00Z' }, { completed: false, due_at: '2026-09-10T08:00:00Z' })).toBe(true)
|
||||
expect(isSameTaskSortTier({ completed: false, due_at: '2026-09-10T08:00:00Z' }, { completed: false, due_at: '2026-09-10T16:00:00+08:00' })).toBe(true)
|
||||
expect(isSameTaskSortTier(noDueOpen, { completed: false, due_at: '2026-09-10T08:00:00Z' })).toBe(false)
|
||||
expect(isSameTaskSortTier(noDueOpen, { completed: true, due_at: null })).toBe(false)
|
||||
expect(isSameTaskSortTier({ completed: false, due_at: '2026-09-10T08:00:00Z' }, { completed: false, due_at: '2026-09-11T08:00:00Z' })).toBe(false)
|
||||
})
|
||||
|
||||
it('moves an item before or after another item without changing other scopes', () => {
|
||||
const rows = [
|
||||
{ id: 'a', parent_id: null },
|
||||
|
||||
@@ -4,6 +4,7 @@ export type MinimalTask = {
|
||||
description?: string
|
||||
parent_id?: string | null
|
||||
completed?: boolean
|
||||
due_at?: string | null
|
||||
list_name?: string
|
||||
subtasks?: MinimalTask[]
|
||||
}
|
||||
@@ -92,6 +93,30 @@ export function groupTaskTree<T extends MinimalTask>(tasks: T[]) {
|
||||
}))
|
||||
}
|
||||
|
||||
export function classifyTaskForToday(
|
||||
task: Pick<MinimalTask, 'completed' | 'due_at'>,
|
||||
start: Date,
|
||||
end: Date,
|
||||
): 'overdue' | 'today' | 'outside' {
|
||||
if (task.completed || !task.due_at) return 'outside'
|
||||
const due = Date.parse(task.due_at)
|
||||
if (!Number.isFinite(due)) return 'outside'
|
||||
if (due < start.valueOf()) return 'overdue'
|
||||
return due < end.valueOf() ? 'today' : 'outside'
|
||||
}
|
||||
|
||||
export function isSameTaskSortTier(
|
||||
source: Pick<MinimalTask, 'completed' | 'due_at'>,
|
||||
target: Pick<MinimalTask, 'completed' | 'due_at'>,
|
||||
) {
|
||||
if (Boolean(source.completed) !== Boolean(target.completed)) return false
|
||||
if (!source.due_at && !target.due_at) return true
|
||||
if (!source.due_at || !target.due_at) return false
|
||||
const sourceTime = Date.parse(source.due_at)
|
||||
const targetTime = Date.parse(target.due_at)
|
||||
return Number.isFinite(sourceTime) && Number.isFinite(targetTime) && sourceTime === targetTime
|
||||
}
|
||||
|
||||
export function moveItemWithinScope<T extends { id: string; parent_id?: string | null }>(items: T[], sourceId: string, targetId: string, placement: 'before' | 'after') {
|
||||
if (sourceId === targetId) return items
|
||||
const source = items.find((item) => item.id === sourceId)
|
||||
|
||||
Reference in New Issue
Block a user