Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion options/locale/locale_en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -2826,7 +2826,7 @@
"org.teams.manage_team_member_prompt": "Members are managed through teams. Add users to a team to invite them to this organization.",
"org.teams.update_settings": "Update Settings",
"org.teams.delete_team": "Delete Team",
"org.teams.add_team_member": "Add Team Member",
"org.teams.add_team_member": "Add team member",
"org.teams.invite_team_member": "Invite to %s",
"org.teams.invite_team_member.list": "Pending Invitations",
"org.teams.delete_team_title": "Delete Team",
Expand Down
6 changes: 4 additions & 2 deletions routers/web/org/home.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,10 @@ func home(ctx *context.Context, viewRepositories bool) {
ctx.ServerError("FindOrgMembers", err)
return
}
ctx.Data["Members"] = members
ctx.Data["Teams"] = ctx.Org.Teams

const orgOverviewTeamsLimit = 5
ctx.Data["OrgOverviewMembers"] = members
ctx.Data["OrgOverviewTeams"] = ctx.Org.Teams[:min(len(ctx.Org.Teams), orgOverviewTeamsLimit)]
Comment thread
wxiaoguang marked this conversation as resolved.
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0

Expand Down
48 changes: 45 additions & 3 deletions routers/web/org/teams.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
"code.gitea.io/gitea/services/context"
Expand Down Expand Up @@ -54,13 +55,54 @@ 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)
pagingNum := setting.UI.MembersPagingNum

searchTeams := func() (teams []*org_model.Team, count int64, err error) {
if keyword == "" {
// fast path, use existing teams in context if no need to filter from database
count = int64(len(ctx.Org.Teams))
start := (page - 1) * pagingNum
if start > len(ctx.Org.Teams) {
return nil, count, nil
}
end := min(start+pagingNum, len(ctx.Org.Teams))
return ctx.Org.Teams[start:end], count, nil
}

shouldSeeAllOrgTeams, err := context.UserShouldSeeAllOrgTeams(ctx)
if err != nil {
return nil, 0, err
}
opts := &org_model.SearchTeamOptions{
OrgID: org.ID,
UserID: util.Iif(shouldSeeAllOrgTeams, 0, ctx.Doer.ID),
Keyword: keyword,
IncludeDesc: true,
ListOptions: db.ListOptions{Page: page, PageSize: pagingNum},
}
return org_model.SearchTeam(ctx, opts)
}

