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