mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-07 16:09:06 +00:00
Merge branch 'main' into renovate/npm-dependencies
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -31,10 +31,14 @@ func Members(ctx *context.Context) {
|
||||
ctx.Data["PageIsOrgMembers"] = true
|
||||
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
keyword := ctx.FormTrim("q")
|
||||
ctx.Data["Keyword"] = keyword
|
||||
|
||||
opts := &organization.FindOrgMembersOpts{
|
||||
Doer: ctx.Doer,
|
||||
OrgID: org.ID,
|
||||
Doer: ctx.Doer,
|
||||
OrgID: org.ID,
|
||||
Keyword: keyword,
|
||||
SearchByEmail: true,
|
||||
}
|
||||
|
||||
if ctx.Doer != nil {
|
||||
@@ -58,9 +62,11 @@ func Members(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
pager := context.NewPagination(total, setting.UI.MembersPagingNum, page, 5)
|
||||
opts.ListOptions.Page = page
|
||||
opts.ListOptions.PageSize = setting.UI.MembersPagingNum
|
||||
pageSize := setting.UI.MembersPagingNum
|
||||
pager := context.NewPagination(total, pageSize, page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
opts.ListOptions.Page = pager.Paginater.Current()
|
||||
opts.ListOptions.PageSize = pageSize
|
||||
members, membersIsPublic, err := organization.FindOrgMembers(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetMembers", err)
|
||||
@@ -68,6 +74,8 @@ func Members(ctx *context.Context) {
|
||||
}
|
||||
ctx.Data["Page"] = pager
|
||||
ctx.Data["Members"] = members
|
||||
ctx.Data["MembersShown"] = len(members)
|
||||
ctx.Data["MembersTotal"] = total
|
||||
ctx.Data["MembersIsPublicMember"] = membersIsPublic
|
||||
ctx.Data["MembersIsUserOrgOwner"] = organization.IsUserOrgOwner(ctx, members, org.ID)
|
||||
ctx.Data["MembersTwoFaStatus"] = members.GetTwoFaStatus(ctx)
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
{{end}}
|
||||
<div class="ui small secondary filter menu">
|
||||
<form id="org-member-search-form" class="ui form ignore-dirty tw-flex-1 tw-flex tw-items-center tw-gap-x-2">
|
||||
<div class="ui small fluid action input tw-flex-1">
|
||||
{{template "shared/search/input" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.user_kind")}}
|
||||
{{template "shared/search/button"}}
|
||||
</div>
|
||||
</form>
|
||||
<div class="item tw-text-text-light">
|
||||
{{ctx.Locale.Tr "org.members"}}: {{CountFmt .MembersShown}} / {{CountFmt .MembersTotal}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-divided-list items-with-main">
|
||||
{{range .Members}}
|
||||
{{$isPublic := index $.MembersIsPublicMember .ID}}
|
||||
@@ -67,6 +78,12 @@
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="flex-item">
|
||||
<div class="flex-item-main">
|
||||
{{ctx.Locale.Tr "search.no_results"}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{template "base/paginate" .}}
|
||||
|
||||
@@ -171,14 +171,3 @@
|
||||
|
||||
{{template "repo/issue/view_content/reference_issue_dialog" .}}
|
||||
{{template "shared/user/block_user_dialog" .}}
|
||||
|
||||
<div class="ui g-modal-confirm delete modal">
|
||||
<div class="header">
|
||||
{{svg "octicon-trash"}}
|
||||
{{ctx.Locale.Tr "repo.branch.delete" .HeadTarget}}
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>{{ctx.Locale.Tr "repo.branch.delete_desc"}}</p>
|
||||
</div>
|
||||
{{template "base/modal_actions_confirm" .}}
|
||||
</div>
|
||||
|
||||
@@ -382,16 +382,6 @@ a.label,
|
||||
color: var(--color-text-light-2);
|
||||
}
|
||||
|
||||
/* styles from removed fomantic transition module */
|
||||
.hidden.transition {
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
}
|
||||
.visible.transition {
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.ui.comments .comment .metadata {
|
||||
color: var(--color-text-light-2);
|
||||
}
|
||||
@@ -438,11 +428,6 @@ img.ui.avatar,
|
||||
margin-top: calc(var(--page-spacing) - 1rem);
|
||||
}
|
||||
|
||||
.ui.message.flash-message pre {
|
||||
white-space: pre-line;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ui .header > i + .content {
|
||||
padding-left: 0.75rem;
|
||||
vertical-align: middle;
|
||||
@@ -486,10 +471,6 @@ img.ui.avatar,
|
||||
color: var(--color-text) !important;
|
||||
}
|
||||
|
||||
.ui .border {
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.user-menu > .item {
|
||||
width: 100%;
|
||||
border-radius: 0 !important;
|
||||
|
||||
@@ -22,8 +22,9 @@
|
||||
@import "./modules/tab.css";
|
||||
@import "./modules/form.css";
|
||||
@import "./modules/dropdown.css";
|
||||
@import "./modules/shortcut.css";
|
||||
@import "./modules/transition.css";
|
||||
|
||||
@import "./modules/shortcut.css";
|
||||
@import "./modules/tippy.css";
|
||||
@import "./modules/breadcrumb.css";
|
||||
@import "./modules/comment.css";
|
||||
|
||||
@@ -101,13 +101,6 @@ code.language-math.is-loading::after {
|
||||
animation: pulse-1p5 200ms linear;
|
||||
}
|
||||
|
||||
.ui.modal,
|
||||
.ui.dimmer.transition {
|
||||
animation-name: fadein;
|
||||
animation-duration: 100ms;
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
.rotate-clockwise {
|
||||
animation: rotate-clockwise-keyframes 1s linear infinite;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,11 @@ details.ui.message:not(:has(pre)) summary {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.ui.message.flash-message pre {
|
||||
white-space: pre-line;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ui.message:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
24
web_src/css/modules/transition.css
Normal file
24
web_src/css/modules/transition.css
Normal file
@@ -0,0 +1,24 @@
|
||||
/* styles for Fomantic transition (toggle): dimmer, modal, dropdown menu */
|
||||
|
||||
/* this is the only place using "hidden" and "visible" classes, it can safely work with tailwind without prefix in the future */
|
||||
.ui.modal.transition.hidden,
|
||||
.ui.dropdown > .menu.transition.hidden {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.ui.modal.transition.visible,
|
||||
.ui.dropdown > .menu.transition.visible {
|
||||
display: block;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* dimmer uses "active" but not "hidden/visible" classes, no special classes for it here */
|
||||
|
||||
/* only modal and dimmer need animation, dropdown menu doesn't */
|
||||
.ui.modal.transition,
|
||||
.ui.dimmer.transition {
|
||||
animation-name: fadein;
|
||||
animation-duration: 100ms;
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
Reference in New Issue
Block a user