forked from NYANDEV/forgejo
Add mentionable teams to tributeValues and change team mention rules to gh's style (#13198)
* Add mentionable teams to tributeValues Signed-off-by: a1012112796 <1012112796@qq.com> * Apply suggestions from code review Co-authored-by: silverwind <me@silverwind.io> * Change team mention rules to gh's style * use org's avator as team avator in ui Signed-off-by: a1012112796 <1012112796@qq.com> * Update modules/markup/html.go * Update models/issue.go Co-authored-by: Lauris BH <lauris@nix.lv> * Update models/issue.go * fix a small nit and update test code Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Lauris BH <lauris@nix.lv> Co-authored-by: 6543 <6543@obermui.de>
This commit is contained in:
parent
1b1adab26c
commit
34df4e5df5
8 changed files with 114 additions and 36 deletions
|
@ -1847,30 +1847,43 @@ func (issue *Issue) ResolveMentionsByVisibility(ctx DBContext, doer *User, menti
|
||||||
if err = issue.loadRepo(ctx.e); err != nil {
|
if err = issue.loadRepo(ctx.e); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resolved := make(map[string]bool, 20)
|
|
||||||
names := make([]string, 0, 20)
|
resolved := make(map[string]bool, 10)
|
||||||
|
var mentionTeams []string
|
||||||
|
|
||||||
|
if err := issue.Repo.getOwner(ctx.e); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
repoOwnerIsOrg := issue.Repo.Owner.IsOrganization()
|
||||||
|
if repoOwnerIsOrg {
|
||||||
|
mentionTeams = make([]string, 0, 5)
|
||||||
|
}
|
||||||
|
|
||||||
resolved[doer.LowerName] = true
|
resolved[doer.LowerName] = true
|
||||||
for _, name := range mentions {
|
for _, name := range mentions {
|
||||||
name := strings.ToLower(name)
|
name := strings.ToLower(name)
|
||||||
if _, ok := resolved[name]; ok {
|
if _, ok := resolved[name]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
resolved[name] = false
|
if repoOwnerIsOrg && strings.Contains(name, "/") {
|
||||||
names = append(names, name)
|
names := strings.Split(name, "/")
|
||||||
|
if len(names) < 2 || names[0] != issue.Repo.Owner.LowerName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mentionTeams = append(mentionTeams, names[1])
|
||||||
|
resolved[name] = true
|
||||||
|
} else {
|
||||||
|
resolved[name] = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := issue.Repo.getOwner(ctx.e); err != nil {
|
if issue.Repo.Owner.IsOrganization() && len(mentionTeams) > 0 {
|
||||||
return nil, err
|
teams := make([]*Team, 0, len(mentionTeams))
|
||||||
}
|
|
||||||
|
|
||||||
if issue.Repo.Owner.IsOrganization() {
|
|
||||||
// Since there can be users with names that match the name of a team,
|
|
||||||
// if the team exists and can read the issue, the team takes precedence.
|
|
||||||
teams := make([]*Team, 0, len(names))
|
|
||||||
if err := ctx.e.
|
if err := ctx.e.
|
||||||
Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
Join("INNER", "team_repo", "team_repo.team_id = team.id").
|
||||||
Where("team_repo.repo_id=?", issue.Repo.ID).
|
Where("team_repo.repo_id=?", issue.Repo.ID).
|
||||||
In("team.lower_name", names).
|
In("team.lower_name", mentionTeams).
|
||||||
Find(&teams); err != nil {
|
Find(&teams); err != nil {
|
||||||
return nil, fmt.Errorf("find mentioned teams: %v", err)
|
return nil, fmt.Errorf("find mentioned teams: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1883,7 +1896,7 @@ func (issue *Issue) ResolveMentionsByVisibility(ctx DBContext, doer *User, menti
|
||||||
for _, team := range teams {
|
for _, team := range teams {
|
||||||
if team.Authorize >= AccessModeOwner {
|
if team.Authorize >= AccessModeOwner {
|
||||||
checked = append(checked, team.ID)
|
checked = append(checked, team.ID)
|
||||||
resolved[team.LowerName] = true
|
resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
has, err := ctx.e.Get(&TeamUnit{OrgID: issue.Repo.Owner.ID, TeamID: team.ID, Type: unittype})
|
has, err := ctx.e.Get(&TeamUnit{OrgID: issue.Repo.Owner.ID, TeamID: team.ID, Type: unittype})
|
||||||
|
@ -1892,7 +1905,7 @@ func (issue *Issue) ResolveMentionsByVisibility(ctx DBContext, doer *User, menti
|
||||||
}
|
}
|
||||||
if has {
|
if has {
|
||||||
checked = append(checked, team.ID)
|
checked = append(checked, team.ID)
|
||||||
resolved[team.LowerName] = true
|
resolved[issue.Repo.Owner.LowerName+"/"+team.LowerName] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(checked) != 0 {
|
if len(checked) != 0 {
|
||||||
|
@ -1916,24 +1929,28 @@ func (issue *Issue) ResolveMentionsByVisibility(ctx DBContext, doer *User, menti
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove names already in the list to avoid querying the database if pending names remain
|
|
||||||
names = make([]string, 0, len(resolved))
|
|
||||||
for name, already := range resolved {
|
|
||||||
if !already {
|
|
||||||
names = append(names, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(names) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unchecked := make([]*User, 0, len(names))
|
// Remove names already in the list to avoid querying the database if pending names remain
|
||||||
|
mentionUsers := make([]string, 0, len(resolved))
|
||||||
|
for name, already := range resolved {
|
||||||
|
if !already {
|
||||||
|
mentionUsers = append(mentionUsers, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(mentionUsers) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if users == nil {
|
||||||
|
users = make([]*User, 0, len(mentionUsers))
|
||||||
|
}
|
||||||
|
|
||||||
|
unchecked := make([]*User, 0, len(mentionUsers))
|
||||||
if err := ctx.e.
|
if err := ctx.e.
|
||||||
Where("`user`.is_active = ?", true).
|
Where("`user`.is_active = ?", true).
|
||||||
And("`user`.prohibit_login = ?", false).
|
And("`user`.prohibit_login = ?", false).
|
||||||
In("`user`.lower_name", names).
|
In("`user`.lower_name", mentionUsers).
|
||||||
Find(&unchecked); err != nil {
|
Find(&unchecked); err != nil {
|
||||||
return nil, fmt.Errorf("find mentioned users: %v", err)
|
return nil, fmt.Errorf("find mentioned users: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -400,5 +400,5 @@ func TestIssue_ResolveMentions(t *testing.T) {
|
||||||
// Private repo, not a team member
|
// Private repo, not a team member
|
||||||
testSuccess("user17", "big_test_private_4", "user20", []string{"user5"}, []int64{})
|
testSuccess("user17", "big_test_private_4", "user20", []string{"user5"}, []int64{})
|
||||||
// Private repo, whole team
|
// Private repo, whole team
|
||||||
testSuccess("user17", "big_test_private_4", "user15", []string{"owners"}, []int64{18})
|
testSuccess("user17", "big_test_private_4", "user15", []string{"user17/owners"}, []int64{18})
|
||||||
}
|
}
|
||||||
|
|
|
@ -596,11 +596,15 @@ func mentionProcessor(ctx *postProcessCtx, node *html.Node) {
|
||||||
mention := node.Data[loc.Start:loc.End]
|
mention := node.Data[loc.Start:loc.End]
|
||||||
var teams string
|
var teams string
|
||||||
teams, ok := ctx.metas["teams"]
|
teams, ok := ctx.metas["teams"]
|
||||||
if ok && strings.Contains(teams, ","+strings.ToLower(mention[1:])+",") {
|
// team mention should follow @orgName/teamName style
|
||||||
replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, "org", ctx.metas["org"], "teams", mention[1:]), mention, "mention"))
|
if ok && strings.Contains(mention, "/") {
|
||||||
} else {
|
mentionOrgAndTeam := strings.Split(mention, "/")
|
||||||
replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mention[1:]), mention, "mention"))
|
if mentionOrgAndTeam[0][1:] == ctx.metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") {
|
||||||
|
replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, "org", ctx.metas["org"], "teams", mentionOrgAndTeam[1]), mention, "mention"))
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mention[1:]), mention, "mention"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func shortLinkProcessor(ctx *postProcessCtx, node *html.Node) {
|
func shortLinkProcessor(ctx *postProcessCtx, node *html.Node) {
|
||||||
|
|
|
@ -26,8 +26,8 @@ var (
|
||||||
// While fast, this is also incorrect and lead to false positives.
|
// While fast, this is also incorrect and lead to false positives.
|
||||||
// TODO: fix invalid linking issue
|
// TODO: fix invalid linking issue
|
||||||
|
|
||||||
// mentionPattern matches all mentions in the form of "@user"
|
// mentionPattern matches all mentions in the form of "@user" or "@org/team"
|
||||||
mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_][0-9a-zA-Z-_.]+[0-9a-zA-Z-_])(?:\s|[:,;.?!]\s|[:,;.?!]?$|\)|\])`)
|
mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_]+\/?[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_][0-9a-zA-Z-_.]+\/?[0-9a-zA-Z-_.]+[0-9a-zA-Z-_])(?:\s|[:,;.?!]\s|[:,;.?!]?$|\)|\])`)
|
||||||
// issueNumericPattern matches string that references to a numeric issue, e.g. #1287
|
// issueNumericPattern matches string that references to a numeric issue, e.g. #1287
|
||||||
issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
|
issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
|
||||||
// issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234
|
// issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234
|
||||||
|
|
|
@ -325,6 +325,7 @@ func TestRegExp_mentionPattern(t *testing.T) {
|
||||||
{"@gitea.", "@gitea"},
|
{"@gitea.", "@gitea"},
|
||||||
{"@gitea,", "@gitea"},
|
{"@gitea,", "@gitea"},
|
||||||
{"@gitea;", "@gitea"},
|
{"@gitea;", "@gitea"},
|
||||||
|
{"@gitea/team1;", "@gitea/team1"},
|
||||||
}
|
}
|
||||||
falseTestCases := []string{
|
falseTestCases := []string{
|
||||||
"@ 0",
|
"@ 0",
|
||||||
|
@ -340,6 +341,7 @@ func TestRegExp_mentionPattern(t *testing.T) {
|
||||||
"@gitea?this",
|
"@gitea?this",
|
||||||
"@gitea,this",
|
"@gitea,this",
|
||||||
"@gitea;this",
|
"@gitea;this",
|
||||||
|
"@gitea/team1/more",
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testCase := range trueTestCases {
|
for _, testCase := range trueTestCases {
|
||||||
|
|
|
@ -274,6 +274,11 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleTeamMentions(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
labels, err := models.GetLabelsByRepoID(repo.ID, "", models.ListOptions{})
|
labels, err := models.GetLabelsByRepoID(repo.ID, "", models.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetLabelsByRepoID", err)
|
ctx.ServerError("GetLabelsByRepoID", err)
|
||||||
|
@ -410,6 +415,11 @@ func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *models.Repos
|
||||||
ctx.ServerError("GetAssignees", err)
|
ctx.ServerError("GetAssignees", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleTeamMentions(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func retrieveProjects(ctx *context.Context, repo *models.Repository) {
|
func retrieveProjects(ctx *context.Context, repo *models.Repository) {
|
||||||
|
@ -2445,3 +2455,40 @@ func combineLabelComments(issue *models.Issue) {
|
||||||
i--
|
i--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get all teams that current user can mention
|
||||||
|
func handleTeamMentions(ctx *context.Context) {
|
||||||
|
if ctx.User == nil || !ctx.Repo.Owner.IsOrganization() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdmin := false
|
||||||
|
var err error
|
||||||
|
// Admin has super access.
|
||||||
|
if ctx.User.IsAdmin {
|
||||||
|
isAdmin = true
|
||||||
|
} else {
|
||||||
|
isAdmin, err = ctx.Repo.Owner.IsOwnedBy(ctx.User.ID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("IsOwnedBy", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isAdmin {
|
||||||
|
if err := ctx.Repo.Owner.GetTeams(&models.SearchTeamOptions{}); err != nil {
|
||||||
|
ctx.ServerError("GetTeams", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.Repo.Owner.Teams, err = ctx.Repo.Owner.GetUserTeams(ctx.User.ID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetUserTeams", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["MentionableTeams"] = ctx.Repo.Owner.Teams
|
||||||
|
ctx.Data["MentionableTeamsOrg"] = ctx.Repo.Owner.Name
|
||||||
|
ctx.Data["MentionableTeamsOrgAvator"] = ctx.Repo.Owner.RelAvatarLink()
|
||||||
|
}
|
||||||
|
|
|
@ -684,6 +684,10 @@ func ViewPullFiles(ctx *context.Context) {
|
||||||
ctx.ServerError("GetAssignees", err)
|
ctx.ServerError("GetAssignees", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
handleTeamMentions(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.Data["CurrentReview"], err = models.GetCurrentReview(ctx.User, issue)
|
ctx.Data["CurrentReview"], err = models.GetCurrentReview(ctx.User, issue)
|
||||||
if err != nil && !models.IsErrReviewNotExist(err) {
|
if err != nil && !models.IsErrReviewNotExist(err) {
|
||||||
ctx.ServerError("GetCurrentReview", err)
|
ctx.ServerError("GetCurrentReview", err)
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
EventSourceUpdateTime: {{NotificationSettings.EventSourceUpdateTime}},
|
EventSourceUpdateTime: {{NotificationSettings.EventSourceUpdateTime}},
|
||||||
},
|
},
|
||||||
PageIsProjects: {{if .PageIsProjects }}true{{else}}false{{end}},
|
PageIsProjects: {{if .PageIsProjects }}true{{else}}false{{end}},
|
||||||
{{if .RequireTribute}}
|
{{if .RequireTribute}}
|
||||||
tributeValues: Array.from(new Map([
|
tributeValues: Array.from(new Map([
|
||||||
{{ range .Participants }}
|
{{ range .Participants }}
|
||||||
['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}',
|
['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}',
|
||||||
|
@ -54,6 +54,10 @@
|
||||||
['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}',
|
['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}',
|
||||||
name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.RelAvatarLink}}'}],
|
name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.RelAvatarLink}}'}],
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ range .MentionableTeams }}
|
||||||
|
['{{$.MentionableTeamsOrg}}/{{.Name}}', {key: '{{$.MentionableTeamsOrg}}/{{.Name}}', value: '{{$.MentionableTeamsOrg}}/{{.Name}}',
|
||||||
|
name: '{{$.MentionableTeamsOrg}}/{{.Name}}', avatar: '{{$.MentionableTeamsOrgAvator}}'}],
|
||||||
|
{{ end }}
|
||||||
]).values()),
|
]).values()),
|
||||||
{{end}}
|
{{end}}
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue