feat(tasks): write a task straight into a section

The box at the top puts things in the project with no section, which is right for
capturing. But a project has sections because the work belongs somewhere, and
dragging every new task down into the right one is a second gesture for something
you already knew when you typed it.

Two ways in, because they answer different moments. A field at the foot of each
section and each board column — standing there is enough, no syntax to know. And
`/section` in quick add, for the box at the top and for Today and Upcoming, where
you are not standing in a section at all.

The field goes through quick add rather than createTask, so a line written there
still parses its dates, priorities and labels.

The sigil needed care: '#' and '@' never turn up in a Danish task line by accident
and '/' turns up constantly. It has to follow whitespace and be followed
immediately by the name, which "km/t", "3/4" and "20 kr / stk" all fail. The bare
form is greedy, because "Kunder/Ordrer" is a real section name and the sigil is
inside it.

A section id from the client is checked against the project the task actually
landed in. A `#project` in the same line can move the task elsewhere, and the id
would otherwise follow it into a project that section does not belong to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kristian Wind
2026-08-18 11:13:10 +02:00
co-authored by Claude Opus 5
parent 9cc7df69f0
commit 0dbf7a40bf
11 changed files with 463 additions and 9 deletions
+33
View File
@@ -630,3 +630,36 @@ func (db *DB) ReorderProjects(ctx context.Context, userID string, ids []string)
return nil
})
}
// SectionByName resolves a "/name" from quick add within one project.
//
// Case-insensitive, and scoped to the project the task is landing in — a section
// belongs to exactly one project, so the name cannot be looked up before that is
// decided. Two projects may both have a "Produktion" and they are different
// sections; asking without the project would be asking the wrong question.
func (db *DB) SectionByName(ctx context.Context, projectID, name string) (string, error) {
var id string
err := db.QueryRowContext(ctx, `
SELECT id FROM sections
WHERE project_id = ? AND lower(name) = lower(?) AND deleted_at IS NULL
ORDER BY sort_order
LIMIT 1`, projectID, name).Scan(&id)
if errors.Is(err, sql.ErrNoRows) {
return "", ErrNotFound
}
return id, err
}
// SectionInProject reports whether a section id belongs to the given project.
//
// Quick add takes a section id from the client — the field at the foot of a
// section passes the one it sits in — and a "#project" in the same line can move
// the task somewhere else entirely. Without this the id would follow it, and the
// task would land in a section belonging to a project it is not in.
func (db *DB) SectionInProject(ctx context.Context, sectionID, projectID string) (bool, error) {
var n int
err := db.QueryRowContext(ctx,
`SELECT count(*) FROM sections WHERE id = ? AND project_id = ? AND deleted_at IS NULL`,
sectionID, projectID).Scan(&n)
return n == 1, err
}