teams, count, err := searchTeams()
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["OrgListTeams"] = 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 Expand Up @@ -213,7 +255,7 @@ func checkIsOrgMemberAndRedirect(ctx *context.Context, defaultRedirect string) {
if isOrgMember, err := org_model.IsOrganizationMember(ctx, ctx.Org.Organization.ID, ctx.Doer.ID); err != nil {
ctx.ServerError("IsOrganizationMember", err)
return
} else if !isOrgMember {
} else if !isOrgMember && !ctx.Doer.IsAdmin {
if ctx.Org.Organization.Visibility.IsPrivate() {
defaultRedirect = setting.AppSubURL + "/"
} else {
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
}
9 changes: 4 additions & 5 deletions templates/org/home.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,9 @@
<a class="tw-text-text-light flex-text-inline" href="{{.OrgLink}}/members"><span>{{.NumMembers}}</span> {{svg "octicon-chevron-right"}}</a>
</h4>
<div class="ui attached segment members">
{{$isMember := .IsOrganizationMember}}
{{range .Members}}
{{if or $isMember (call $.IsPublicMember .ID)}}
<a href="{{.HomeLink}}" title="{{.Name}}{{if .FullName}} ({{.FullName}}){{end}}">{{ctx.AvatarUtils.Avatar . 48}}</a>
{{range $memberUser := .OrgOverviewMembers}}
{{if or $.IsOrganizationMember (call $.IsPublicMember $memberUser.ID)}}
{{template "shared/user/avatarlink" dict "user" $memberUser "size" 32 "tooltip" true}}
{{end}}
{{end}}
</div>
Expand All @@ -74,7 +73,7 @@
<a class="tw-text-text-light flex-text-inline" href="{{.OrgLink}}/teams"><span>{{.Org.NumTeams}}</span> {{svg "octicon-chevron-right"}}</a>
</div>
<div class="ui attached table segment teams">
{{range .Teams}}
{{range .OrgOverviewTeams}}
<div class="item">
<a href="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}"><strong class="team-name">{{.Name}}</strong></a>
<p class="tw-text-text-light">
Expand Down
2 changes: 1 addition & 1 deletion templates/org/team/new.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
</div>
<div class="field {{if .Err_Description}}error{{end}}">
<label for="description">{{ctx.Locale.Tr "org.team_desc"}}</label>
<input id="description" name="description" value="{{.Team.Description}}">
<input id="description" name="description" value="{{.Team.Description}}" maxlength="255">
<span class="help">{{ctx.Locale.Tr "org.team_desc_helper"}}</span>
</div>
{{if not (eq .Team.LowerName "owners")}}
Expand Down
25 changes: 12 additions & 13 deletions templates/org/team/sidebar.tmpl
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
<div class="ui six wide column">
<h4 class="ui top attached header">
<h4 class="ui top attached header flex-left-right">
<strong>{{.Team.Name}}</strong>
<div class="ui right">
<div class="flex-center-wrap">
{{if .Team.IsMember ctx $.SignedUser.ID}}
<form>
<button class="ui red tiny button delete-button" data-modal-id="leave-team-sidebar"
data-url="{{.OrgLink}}/teams/{{.Team.LowerName | PathEscape}}/action/leave" data-datauid="{{$.SignedUser.ID}}"
data-name="{{.Team.Name}}">{{ctx.Locale.Tr "org.teams.leave"}}</button>
</form>
<button class="ui red mini compact button show-modal" data-modal="#org-member-leave-team"
data-modal-form.action="{{$.OrgLink}}/teams/{{$.Team.LowerName | PathEscape}}/action/leave?uid={{$.SignedUser.ID}}"
data-modal-to-leave-team-name="{{$.Team.Name}}"
>{{ctx.Locale.Tr "org.teams.leave"}}</button>
{{else if .IsOrganizationOwner}}
<form method="post" action="{{.OrgLink}}/teams/{{.Team.LowerName | PathEscape}}/action/join">
<input type="hidden" name="page" value="team">
<button type="submit" class="ui primary tiny button" name="uid" value="{{$.SignedUser.ID}}">{{ctx.Locale.Tr "org.teams.join"}}</button>
<button type="submit" class="ui primary mini compact button" name="uid" value="{{$.SignedUser.ID}}">{{ctx.Locale.Tr "org.teams.join"}}</button>
</form>
{{end}}
</div>
Expand Down Expand Up @@ -85,12 +84,12 @@
</div>
{{end}}
</div>
<div class="ui g-modal-confirm delete modal" id="leave-team-sidebar">
<div class="ui mini modal" id="org-member-leave-team">
<div class="header">
{{ctx.Locale.Tr "org.teams.leave"}}
</div>
<div class="content">
<p>{{ctx.Locale.Tr "org.teams.leave.detail" (HTMLFormat `<span class="%s"></span>` "name")}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
<form class="content ui form form-fetch-action" method="post">
<p>{{ctx.Locale.Tr "org.teams.leave.detail" (HTMLFormat `<span class="%s"></span>` "to-leave-team-name")}}</p>
{{template "base/modal_actions_confirm" .}}
</form>
</div>
64 changes: 37 additions & 27 deletions templates/org/team/teams.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -8,49 +8,59 @@
<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}}

<form class="ui form ignore-dirty tw-my-4" method="get" action="{{$.Link}}">
<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>

<div class="ui two column stackable grid">
{{range .Teams}}
<div class="column">
<div class="ui top attached header">
<a class="tw-text-text" href="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}"><strong>{{.Name}}</strong></a>
<div class="ui right">
<a class="ui primary tiny button" href="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}">{{ctx.Locale.Tr "view"}}</a>
{{range $team := $.OrgListTeams}}
<div class="column team-item-box">
<div class="ui top attached header muted-links flex-left-right team-item-header">
<a href="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}"><strong>{{.Name}}</strong></a>
<div class="flex-center-wrap">
<a href="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}">{{.NumMembers}} {{ctx.Locale.Tr "org.lower_members"}}</a>
·
<a href="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}/repositories">{{.NumRepos}} {{ctx.Locale.Tr "org.lower_repositories"}}</a>
{{if .IsMember ctx $.SignedUser.ID}}
<form>
<button class="ui red tiny button delete-button" data-modal-id="leave-team"
data-url="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}/action/leave" data-datauid="{{$.SignedUser.ID}}"
data-name="{{.Name}}">{{ctx.Locale.Tr "org.teams.leave"}}</button>
</form>
{{else if $.IsOrganizationOwner}}
<form method="post" action="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}/action/join">
<button type="submit" class="ui primary tiny button" name="uid" value="{{$.SignedUser.ID}}">{{ctx.Locale.Tr "org.teams.join"}}</button>
</form>
<button class="ui red mini compact button show-modal" data-modal="#org-member-leave-team"
data-modal-form.action="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}/action/leave?uid={{$.SignedUser.ID}}"
data-modal-to-leave-team-name="{{.Name}}"
>{{ctx.Locale.Tr "org.teams.leave"}}</button>
Comment thread
wxiaoguang marked this conversation as resolved.
{{end}}
</div>
</div>
<div class="ui attached segment members">
{{range .Members}}
{{template "shared/user/avatarlink" dict "user" .}}
{{end}}
{{if $team.Description}}
<div class="ui attached header team-item-description">
{{if $team.Description}}{{$team.Description}}{{end}}
</div>
<div class="ui bottom attached header">
<p class="team-meta"><a class="muted" href="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}">{{.NumMembers}} {{ctx.Locale.Tr "org.lower_members"}}</a> · <a class="muted" href="{{$.OrgLink}}/teams/{{.LowerName | PathEscape}}/repositories">{{.NumRepos}} {{ctx.Locale.Tr "org.lower_repositories"}}</a></p>
{{end}}
<div class="ui attached segment">
<div class="flex-center-wrap">
{{range .Members}}
{{template "shared/user/avatarlink" dict "user" . "size" 32 "tooltip" true}}
{{else}}
<a class="flex-text-inline tw-h-[32px]" href="{{$.OrgLink}}/teams/{{$team.LowerName | PathEscape}}">{{ctx.Locale.Tr "org.teams.add_team_member"}}</a>
{{end}}
</div>
</div>
</div>
{{end}}
</div>
{{template "base/paginate" .}}
</div>
</div>
<div class="ui g-modal-confirm delete modal" id="leave-team">
<div class="ui mini modal" id="org-member-leave-team">
<div class="header">
{{ctx.Locale.Tr "org.teams.leave"}}
</div>
<div class="content">
<p>{{ctx.Locale.Tr "org.teams.leave.detail" (HTMLFormat `<span class="%s"></span>` "name")}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
<form class="content ui form form-fetch-action" method="post">
<p>{{ctx.Locale.Tr "org.teams.leave.detail" (HTMLFormat `<span class="%s"></span>` "to-leave-team-name")}}</p>
{{template "base/modal_actions_confirm" .}}
</form>
</div>
{{template "base/footer" .}}
2 changes: 1 addition & 1 deletion templates/shared/user/avatarlink.tmpl
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<a class="avatar-with-link" {{if gt .user.ID 0}}href="{{.user.HomeLink}}"{{end}}>{{ctx.AvatarUtils.Avatar .user (or .size 20)}}</a>
<a class="avatar-with-link" {{if .tooltip}}data-tooltip-content="{{.user.Name}}{{if .user.FullName}} ({{.user.FullName}}){{end}}"{{end}} {{if gt .user.ID 0}}href="{{.user.HomeLink}}"{{end}}>{{ctx.AvatarUtils.Avatar .user (or .size 20)}}</a>
Loading