fix: sort TickTick tasks so parents come before children

TickTick CSV exports don't guarantee parent tasks appear before their
subtasks. When a child row came first, the shared migration pipeline
tried to create a title-less placeholder for the missing parent, which
failed with 'Task title cannot be empty'.

Resolves go-vikunja/vikunja#2487
This commit is contained in:
kolaente 2026-03-26 15:50:47 +01:00 committed by kolaente
parent c49636430f
commit 9b1c52e9e3
1 changed files with 40 additions and 0 deletions

View File

@ -84,7 +84,47 @@ func (date *tickTickTime) UnmarshalCSV(csv string) (err error) {
return err
}
// sortParentsBeforeChildren reorders tasks so that every parent task
// appears before any of its children. Tasks without a parent come first.
// The relative order of siblings / unrelated tasks is preserved.
func sortParentsBeforeChildren(tasks []*tickTickTask) []*tickTickTask {
tasksByID := make(map[int64]*tickTickTask, len(tasks))
for _, t := range tasks {
tasksByID[t.TaskID] = t
}
placed := make(map[int64]bool, len(tasks))
result := make([]*tickTickTask, 0, len(tasks))
var place func(t *tickTickTask)
place = func(t *tickTickTask) {
if placed[t.TaskID] {
return
}
// If this task has a parent that we know about, place the parent first.
if t.ParentID != 0 {
if parent, ok := tasksByID[t.ParentID]; ok {
place(parent)
}
}
placed[t.TaskID] = true
result = append(result, t)
}
for _, t := range tasks {
place(t)
}
return result
}
func convertTickTickToVikunja(tasks []*tickTickTask) (result []*models.ProjectWithTasksAndBuckets) {
// Sort tasks so that parent tasks always come before their children.
// Without this, create_from_structure.go would try to create a
// placeholder for a not-yet-seen parent, which fails because the
// placeholder has no title. (go-vikunja/vikunja#2487)
tasks = sortParentsBeforeChildren(tasks)
var pseudoParentID int64 = 1
result = []*models.ProjectWithTasksAndBuckets{
{