mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add search and filter support to chats endpoint (#25391)
Fixes https://linear.app/codercom/issue/CODAGT-432 Adds structured search/filter capabilities to the `GET /api/experimental/chats/` endpoint via the `q` query parameter. All filters use explicit `key:value` syntax; bare terms are rejected to reserve them for potential future full-text search. > Generated by Coder Agents Co-authored-by: Danielle Maywood <danielle@themaywoods.com> Co-authored-by: Jaayden Halko <jaayden.halko@gmail.com>
This commit is contained in:
co-authored by
Danielle Maywood
Jaayden Halko
parent
46e93e6325
commit
b7525a9b40
@@ -543,7 +543,13 @@ func Tasks(ctx context.Context, db database.Store, query string, actorID uuid.UU
|
||||
// Chats parses a search query for chats.
|
||||
//
|
||||
// Supported query parameters:
|
||||
// - archived: boolean (default: false, excludes archived chats unless explicitly set)
|
||||
// - title: case-insensitive title substring match via ILIKE (bare terms
|
||||
// are rejected; use title:<value> for title filtering)
|
||||
// - archived: boolean (default: false, excludes archived chats unless
|
||||
// explicitly set)
|
||||
// - has_unread: nullable boolean (filter by unread message status)
|
||||
// - pr_status: repeated or comma-separated list of draft, open,
|
||||
// merged, closed
|
||||
// - diff_url: string (matches chats whose linked diff URL equals the
|
||||
// given value, case-insensitively; URLs typically contain ':' so
|
||||
// they must be quoted, e.g. q=diff_url:"https://github.com/o/r/pull/1")
|
||||
@@ -570,6 +576,16 @@ func Chats(query string) (database.GetChatsParams, []codersdk.ValidationError) {
|
||||
|
||||
parser := httpapi.NewQueryParamParser()
|
||||
filter.Archived = parser.NullableBoolean(values, filter.Archived, "archived")
|
||||
filter.HasUnread = parser.NullableBoolean(values, filter.HasUnread, "has_unread")
|
||||
filter.PullRequestStatuses = httpapi.ParseCustomList(parser, values, nil, "pr_status", func(v string) (string, error) {
|
||||
normalizedPRStatus := strings.ToLower(strings.TrimSpace(v))
|
||||
switch normalizedPRStatus {
|
||||
case "draft", "open", "merged", "closed":
|
||||
return normalizedPRStatus, nil
|
||||
default:
|
||||
return "", xerrors.Errorf("%q is not a valid value", v)
|
||||
}
|
||||
})
|
||||
if diffURL := parser.String(values, "", "diff_url"); diffURL != "" {
|
||||
if err := validateDiffURL(diffURL); err != nil {
|
||||
parser.Errors = append(parser.Errors, codersdk.ValidationError{
|
||||
@@ -581,6 +597,8 @@ func Chats(query string) (database.GetChatsParams, []codersdk.ValidationError) {
|
||||
}
|
||||
}
|
||||
|
||||
filter.TitleQuery = parser.String(values, "", "title")
|
||||
|
||||
parser.ErrorExcessParams(values)
|
||||
return filter, parser.Errors
|
||||
}
|
||||
|
||||
@@ -1257,6 +1257,96 @@ func TestSearchChats(t *testing.T) {
|
||||
Archived: sql.NullBool{Bool: false, Valid: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "HasUnreadTrue",
|
||||
Query: "has_unread:true",
|
||||
Expected: database.GetChatsParams{
|
||||
Archived: sql.NullBool{Bool: false, Valid: true},
|
||||
HasUnread: sql.NullBool{Bool: true, Valid: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "HasUnreadFalse",
|
||||
Query: "has_unread:false",
|
||||
Expected: database.GetChatsParams{
|
||||
Archived: sql.NullBool{Bool: false, Valid: true},
|
||||
HasUnread: sql.NullBool{Bool: false, Valid: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "HasUnreadInvalid",
|
||||
Query: "has_unread:bogus",
|
||||
ExpectedErrorContains: "has_unread",
|
||||
},
|
||||
{
|
||||
Name: "PRStatusDraft",
|
||||
Query: "pr_status:draft",
|
||||
Expected: database.GetChatsParams{
|
||||
Archived: sql.NullBool{Bool: false, Valid: true},
|
||||
PullRequestStatuses: []string{"draft"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "PRStatusOpen",
|
||||
Query: "pr_status:open",
|
||||
Expected: database.GetChatsParams{
|
||||
Archived: sql.NullBool{Bool: false, Valid: true},
|
||||
PullRequestStatuses: []string{"open"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "PRStatusMerged",
|
||||
Query: "pr_status:merged",
|
||||
Expected: database.GetChatsParams{
|
||||
Archived: sql.NullBool{Bool: false, Valid: true},
|
||||
PullRequestStatuses: []string{"merged"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "PRStatusClosed",
|
||||
Query: "pr_status:closed",
|
||||
Expected: database.GetChatsParams{
|
||||
Archived: sql.NullBool{Bool: false, Valid: true},
|
||||
PullRequestStatuses: []string{"closed"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "PRStatusMultipleRepeated",
|
||||
Query: "pr_status:draft pr_status:merged",
|
||||
Expected: database.GetChatsParams{
|
||||
Archived: sql.NullBool{Bool: false, Valid: true},
|
||||
PullRequestStatuses: []string{"draft", "merged"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "PRStatusMultipleCSV",
|
||||
Query: "pr_status:draft,closed",
|
||||
Expected: database.GetChatsParams{
|
||||
Archived: sql.NullBool{Bool: false, Valid: true},
|
||||
PullRequestStatuses: []string{"draft", "closed"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "PRStatusValueCaseInsensitive",
|
||||
Query: "pr_status:DRAFT",
|
||||
Expected: database.GetChatsParams{
|
||||
Archived: sql.NullBool{Bool: false, Valid: true},
|
||||
PullRequestStatuses: []string{"draft"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "PRStatusInvalid",
|
||||
Query: "pr_status:review",
|
||||
ExpectedErrorContains: "pr_status",
|
||||
},
|
||||
{
|
||||
Name: "PRStatusWithArchived",
|
||||
Query: "archived:true pr_status:open",
|
||||
Expected: database.GetChatsParams{
|
||||
Archived: sql.NullBool{Bool: true, Valid: true},
|
||||
PullRequestStatuses: []string{"open"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "ExtraParam",
|
||||
Query: "archived:true invalid:param",
|
||||
@@ -1336,6 +1426,44 @@ func TestSearchChats(t *testing.T) {
|
||||
Query: `diff_url:"http://%41:8080/"`,
|
||||
ExpectedErrorContains: "not a valid URL",
|
||||
},
|
||||
{
|
||||
Name: "TitleSearch",
|
||||
Query: `title:"hello world"`,
|
||||
Expected: database.GetChatsParams{
|
||||
Archived: sql.NullBool{Bool: false, Valid: true},
|
||||
TitleQuery: "hello world",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "TitleSearchWithArchived",
|
||||
Query: `title:"my chat" archived:true`,
|
||||
Expected: database.GetChatsParams{
|
||||
Archived: sql.NullBool{Bool: true, Valid: true},
|
||||
TitleQuery: "my chat",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "TitleSearchSingleWord",
|
||||
Query: "title:deploy",
|
||||
Expected: database.GetChatsParams{
|
||||
Archived: sql.NullBool{Bool: false, Valid: true},
|
||||
TitleQuery: "deploy",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "TitleSearchWithDiffURL",
|
||||
Query: `title:deploy diff_url:"https://github.com/coder/coder/pull/456"`,
|
||||
Expected: database.GetChatsParams{
|
||||
Archived: sql.NullBool{Bool: false, Valid: true},
|
||||
TitleQuery: "deploy",
|
||||
DiffURL: sql.NullString{String: "https://github.com/coder/coder/pull/456", Valid: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "BareTermsRejected",
|
||||
Query: "some random words",
|
||||
ExpectedErrorContains: `unsupported search term: "some random words"`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range testCases {
|
||||
|
||||
Reference in New Issue
Block a user