fix(sections): a section can hold more than one task

Reported as "sections can only have one task", and that is exactly what it looked
like.

TaskList read the dragged task from its own `draggingId`, which is only set when
the drag *started* in that list. A task dragged in from the unsectioned rows above,
or from another section, arrived as null and the handler returned early — after it
had already called `stopPropagation`, so the section around it never got the drop
either.

So a section could only be dropped into while it was empty, because an empty one
has no row in the way to swallow the drop. Put one task in and there was no route
to a second. The id comes out of `dataTransfer` at drop time now, and `dragover`
decides with `carries()` rather than local state — which is the only thing a target
can read while a drag is in the air.

The stuck frame was the same cause: the section's own cleanup never ran, so the
highlight stayed lit after the drop. It is cleared on `dragend` now, which fires
once per drag on the source and bubbles to the window — the one place that is true
however a drag ends, including one abandoned with Escape, which produces no drop at
all and left the frame standing too.

The smoke test drops a second task onto the row already in the section, which is
the gesture that did nothing, and runs in WebKit as well.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kristian Wind
2026-08-18 10:35:06 +02:00
co-authored by Claude Opus 5
parent 6853897d00
commit d6d5bf590b
6 changed files with 168 additions and 6 deletions
+56
View File
@@ -365,3 +365,59 @@ func TestAProjectCanBeCreatedIntoAGroup(t *testing.T) {
t.Errorf("the stored project has group_id = %v, want %q", found["group_id"], groupID)
}
}
// A section holds as many tasks as you put in it.
//
// Reported from use: "sections can only have one task". Driven through the same
// endpoint the interface uses, with the same `after_id` the drop handler sends —
// the second task goes after the first, which is the case that would fail if
// positioning inside a section were wrong.
func TestASectionHoldsMoreThanOneTask(t *testing.T) {
ts := newTestServer(t)
ts.bootstrap(t)
_, project := ts.do(t, "POST", "/api/v1/projects", map[string]any{"name": "Skovvænget"})
projectID := project["id"].(string)
_, section := ts.do(t, "POST", "/api/v1/projects/"+projectID+"/sections",
map[string]any{"name": "Håndværker"})
sectionID := section["id"].(string)
var previous string
for _, content := range []string{"knager på badeværelset", "plade på opvaskemaskine", "fuge om vinduet"} {
_, created := ts.do(t, "POST", "/api/v1/tasks", map[string]any{
"content": content, "project_id": projectID,
})
id := created["id"].(string)
resp, moved := ts.do(t, "POST", "/api/v1/tasks/"+id+"/move", map[string]any{
"project_id": projectID, "section_id": sectionID, "after_id": previous,
})
if resp.StatusCode != http.StatusOK {
t.Fatalf("move %q: status %d, body %v", content, resp.StatusCode, moved)
}
if moved["section_id"] != sectionID {
t.Fatalf("%q did not land in the section: %v", content, moved["section_id"])
}
previous = id
}
_, listed := ts.do(t, "GET", "/api/v1/tasks?project_id="+projectID, nil)
tasks, _ := listed["tasks"].([]any)
inSection := []string{}
for _, raw := range tasks {
task := raw.(map[string]any)
if task["section_id"] == sectionID {
inSection = append(inSection, task["content"].(string))
}
}
if len(inSection) != 3 {
t.Fatalf("the section holds %d tasks, want 3: %v", len(inSection), inSection)
}
// And in the order they were placed, since each went after the one before it.
want := []string{"knager på badeværelset", "plade på opvaskemaskine", "fuge om vinduet"}
for i, content := range want {
if inSection[i] != content {
t.Errorf("position %d is %q, want %q — the whole list: %v", i, inSection[i], content, inSection)
}
}
}