From db6e5a2a55b3be679b05f592d747ac3d5f3bbdba Mon Sep 17 00:00:00 2001 From: Bobby Ho Date: Fri, 26 Jun 2026 09:48:05 -0700 Subject: [PATCH] feat(codersdk): add Organization field to WorkspaceFilter (#26727) Add `Organization` as a first-class field to `WorkspaceFilter` so Go SDK callers can filter workspaces by organization name or UUID without constructing a raw `FilterQuery` string. The backend already supports `organization:` as a search parameter via `searchquery.Workspaces()`. This change surfaces it consistently alongside the existing `Owner`, `Template`, and `Status` fields. Closes https://github.com/coder/coder/issues/21545 --- codersdk/workspaces.go | 5 ++ codersdk/workspaces_internal_test.go | 88 ++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 codersdk/workspaces_internal_test.go diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index b520f27e4f..074acb6f90 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -520,6 +520,8 @@ type WorkspaceFilter struct { Name string `json:"name,omitempty" typescript:"-"` // Status is a workspace status, which is really the status of the latest build Status string `json:"status,omitempty" typescript:"-"` + // Organization is an organization name or ID + Organization string `json:"organization,omitempty" typescript:"-"` // Offset is the number of workspaces to skip before returning results. Offset int `json:"offset,omitempty" typescript:"-"` // Limit is a limit on the number of workspaces returned. @@ -553,6 +555,9 @@ func (f WorkspaceFilter) asRequestOption() RequestOption { if f.Status != "" { params = append(params, fmt.Sprintf("status:%q", f.Status)) } + if f.Organization != "" { + params = append(params, fmt.Sprintf("organization:%q", f.Organization)) + } if f.Shared != nil { params = append(params, fmt.Sprintf("shared:%v", *f.Shared)) } diff --git a/codersdk/workspaces_internal_test.go b/codersdk/workspaces_internal_test.go new file mode 100644 index 0000000000..58174b407d --- /dev/null +++ b/codersdk/workspaces_internal_test.go @@ -0,0 +1,88 @@ +package codersdk + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestWorkspaceFilterAsRequestOption(t *testing.T) { + t.Parallel() + + // applyFilter applies the filter's request option to a blank request and + // returns the resulting "q" query parameter. + applyFilter := func(f WorkspaceFilter) string { + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://example.com", nil) + require.NoError(t, err) + f.asRequestOption()(req) + return req.URL.Query().Get("q") + } + + tests := []struct { + name string + filter WorkspaceFilter + contains []string + empty bool + }{ + { + name: "Empty", + filter: WorkspaceFilter{}, + empty: true, + }, + { + name: "Owner", + filter: WorkspaceFilter{Owner: "alice"}, + contains: []string{`owner:"alice"`}, + }, + { + name: "Name", + filter: WorkspaceFilter{Name: "my-workspace"}, + contains: []string{`name:"my-workspace"`}, + }, + { + name: "Template", + filter: WorkspaceFilter{Template: "base"}, + contains: []string{`template:"base"`}, + }, + { + name: "Status", + filter: WorkspaceFilter{Status: "running"}, + contains: []string{`status:"running"`}, + }, + { + name: "Organization", + filter: WorkspaceFilter{Organization: "acme"}, + contains: []string{`organization:"acme"`}, + }, + { + name: "OrganizationByUUID", + filter: WorkspaceFilter{Organization: "550e8400-e29b-41d4-a716-446655440000"}, + contains: []string{`organization:"550e8400-e29b-41d4-a716-446655440000"`}, + }, + { + name: "MultipleFields", + filter: WorkspaceFilter{Owner: "alice", Organization: "acme", Status: "running"}, + contains: []string{ + `owner:"alice"`, + `organization:"acme"`, + `status:"running"`, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + q := applyFilter(tt.filter) + if tt.empty { + require.Empty(t, q) + return + } + for _, s := range tt.contains { + require.Contains(t, q, s) + } + }) + } +}