Skip to content
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
ed2ca46
feat: add pagination, search, and description to org teams page
kmranimesh Feb 10, 2026
be33fd7
Refactor team visibility logic and update search input attributes
kmranimesh Feb 13, 2026
6e5497a
Fix linting errors
kmranimesh Feb 13, 2026
f07ea43
Apply suggestion from @silverwind
silverwind Feb 13, 2026
824e52c
Apply suggestion from @silverwind
silverwind Feb 13, 2026
f5773c1
Fix CanUserSeeAllTeams to handle Site Admins correctly
kmranimesh Feb 13, 2026
69a5dec
Merge branch 'main' into feat/34482-team-list-pagination-search
silverwind Feb 18, 2026
6f58072
optimize team loading and CanUserSeeAllTeams query
kmranimesh Feb 22, 2026
e49147f
Merge branch 'main' into feat/34482-team-list-pagination-search
kmranimesh Feb 22, 2026
1b50a4c
5 teams shown at most in overview
PineBale Apr 17, 2026
962c39e
Merge branch 'pr-36602' into patch-orgpage
PineBale Apr 17, 2026
9c4589a
Revert "optimize team loading and CanUserSeeAllTeams query"
PineBale Apr 17, 2026
9046a7d
Revert "Fix CanUserSeeAllTeams to handle Site Admins correctly"
PineBale Apr 17, 2026
a0657d7
Revert "Apply suggestion from @silverwind"
PineBale Apr 17, 2026
4396287
Revert "Apply suggestion from @silverwind"
PineBale Apr 17, 2026
57cdfa5
Revert "Fix linting errors"
PineBale Apr 17, 2026
082f07f
Revert "Refactor team visibility logic and update search input attrib…
PineBale Apr 17, 2026
51c06e8
Revert "feat: add pagination, search, and description to org teams page"
PineBale Apr 17, 2026
76e9705
Reapply
PineBale Apr 17, 2026
5c58d8b
Button
PineBale Apr 17, 2026
783c886
Reapply
PineBale Apr 17, 2026
b298002
Reapply
PineBale Apr 17, 2026
00e9e0e
lint
PineBale Apr 17, 2026
9763283
Merge remote-tracking branch 'origin/patch-orgpage' into patch-orgpage
PineBale Apr 17, 2026
20e6399
Pager
PineBale Apr 17, 2026
0e86bbc
SearchTeam CASE WHEN name LIKE OwnerTeamName
PineBale Apr 17, 2026
9367c74
Fix pager
PineBale Apr 17, 2026
4fa53ae
const numOfTeamsAtMostInOverview https://github.com/go-gitea/gitea/pu…
PineBale Apr 17, 2026
b5a6d74
CASE WHEN name LIKE strings.ToLower(OwnerTeamName) https://github.com…
PineBale Apr 17, 2026
f117108
Possible shouldSeeAllTeams fix https://github.com/go-gitea/gitea/pull…
PineBale Apr 17, 2026
f2d4c6e
lint
PineBale Apr 17, 2026
a71adcd
Merge branch 'main' into patch-orgpage
PineBale Apr 17, 2026
52391d9
Merge remote-tracking branch 'origin/patch-orgpage' into patch-orgpage
PineBale Apr 17, 2026
a2d23b5
Remove autofocus https://github.com/go-gitea/gitea/pull/37245#issueco…
PineBale Apr 17, 2026
8afd0f4
Add test for org teams page pagination, search, and permissions
silverwind Apr 17, 2026
ed4f12e
fix
wxiaoguang Apr 17, 2026
9ea6110
fix
wxiaoguang Apr 17, 2026
9600ed3
refactor
wxiaoguang Apr 17, 2026
eefd676
add to test
PineBale Apr 17, 2026
4aed576
refactor
wxiaoguang Apr 17, 2026
41e88cb
optimize
wxiaoguang Apr 17, 2026
4cdddb0
avatar size = 32
wxiaoguang Apr 17, 2026
92e6765
remove useless TrimSpace
wxiaoguang Apr 17, 2026
0592df9
Merge branch 'main' into patch-orgpage
silverwind Apr 17, 2026
80c953c
add form action
wxiaoguang Apr 17, 2026
82ed2bf
only show tooltip when needed
wxiaoguang Apr 17, 2026
6044c82
fix layout
wxiaoguang Apr 17, 2026
5bb3387
add team description
wxiaoguang Apr 17, 2026
654dba1
fix layout
wxiaoguang Apr 17, 2026
3041637
fix color
wxiaoguang Apr 17, 2026
acafaa8
fix layout
wxiaoguang Apr 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion models/organization/team_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func SearchTeam(ctx context.Context, opts *SearchTeamOptions) (TeamList, int64,
sess = db.SetSessionPagination(sess, opts)

teams := make([]*Team, 0, opts.PageSize)
count, err := sess.Where(cond).OrderBy("lower_name").FindAndCount(&teams)
count, err := sess.Where(cond).OrderBy("CASE WHEN name=? THEN '' ELSE lower_name END", OwnerTeamName).FindAndCount(&teams)
if err != nil {
return nil, 0, err
}
Expand Down
4 changes: 3 additions & 1 deletion routers/web/org/home.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (

const tplOrgHome templates.TplName = "org/home"

const numOfTeamsAtMostInOverview = 5

// Home show organization home page
func Home(ctx *context.Context) {
uname := ctx.PathParam("username")
Expand Down Expand Up @@ -99,7 +101,7 @@ func home(ctx *context.Context, viewRepositories bool) {
return
}
ctx.Data["Members"] = members
ctx.Data["Teams"] = ctx.Org.Teams
ctx.Data["Teams"] = ctx.Org.Teams[:min(len(ctx.Org.Teams), numOfTeamsAtMostInOverview)]
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0

Expand Down
40 changes: 38 additions & 2 deletions routers/web/org/teams.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,49 @@ func Teams(ctx *context.Context) {
ctx.Data["Title"] = org.FullName
ctx.Data["PageIsOrgTeams"] = true

for _, t := range ctx.Org.Teams {
keyword := ctx.FormTrim("q")
page := max(ctx.FormInt("page"), 1)

opts := &org_model.SearchTeamOptions{
OrgID: org.ID,
ListOptions: db.ListOptions{
Page: page,
PageSize: setting.UI.MembersPagingNum,
},
}

shouldSeeAllOrgTeams, err := context.UserShouldSeeAllOrgTeams(ctx)
if err != nil {
ctx.ServerError("UserShouldSeeAllOrgTeams", err)
return
}

if !shouldSeeAllOrgTeams {
opts.UserID = ctx.Doer.ID
}

if keyword != "" {
opts.Keyword = keyword
opts.IncludeDesc = true
}

teams, count, err := org_model.SearchTeam(ctx, opts)
if err != nil {
ctx.ServerError("SearchTeam", err)
return
}

for _, t := range teams {
if err := t.LoadMembers(ctx); err != nil {
ctx.ServerError("GetMembers", err)
return
}
}
ctx.Data["Teams"] = ctx.Org.Teams
ctx.Data["Teams"] = teams
ctx.Data["Keyword"] = keyword
pager := context.NewPagination(count, setting.UI.MembersPagingNum, page, 5)
pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager

ctx.HTML(http.StatusOK, tplTeams)
}
Expand Down
43 changes: 27 additions & 16 deletions services/context/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,23 +174,12 @@ func OrgAssignment(orgAssignmentOpts OrgAssignmentOptions) func(ctx *Context) {
}

// Team.
shouldSeeAllTeams, err := UserShouldSeeAllOrgTeams(ctx)
if err != nil {
ctx.ServerError("UserShouldSeeAllOrgTeams", err)
return
}
if ctx.Org.IsMember {
shouldSeeAllTeams := false
if ctx.Org.IsOwner {
shouldSeeAllTeams = true
} else {
teams, err := org.GetUserTeams(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("GetUserTeams", err)
return
}
for _, team := range teams {
if team.IncludesAllRepositories && team.HasAdminAccess() {
shouldSeeAllTeams = true
break
}
}
}
if shouldSeeAllTeams {
ctx.Org.Teams, err = org.LoadTeams(ctx)
if err != nil {
Expand Down Expand Up @@ -255,3 +244,25 @@ func OrgAssignment(orgAssignmentOpts OrgAssignmentOptions) func(ctx *Context) {
}
}
}

// UserShouldSeeAllOrgTeams tells if a user has permission to view all teams in the org.
func UserShouldSeeAllOrgTeams(ctx *Context) (bool, error) {
if !ctx.Org.IsMember {
return false, nil
}

if ctx.Org.IsOwner {
return true, nil
}

teams, err := ctx.Org.Organization.GetUserTeams(ctx, ctx.Doer.ID)
if err != nil {
return false, err
}
for _, team := range teams {
if team.IncludesAllRepositories && team.HasAdminAccess() {
return true, nil
}
}
return false, nil
}
13 changes: 12 additions & 1 deletion templates/org/team/teams.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,19 @@
<div class="tw-flex-1">{{ctx.Locale.Tr "org.teams.manage_team_member_prompt"}}</div>
<a class="ui primary button" href="{{.OrgLink}}/teams/new">{{svg "octicon-plus"}} {{ctx.Locale.Tr "org.create_new_team"}}</a>
</div>
<div class="divider"></div>
{{end}}

<div class="ui secondary filter menu tw-mx-0">
<form class="ui form ignore-dirty tw-flex-1" method="get">
<div class="ui fluid action input">
<input type="search" name="q" value="{{$.Keyword}}" placeholder="{{ctx.Locale.Tr "search.team_kind"}}" maxlength="255" spellcheck="false">
<button class="ui button" type="submit">{{svg "octicon-search"}}</button>
</div>
</form>
Comment thread
wxiaoguang marked this conversation as resolved.
Outdated
</div>

<div class="divider"></div>

<div class="ui two column stackable grid">
{{range .Teams}}
<div class="column">
Expand Down Expand Up @@ -42,6 +52,7 @@
</div>
{{end}}
</div>
{{template "base/paginate" .}}
</div>
</div>
<div class="ui g-modal-confirm delete modal" id="leave-team">
Expand Down
44 changes: 44 additions & 0 deletions tests/integration/org_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ import (
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"

"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand All @@ -31,6 +34,7 @@ func TestOrg(t *testing.T) {
t.Run("OrgMembers", testOrgMembers)
t.Run("OrgRestrictedUser", testOrgRestrictedUser)
t.Run("TeamSearch", testTeamSearch)
t.Run("TeamsPage", testTeamsPage)
t.Run("OrgSettings", testOrgSettings)
}

Expand Down Expand Up @@ -251,6 +255,46 @@ func testTeamSearch(t *testing.T) {
})
}

func testTeamsPage(t *testing.T) {
// org 17 has three teams in fixtures: Owners (id 5), test_team (id 8), review_team (id 9).
// user15 is in Owners; user20 is in review_team only; user5 is not a member.
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17})

listTeams := func(t *testing.T, session *TestSession, query string) []string {
req := NewRequestf(t, "GET", "/org/%s/teams%s", org.Name, query)
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
sel := htmlDoc.doc.Find(".ui.top.attached.header strong")
names := make([]string, 0, sel.Length())
sel.Each(func(_ int, s *goquery.Selection) {
names = append(names, s.Text())
})
return names
}

// Owner sees all teams, "Owners" sorted first regardless of alphabetical order
ownerSession := loginUser(t, "user15")
assert.Equal(t, []string{"Owners", "review_team", "test_team"}, listTeams(t, ownerSession, ""))

// Keyword filter narrows by name
assert.Equal(t, []string{"review_team"}, listTeams(t, ownerSession, "?q=review"))

// Non-admin org member sees only the teams they belong to
memberSession := loginUser(t, "user20")
assert.Equal(t, []string{"review_team"}, listTeams(t, memberSession, ""))

// Non-member is denied
nonMemberSession := loginUser(t, "user5")
req := NewRequestf(t, "GET", "/org/%s/teams", org.Name)
nonMemberSession.MakeRequest(t, req, http.StatusNotFound)

t.Run("Pagination", func(t *testing.T) {
defer test.MockVariableValue(&setting.UI.MembersPagingNum, 2)()
assert.Len(t, listTeams(t, ownerSession, "?page=1"), 2)
assert.Equal(t, []string{"test_team"}, listTeams(t, ownerSession, "?page=2"))
})
}

func testOrgSettings(t *testing.T) {
session := loginUser(t, "user2")

Expand Down
Loading