feat(search): find a task by its label
Search covered task text only, so "regnskab" found nothing with the task labelled @regnskab sitting in front of you — which reads as the search box having lost it. A label is often the only thing you remember. "That thing about the accounts" is a search for @regnskab, not for any word in the title. The label side is a plain lowercase match rather than a second FTS index. Labels are short, and their names are typed by the same person now searching for them, so there is no spelling to be generous about — the diacritic folding that earns its keep on task text does not earn a second index here. The join sits inside the WHERE the visibility rules already build, so somebody else's label cannot pull their task into your results. There is a test for that, because it is the kind of thing an OR is good at breaking. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
committed by
Kristian Wind
co-authored by
Claude Opus 5
parent
3b521b463c
commit
d37d240625
@@ -0,0 +1,55 @@
|
||||
package httpapi
|
||||
|
||||
import "testing"
|
||||
|
||||
// A label is how you find a task whose wording you have forgotten. Search
|
||||
// covered the text only, so "regnskab" found nothing even with the task
|
||||
// labelled @regnskab in front of you.
|
||||
func TestSearchFindsTasksByLabel(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
ts.bootstrap(t)
|
||||
|
||||
ts.do(t, "POST", "/api/v1/tasks/quick-add",
|
||||
map[string]any{"text": "ring til revisoren @regnskab"})
|
||||
ts.do(t, "POST", "/api/v1/tasks/quick-add",
|
||||
map[string]any{"text": "køb kaffe"})
|
||||
|
||||
_, body := ts.do(t, "GET", "/api/v1/search?q=regnskab", nil)
|
||||
tasks, _ := body["tasks"].([]any)
|
||||
if len(tasks) != 1 {
|
||||
t.Fatalf("found %d tasks searching for a label, want 1: %v", len(tasks), body)
|
||||
}
|
||||
if got := tasks[0].(map[string]any)["content"]; got != "ring til revisoren" {
|
||||
t.Errorf("found %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The text search still works, and a label match does not swallow it.
|
||||
func TestSearchStillFindsTasksByText(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
ts.bootstrap(t)
|
||||
|
||||
ts.do(t, "POST", "/api/v1/tasks/quick-add", map[string]any{"text": "køb grøn maling"})
|
||||
|
||||
// Diacritic folding still applies to the text side.
|
||||
_, body := ts.do(t, "GET", "/api/v1/search?q=gron", nil)
|
||||
if tasks, _ := body["tasks"].([]any); len(tasks) != 1 {
|
||||
t.Errorf("searching \"gron\" found %d tasks, want 1", len(tasks))
|
||||
}
|
||||
}
|
||||
|
||||
// Somebody else's label must not pull their task into your results — the label
|
||||
// join has to stay inside the visibility the outer query already applies.
|
||||
func TestSearchByLabelStaysWithinWhatYouCanSee(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
ts.bootstrap(t)
|
||||
|
||||
other := ts.newUser(t, "[email protected]", "Anden")
|
||||
other.do(t, "POST", "/api/v1/tasks/quick-add",
|
||||
map[string]any{"text": "deres hemmelighed @regnskab"})
|
||||
|
||||
_, body := ts.do(t, "GET", "/api/v1/search?q=regnskab", nil)
|
||||
if tasks, _ := body["tasks"].([]any); len(tasks) != 0 {
|
||||
t.Errorf("found somebody else's task: %v", tasks)
|
||||
}
|
||||
}
|
||||
+17
-2
@@ -136,8 +136,23 @@ func (db *DB) ListTasks(ctx context.Context, userID string, f TaskFilter) ([]Tas
|
||||
args = append(args, f.DueFrom)
|
||||
}
|
||||
if expr := MatchExpr(f.Search); expr != "" {
|
||||
where = append(where, `t.rowid IN (SELECT rowid FROM tasks_fts WHERE tasks_fts MATCH ?)`)
|
||||
args = append(args, expr)
|
||||
// Text or a label. A label is how you find a task whose wording you have
|
||||
// forgotten — "that thing about the accounts" is a search for @regnskab —
|
||||
// and leaving them out made the box look like it had lost the task.
|
||||
//
|
||||
// The label side is a plain lowercase match rather than another FTS index:
|
||||
// labels are short, and their names are typed by the person searching for
|
||||
// them, so there is no spelling to be generous about. The diacritic
|
||||
// folding that matters for task text does not earn a second index here.
|
||||
where = append(where, `(
|
||||
t.rowid IN (SELECT rowid FROM tasks_fts WHERE tasks_fts MATCH ?)
|
||||
OR t.id IN (
|
||||
SELECT tl.task_id FROM task_labels tl
|
||||
JOIN labels l ON l.id = tl.label_id
|
||||
WHERE lower(l.name) LIKE ?
|
||||
)
|
||||
)`)
|
||||
args = append(args, expr, "%"+strings.ToLower(strings.TrimSpace(f.Search))+"%")
|
||||
}
|
||||
if f.FilterSQL != "" {
|
||||
where = append(where, "("+f.FilterSQL+")")
|
||||
|
||||
Reference in New Issue
Block a user