feat: search users by display name (#27398)

Free-text member search previously matched only username and email, so
typing a person's display name returned no results even though the UI
shows the display name as the primary label. This broadens the free-text
`@search` filter to also match `users.name`.

The change is in three queries: `GetUsers`,
`PaginatedOrganizationMembers`, and `GetGroupMembersByGroupIDPaginated`.
This covers every server-filtered surface: the Users page, the
Organization Members page, the Group Members page, and the
`UserAutocomplete` / `WorkspaceUserAutocomplete` pickers (which query
`GetUsers` with `q`). The org member picker (`MemberAutocomplete`)
filters client-side via cmdk, so display name is added to its
`keywords`.

Explicit filters (`name:`, `username`/`email`) and pagination counts are
unchanged; the group members count still comes from the filtered
`COUNT(*) OVER()` in the same query.

Refs DEVEX-484
Refs DEVEX-565

<details>
<summary>Implementation plan</summary>

## Problem

Member search (both the global Users page and the Organization Members
page) matches only on `username` and `email`. It does not match on the
user's display name (`users.name`), even though the Organization Members
table shows `name` as the primary title. So typing a person's full name
in the search box returns nothing.

Today a bare search term (`alice`) is routed to the SQL `@search`
filter, which only checks `email`/`username`. Display name is only
matched if the user explicitly types `name:alice`, which is
undiscoverable.

## Design decision

Include `name` in the free-text `@search` condition in the affected SQL
queries. A bare term then matches `email OR username OR name`, using the
same case-insensitive substring `ILIKE` already in place. This keeps the
existing explicit `name:` filter working.

Tradeoff: this broadens the meaning of free-text `search` globally
(anything using these queries now also matches display name). This is
the intended behavior, confirmed against DEVEX-565 (display name search
in the user picker).

## Affected files

Backend:
- `coderd/database/queries/users.sql` (`GetUsers`)
- `coderd/database/queries/organizationmembers.sql`
(`PaginatedOrganizationMembers`)
- `coderd/database/queries/groupmembers.sql`
(`GetGroupMembersByGroupIDPaginated`)
- `coderd/database/queries.sql.go` regenerated via `make gen`

Frontend:
- `site/src/components/UserAutocomplete/UserAutocomplete.tsx` (add
`name` to client-side cmdk keywords)

Tests:
- `coderd/coderdtest/users.go` (shared `UsersFilter` helper): added a
`DisplayNameSearch` case and extended search-based expectations to
include `name`. Exercised by `TestGetUsersFilter`,
`TestGetOrgMembersFilter`, and `TestGetGroupMembersFilter`.

Docs:
- `docs/admin/users/index.md`: documented that free-text search matches
username, email, and display name.

## Frontend surface coverage

| Surface | Sends | Backend | Query |
|---|---|---|---|
| Users page | `q` | `GET /users` | `GetUsers` |
| Organization Members page | `q` | paginated members |
`PaginatedOrganizationMembers` |
| Group Members page | `q` | `groupMembers` |
`GetGroupMembersByGroupIDPaginated` |
| User pickers (server-filtered) | `q` | `GET /users` | `GetUsers` |
| Org member picker (client-filtered) | local cmdk | n/a | keyword
change |

## Out of scope

- Trigram/similarity (fuzzy) matching; keeps `ILIKE` substring
semantics.
- Sort/pagination ordering (still `LOWER(username)`).

</details>

---
_Created by Coder Agents on behalf of @aqandrew._
This commit is contained in:
Andrew Aquino
2026-07-28 12:13:58 -07:00
committed by GitHub
parent e657d2ab9d
commit 09a69e624a
7 changed files with 38 additions and 10 deletions
+6 -3
View File
@@ -15036,11 +15036,12 @@ WHERE
ELSE true
END
-- Start filters
-- Filter by email or username
-- Filter by email, username, or name (display name)
AND CASE
WHEN $3 :: text != '' THEN (
user_email ILIKE concat('%', $3, '%')
OR user_username ILIKE concat('%', $3, '%')
OR user_name ILIKE concat('%', $3, '%')
)
ELSE true
END
@@ -20435,11 +20436,12 @@ WHERE
organization_id = $2
ELSE true
END
-- Filter by email or username
-- Filter by email, username, or name (display name)
AND CASE
WHEN $3 :: text != '' THEN (
users.email ILIKE concat('%', $3, '%')
OR users.username ILIKE concat('%', $3, '%')
OR users.name ILIKE concat('%', $3, '%')
)
ELSE true
END
@@ -30922,11 +30924,12 @@ WHERE
ELSE true
END
-- Start filters
-- Filter by email or username
-- Filter by email, username, or name (display name)
AND CASE
WHEN $2 :: text != '' THEN (
email ILIKE concat('%', $2, '%')
OR username ILIKE concat('%', $2, '%')
OR name ILIKE concat('%', $2, '%')
)
ELSE true
END