feat(orgs): Add search bar for organization members tab page (#37347)

Resolve #37072 

<img width="1312" height="186" alt="image"
src="https://github.com/user-attachments/assets/3ca9eddb-9230-4b0d-992f-5b19e475e267"
/>

---------

Signed-off-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: bircni <bircni@icloud.com>
This commit is contained in:
Lunny Xiao
2026-06-01 13:16:04 -07:00
committed by GitHub
parent 9155a81b9d
commit 689ace1ce2
4 changed files with 145 additions and 8 deletions
+41 -3
View File
@@ -19,6 +19,7 @@ import (
"gitea.dev/modules/util"
"xorm.io/builder"
"xorm.io/xorm"
)
// ErrOrgNotExist represents a "OrgNotExist" kind of error.
@@ -180,15 +181,47 @@ func (org *Organization) HomeLink() string {
// FindOrgMembersOpts represents find org members conditions
type FindOrgMembersOpts struct {
db.ListOptions
Doer *user_model.User
IsDoerMember bool
OrgID int64
Doer *user_model.User
IsDoerMember bool
OrgID int64
Keyword string
SearchByEmail bool
}
func (opts FindOrgMembersOpts) PublicOnly() bool {
return opts.Doer == nil || !(opts.IsDoerMember || opts.Doer.IsAdmin)
}
func (opts FindOrgMembersOpts) applyKeywordFilter(sess *xorm.Session) (*xorm.Session, bool) {
if opts.Keyword == "" {
return sess, false
}
lowerKeyword := strings.ToLower(opts.Keyword)
keywordCond := builder.Or(
builder.Like{"`user`.lower_name", lowerKeyword},
builder.Like{"LOWER(`user`.full_name)", lowerKeyword},
)
if opts.SearchByEmail {
var emailCond builder.Cond = builder.Like{"LOWER(`user`.email)", lowerKeyword}
switch {
case opts.Doer == nil:
emailCond = emailCond.And(builder.Eq{"`user`.keep_email_private": false})
case !opts.Doer.IsAdmin:
emailCond = emailCond.And(
builder.Or(
builder.Eq{"`user`.keep_email_private": false},
builder.Eq{"`user`.id": opts.Doer.ID},
),
)
}
keywordCond = keywordCond.Or(emailCond)
}
sess = sess.Join("INNER", "`user`", "org_user.uid = `user`.id").And(keywordCond)
return sess, true
}
// applyTeamMatesOnlyFilter make sure restricted users only see public team members and there own team mates
func (opts FindOrgMembersOpts) applyTeamMatesOnlyFilter(sess db.Session) {
if opts.Doer != nil && opts.IsDoerMember && opts.Doer.IsRestricted {
@@ -212,6 +245,7 @@ func CountOrgMembers(ctx context.Context, opts *FindOrgMembersOpts) (int64, erro
} else {
opts.applyTeamMatesOnlyFilter(sess)
}
sess, _ = opts.applyKeywordFilter(sess)
return sess.Count(new(OrgUser))
}
@@ -460,7 +494,11 @@ func GetOrgUsersByOrgID(ctx context.Context, opts *FindOrgMembersOpts) ([]*OrgUs
} else {
opts.applyTeamMatesOnlyFilter(sess)
}
if keywordSess, hasKeyword := opts.applyKeywordFilter(sess); hasKeyword {
sess = keywordSess.Select("org_user.*")
}
sess = sess.OrderBy("org_user.uid ASC")
if opts.ListOptions.PageSize > 0 {
db.SetSessionPagination(sess, opts)
+74
View File
@@ -288,6 +288,80 @@ func TestGetOrgUsersByOrgID(t *testing.T) {
assert.Empty(t, orgUsers)
}
func TestOrgMembersSearch(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
member := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
testCases := []struct {
name string
opts *organization.FindOrgMembersOpts
expectedUIDs []int64
}{
{
name: "match by username",
opts: &organization.FindOrgMembersOpts{
OrgID: 3,
Doer: member,
IsDoerMember: true,
Keyword: "user4",
SearchByEmail: true,
},
expectedUIDs: []int64{4},
},
{
name: "match by full name",
opts: &organization.FindOrgMembersOpts{
OrgID: 3,
Doer: member,
IsDoerMember: true,
Keyword: "user27",
SearchByEmail: true,
},
expectedUIDs: []int64{28},
},
{
name: "private email hidden",
opts: &organization.FindOrgMembersOpts{
OrgID: 3,
Doer: member,
IsDoerMember: true,
Keyword: "user2@example.com",
SearchByEmail: true,
},
expectedUIDs: []int64{},
},
{
name: "admin can search private email",
opts: &organization.FindOrgMembersOpts{
OrgID: 3,
Doer: admin,
Keyword: "user2@example.com",
SearchByEmail: true,
},
expectedUIDs: []int64{2},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
count, err := organization.CountOrgMembers(t.Context(), tc.opts)
assert.NoError(t, err)
assert.EqualValues(t, len(tc.expectedUIDs), count)
members, err := organization.GetOrgUsersByOrgID(t.Context(), tc.opts)
assert.NoError(t, err)
memberUIDs := make([]int64, 0, len(members))
for _, member := range members {
memberUIDs = append(memberUIDs, member.UID)
}
slices.Sort(memberUIDs)
assert.Equal(t, tc.expectedUIDs, memberUIDs)
})
}
}
func TestChangeOrgUserStatus(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())