feat: add task and habit reordering
ci / docker (push) Successful in 3m33s

This commit is contained in:
2026-09-08 06:51:31 +08:00
parent a02b6f660c
commit 988d131d9b
13 changed files with 329 additions and 22 deletions
+13 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import { filterTasks, groupTaskTree, renderMarkdown, toDateTimeLocal } from './task-utils'
import { filterTasks, groupTaskTree, moveItemWithinScope, renderMarkdown, toDateTimeLocal } from './task-utils'
type SearchTask = {
id: string
@@ -26,6 +26,18 @@ describe('task utilities', () => {
expect(groupTaskTree(tasks)).toEqual([{ task: tasks[0], subtasks: [tasks[1]] }, { task: tasks[2], subtasks: [] }])
})
it('moves an item before or after another item without changing other scopes', () => {
const rows = [
{ id: 'a', parent_id: null },
{ id: 'a1', parent_id: 'a' },
{ id: 'b', parent_id: null },
{ id: 'c', parent_id: null },
]
expect(moveItemWithinScope(rows, 'c', 'a', 'before').map((row) => row.id)).toEqual(['c', 'a1', 'a', 'b'])
expect(moveItemWithinScope(rows, 'a', 'c', 'after').map((row) => row.id)).toEqual(['b', 'a1', 'c', 'a'])
expect(moveItemWithinScope(rows, 'a1', 'b', 'before')).toEqual(rows)
})
it('renders safe basic markdown and strips unsafe html', () => {
const html = renderMarkdown('# Plan\n**bold** [link](https://example.com)\n<script>alert(1)</script>')
expect(html).toContain('<h1>Plan</h1>')
+16
View File
@@ -88,6 +88,22 @@ export function groupTaskTree<T extends MinimalTask>(tasks: T[]) {
return tasks.filter((task) => !task.parent_id).map((task) => ({ task, subtasks: children.get(task.id) ?? [] }))
}
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)
const target = items.find((item) => item.id === targetId)
if (!source || !target || (source.parent_id ?? null) !== (target.parent_id ?? null)) return items
const scoped = items.filter((item) => (item.parent_id ?? null) === (source.parent_id ?? null))
const sourceIndex = scoped.findIndex((item) => item.id === sourceId)
const targetIndex = scoped.findIndex((item) => item.id === targetId)
const reordered = scoped.filter((item) => item.id !== sourceId)
const adjustedTargetIndex = reordered.findIndex((item) => item.id === targetId)
reordered.splice(adjustedTargetIndex + (placement === 'after' ? 1 : 0), 0, source)
if (sourceIndex === targetIndex || reordered.every((item, index) => item.id === scoped[index]?.id)) return items
const reorderedIterator = reordered[Symbol.iterator]()
return items.map((item) => (item.parent_id ?? null) === (source.parent_id ?? null) ? reorderedIterator.next().value! : item)
}
export function toDateTimeLocal(value: string | null | undefined) {
if (!value) return ''
const date = new Date(value)