Add member, collaborator, contributor, and first-time contributor roles and tooltips (#26658)

GitHub like role descriptor

![image](https://github.com/go-gitea/gitea/assets/18380374/ceaed92c-6749-47b3-89e8-0e0e7ae65321)

![image](https://github.com/go-gitea/gitea/assets/18380374/8193ec34-cbf0-47f9-b0de-10dbddd66970)

![image](https://github.com/go-gitea/gitea/assets/18380374/56c7ed85-6177-425e-9f2f-926e99770782)

---------

Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
yp05327 2023-08-24 14:06:17 +09:00 committed by GitHub
parent 0d55f64e6c
commit d2e4039def
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 106 additions and 73 deletions

View file

@ -23,6 +23,7 @@ import (
"code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/references"
"code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"xorm.io/builder" "xorm.io/builder"
@ -181,40 +182,32 @@ func (t CommentType) HasAttachmentSupport() bool {
return false return false
} }
// RoleDescriptor defines comment tag type // RoleInRepo presents the user's participation in the repo
type RoleDescriptor int type RoleInRepo string
// RoleDescriptor defines comment "role" tags
type RoleDescriptor struct {
IsPoster bool
RoleInRepo RoleInRepo
}
// Enumerate all the role tags. // Enumerate all the role tags.
const ( const (
RoleDescriptorNone RoleDescriptor = iota RoleRepoOwner RoleInRepo = "owner"
RoleDescriptorPoster RoleRepoMember RoleInRepo = "member"
RoleDescriptorWriter RoleRepoCollaborator RoleInRepo = "collaborator"
RoleDescriptorOwner RoleRepoFirstTimeContributor RoleInRepo = "first_time_contributor"
RoleRepoContributor RoleInRepo = "contributor"
) )
// WithRole enable a specific tag on the RoleDescriptor. // LocaleString returns the locale string name of the role
func (rd RoleDescriptor) WithRole(role RoleDescriptor) RoleDescriptor { func (r RoleInRepo) LocaleString(lang translation.Locale) string {
return rd | (1 << role) return lang.Tr("repo.issues.role." + string(r))
} }
func stringToRoleDescriptor(role string) RoleDescriptor { // LocaleHelper returns the locale tooltip of the role
switch role { func (r RoleInRepo) LocaleHelper(lang translation.Locale) string {
case "Poster": return lang.Tr("repo.issues.role." + string(r) + "_helper")
return RoleDescriptorPoster
case "Writer":
return RoleDescriptorWriter
case "Owner":
return RoleDescriptorOwner
default:
return RoleDescriptorNone
}
}
// HasRole returns if a certain role is enabled on the RoleDescriptor.
func (rd RoleDescriptor) HasRole(role string) bool {
roleDescriptor := stringToRoleDescriptor(role)
bitValue := rd & (1 << roleDescriptor)
return (bitValue > 0)
} }
// Comment represents a comment in commit and issue page. // Comment represents a comment in commit and issue page.

View file

@ -199,3 +199,16 @@ func (prs PullRequestList) GetIssueIDs() []int64 {
} }
return issueIDs return issueIDs
} }
// HasMergedPullRequestInRepo returns whether the user(poster) has merged pull-request in the repo
func HasMergedPullRequestInRepo(ctx context.Context, repoID, posterID int64) (bool, error) {
return db.GetEngine(ctx).
Join("INNER", "pull_request", "pull_request.issue_id = issue.id").
Where("repo_id=?", repoID).
And("poster_id=?", posterID).
And("is_pull=?", true).
And("pull_request.has_merged=?", true).
Select("issue.id").
Limit(1).
Get(new(Issue))
}

View file

@ -1480,9 +1480,18 @@ issues.ref_reopening_from = `<a href="%[3]s">referenced a pull request %[4]s tha
issues.ref_closed_from = `<a href="%[3]s">closed this issue %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.ref_closed_from = `<a href="%[3]s">closed this issue %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_reopened_from = `<a href="%[3]s">reopened this issue %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.ref_reopened_from = `<a href="%[3]s">reopened this issue %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
issues.ref_from = `from %[1]s` issues.ref_from = `from %[1]s`
issues.poster = Poster issues.author = Author
issues.collaborator = Collaborator issues.author_helper = This user is the author.
issues.owner = Owner issues.role.owner = Owner
issues.role.owner_helper = This user is the owner of this repository.
issues.role.member = Member
issues.role.member_helper = This user is a member of the organization owning this repository.
issues.role.collaborator = Collaborator
issues.role.collaborator_helper = This user has been invited to collaborate on the repository.
issues.role.first_time_contributor = First-time contributor
issues.role.first_time_contributor_helper = This is the first contribution of this user to the repository.
issues.role.contributor = Contributor
issues.role.contributor_helper = This user has previously committed to the repository.
issues.re_request_review=Re-request review issues.re_request_review=Re-request review
issues.is_stale = There have been changes to this PR since this review issues.is_stale = There have been changes to this PR since this review
issues.remove_request_review=Remove review request issues.remove_request_review=Remove review request

View file

@ -1228,47 +1228,70 @@ func NewIssuePost(ctx *context.Context) {
} }
} }
// roleDescriptor returns the Role Descriptor for a comment in/with the given repo, poster and issue // roleDescriptor returns the role descriptor for a comment in/with the given repo, poster and issue
func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *user_model.User, issue *issues_model.Issue, hasOriginalAuthor bool) (issues_model.RoleDescriptor, error) { func roleDescriptor(ctx stdCtx.Context, repo *repo_model.Repository, poster *user_model.User, issue *issues_model.Issue, hasOriginalAuthor bool) (issues_model.RoleDescriptor, error) {
roleDescriptor := issues_model.RoleDescriptor{}
if hasOriginalAuthor { if hasOriginalAuthor {
return issues_model.RoleDescriptorNone, nil return roleDescriptor, nil
} }
perm, err := access_model.GetUserRepoPermission(ctx, repo, poster) perm, err := access_model.GetUserRepoPermission(ctx, repo, poster)
if err != nil { if err != nil {
return issues_model.RoleDescriptorNone, err return roleDescriptor, err
}
// By default the poster has no roles on the comment.
roleDescriptor := issues_model.RoleDescriptorNone
// Check if the poster is owner of the repo.
if perm.IsOwner() {
// If the poster isn't a admin, enable the owner role.
if !poster.IsAdmin {
roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorOwner)
} else {
// Otherwise check if poster is the real repo admin.
ok, err := access_model.IsUserRealRepoAdmin(repo, poster)
if err != nil {
return issues_model.RoleDescriptorNone, err
}
if ok {
roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorOwner)
}
}
}
// Is the poster can write issues or pulls to the repo, enable the Writer role.
// Only enable this if the poster doesn't have the owner role already.
if !roleDescriptor.HasRole("Owner") && perm.CanWriteIssuesOrPulls(issue.IsPull) {
roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorWriter)
} }
// If the poster is the actual poster of the issue, enable Poster role. // If the poster is the actual poster of the issue, enable Poster role.
if issue.IsPoster(poster.ID) { roleDescriptor.IsPoster = issue.IsPoster(poster.ID)
roleDescriptor = roleDescriptor.WithRole(issues_model.RoleDescriptorPoster)
// Check if the poster is owner of the repo.
if perm.IsOwner() {
// If the poster isn't an admin, enable the owner role.
if !poster.IsAdmin {
roleDescriptor.RoleInRepo = issues_model.RoleRepoOwner
return roleDescriptor, nil
}
// Otherwise check if poster is the real repo admin.
ok, err := access_model.IsUserRealRepoAdmin(repo, poster)
if err != nil {
return roleDescriptor, err
}
if ok {
roleDescriptor.RoleInRepo = issues_model.RoleRepoOwner
return roleDescriptor, nil
}
}
// If repo is organization, check Member role
if err := repo.LoadOwner(ctx); err != nil {
return roleDescriptor, err
}
if repo.Owner.IsOrganization() {
if isMember, err := organization.IsOrganizationMember(ctx, repo.Owner.ID, poster.ID); err != nil {
return roleDescriptor, err
} else if isMember {
roleDescriptor.RoleInRepo = issues_model.RoleRepoMember
return roleDescriptor, nil
}
}
// If the poster is the collaborator of the repo
if isCollaborator, err := repo_model.IsCollaborator(ctx, repo.ID, poster.ID); err != nil {
return roleDescriptor, err
} else if isCollaborator {
roleDescriptor.RoleInRepo = issues_model.RoleRepoCollaborator
return roleDescriptor, nil
}
hasMergedPR, err := issues_model.HasMergedPullRequestInRepo(ctx, repo.ID, poster.ID)
if err != nil {
return roleDescriptor, err
} else if hasMergedPR {
roleDescriptor.RoleInRepo = issues_model.RoleRepoContributor
} else {
// only display first time contributor in the first opening pull request
roleDescriptor.RoleInRepo = issues_model.RoleRepoFirstTimeContributor
} }
return roleDescriptor, nil return roleDescriptor, nil

View file

@ -1,15 +1,10 @@
{{if and (.ShowRole.HasRole "Poster") (not .IgnorePoster)}} {{if and .ShowRole.IsPoster (not .IgnorePoster)}}
<div class="ui basic label role-label"> <div class="ui basic label role-label" data-tooltip-content="{{ctx.Locale.Tr "repo.issues.author_helper"}}">
{{ctx.Locale.Tr "repo.issues.poster"}} {{ctx.Locale.Tr "repo.issues.author"}}
</div> </div>
{{end}} {{end}}
{{if (.ShowRole.HasRole "Writer")}} {{if .ShowRole.RoleInRepo}}
<div class="ui basic label role-label"> <div class="ui basic label role-label" data-tooltip-content="{{.ShowRole.RoleInRepo.LocaleHelper ctx.Locale}}">
{{ctx.Locale.Tr "repo.issues.collaborator"}} {{.ShowRole.RoleInRepo.LocaleString ctx.Locale}}
</div>
{{end}}
{{if (.ShowRole.HasRole "Owner")}}
<div class="ui basic label role-label">
{{ctx.Locale.Tr "repo.issues.owner"}}
</div> </div>
{{end}} {{end}}