Skip to content

Commit 40274b4

Browse files
authored
Team dashboards (#14159)
1 parent 25f8970 commit 40274b4

File tree

12 files changed

+148
-47
lines changed

12 files changed

+148
-47
lines changed

models/action.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ func (a *Action) GetIssueContent() string {
289289
// GetFeedsOptions options for retrieving feeds
290290
type GetFeedsOptions struct {
291291
RequestedUser *User // the user we want activity for
292+
RequestedTeam *Team // the team we want activity for
292293
Actor *User // the user viewing the activity
293294
IncludePrivate bool // include private actions
294295
OnlyPerformedBy bool // only actions performed by requested user
@@ -357,6 +358,15 @@ func activityQueryCondition(opts GetFeedsOptions) (builder.Cond, error) {
357358
}
358359
}
359360

361+
if opts.RequestedTeam != nil {
362+
env := opts.RequestedUser.AccessibleTeamReposEnv(opts.RequestedTeam)
363+
teamRepoIDs, err := env.RepoIDs(1, opts.RequestedUser.NumRepos)
364+
if err != nil {
365+
return nil, fmt.Errorf("GetTeamRepositories: %v", err)
366+
}
367+
cond = cond.And(builder.In("repo_id", teamRepoIDs))
368+
}
369+
360370
cond = cond.And(builder.Eq{"user_id": opts.RequestedUser.ID})
361371

362372
if opts.OnlyPerformedBy {

models/org.go

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,7 @@ type AccessibleReposEnvironment interface {
746746
type accessibleReposEnv struct {
747747
org *User
748748
user *User
749+
team *Team
749750
teamIDs []int64
750751
e Engine
751752
keyword string
@@ -782,16 +783,31 @@ func (org *User) accessibleReposEnv(e Engine, userID int64) (AccessibleReposEnvi
782783
}, nil
783784
}
784785

786+
// AccessibleTeamReposEnv an AccessibleReposEnvironment for the repositories in `org`
787+
// that are accessible to the specified team.
788+
func (org *User) AccessibleTeamReposEnv(team *Team) AccessibleReposEnvironment {
789+
return &accessibleReposEnv{
790+
org: org,
791+
team: team,
792+
e: x,
793+
orderBy: SearchOrderByRecentUpdated,
794+
}
795+
}
796+
785797
func (env *accessibleReposEnv) cond() builder.Cond {
786798
var cond = builder.NewCond()
787-
if env.user == nil || !env.user.IsRestricted {
788-
cond = cond.Or(builder.Eq{
789-
"`repository`.owner_id": env.org.ID,
790-
"`repository`.is_private": false,
791-
})
792-
}
793-
if len(env.teamIDs) > 0 {
794-
cond = cond.Or(builder.In("team_repo.team_id", env.teamIDs))
799+
if env.team != nil {
800+
cond = cond.And(builder.Eq{"team_repo.team_id": env.team.ID})
801+
} else {
802+
if env.user == nil || !env.user.IsRestricted {
803+
cond = cond.Or(builder.Eq{
804+
"`repository`.owner_id": env.org.ID,
805+
"`repository`.is_private": false,
806+
})
807+
}
808+
if len(env.teamIDs) > 0 {
809+
cond = cond.Or(builder.In("team_repo.team_id", env.teamIDs))
810+
}
795811
}
796812
if env.keyword != "" {
797813
cond = cond.And(builder.Like{"`repository`.lower_name", strings.ToLower(env.keyword)})

models/repo_list.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ type SearchRepoOptions struct {
138138
Keyword string
139139
OwnerID int64
140140
PriorityOwnerID int64
141+
TeamID int64
141142
OrderBy SearchOrderBy
142143
Private bool // Include private repositories in results
143144
StarredByID int64
@@ -294,6 +295,10 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond {
294295
cond = cond.And(accessCond)
295296
}
296297

298+
if opts.TeamID > 0 {
299+
cond = cond.And(builder.In("`repository`.id", builder.Select("`team_repo`.repo_id").From("team_repo").Where(builder.Eq{"`team_repo`.team_id": opts.TeamID})))
300+
}
301+
297302
if opts.Keyword != "" {
298303
// separate keyword
299304
var subQueryCond = builder.NewCond()

models/user_heatmap.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ type UserHeatmapData struct {
1717

1818
// GetUserHeatmapDataByUser returns an array of UserHeatmapData
1919
func GetUserHeatmapDataByUser(user *User, doer *User) ([]*UserHeatmapData, error) {
20+
return getUserHeatmapData(user, nil, doer)
21+
}
22+
23+
// GetUserHeatmapDataByUserTeam returns an array of UserHeatmapData
24+
func GetUserHeatmapDataByUserTeam(user *User, team *Team, doer *User) ([]*UserHeatmapData, error) {
25+
return getUserHeatmapData(user, team, doer)
26+
}
27+
28+
func getUserHeatmapData(user *User, team *Team, doer *User) ([]*UserHeatmapData, error) {
2029
hdata := make([]*UserHeatmapData, 0)
2130

2231
if !activityReadable(user, doer) {
@@ -39,6 +48,7 @@ func GetUserHeatmapDataByUser(user *User, doer *User) ([]*UserHeatmapData, error
3948

4049
cond, err := activityQueryCondition(GetFeedsOptions{
4150
RequestedUser: user,
51+
RequestedTeam: team,
4252
Actor: doer,
4353
IncludePrivate: true, // don't filter by private, as we already filter by repo access
4454
IncludeDeleted: true,

options/locale/locale_en-US.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ my_mirrors = My Mirrors
216216
view_home = View %s
217217
search_repos = Find a repository…
218218
filter = Other Filters
219+
filter_by_team_repositories = Filter by team repositories
219220
220221
show_archived = Archived
221222
show_both_archived_unarchived = Showing both archived and unarchived

routers/api/v1/repo/repo.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ func Search(ctx *context.APIContext) {
7070
// description: repo owner to prioritize in the results
7171
// type: integer
7272
// format: int64
73+
// - name: team_id
74+
// in: query
75+
// description: search only for repos that belong to the given team id
76+
// type: integer
77+
// format: int64
7378
// - name: starredBy
7479
// in: query
7580
// description: search only for repos that the user with the given id has starred
@@ -131,6 +136,7 @@ func Search(ctx *context.APIContext) {
131136
Keyword: strings.Trim(ctx.Query("q"), " "),
132137
OwnerID: ctx.QueryInt64("uid"),
133138
PriorityOwnerID: ctx.QueryInt64("priority_owner_id"),
139+
TeamID: ctx.QueryInt64("team_id"),
134140
TopicOnly: ctx.QueryBool("topic"),
135141
Collaborate: util.OptionalBoolNone,
136142
Private: ctx.IsSigned && (ctx.Query("private") == "" || ctx.QueryBool("private")),

routers/routes/macaron.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,13 +444,15 @@ func RegisterMacaronRoutes(m *macaron.Macaron) {
444444

445445
m.Group("/:org", func() {
446446
m.Get("/dashboard", user.Dashboard)
447+
m.Get("/dashboard/:team", user.Dashboard)
447448
m.Get("/^:type(issues|pulls)$", user.Issues)
449+
m.Get("/^:type(issues|pulls)$/:team", user.Issues)
448450
m.Get("/milestones", reqMilestonesDashboardPageEnabled, user.Milestones)
451+
m.Get("/milestones/:team", reqMilestonesDashboardPageEnabled, user.Milestones)
449452
m.Get("/members", org.Members)
450453
m.Post("/members/action/:action", org.MembersAction)
451-
452454
m.Get("/teams", org.Teams)
453-
}, context.OrgAssignment(true))
455+
}, context.OrgAssignment(true, false, true))
454456

455457
m.Group("/:org", func() {
456458
m.Get("/teams/:team", org.TeamMembers)

routers/user/home.go

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,8 @@ func getDashboardContextUser(ctx *context.Context) *models.User {
4242
ctxUser := ctx.User
4343
orgName := ctx.Params(":org")
4444
if len(orgName) > 0 {
45-
// Organization.
46-
org, err := models.GetUserByName(orgName)
47-
if err != nil {
48-
if models.IsErrUserNotExist(err) {
49-
ctx.NotFound("GetUserByName", err)
50-
} else {
51-
ctx.ServerError("GetUserByName", err)
52-
}
53-
return nil
54-
}
55-
ctxUser = org
45+
ctxUser = ctx.Org.Organization
46+
ctx.Data["Teams"] = ctx.Org.Organization.Teams
5647
}
5748
ctx.Data["ContextUser"] = ctxUser
5849

@@ -112,12 +103,13 @@ func Dashboard(ctx *context.Context) {
112103
ctx.Data["PageIsDashboard"] = true
113104
ctx.Data["PageIsNews"] = true
114105
ctx.Data["SearchLimit"] = setting.UI.User.RepoPagingNum
106+
115107
// no heatmap access for admins; GetUserHeatmapDataByUser ignores the calling user
116108
// so everyone would get the same empty heatmap
117109
if setting.Service.EnableUserHeatmap && !ctxUser.KeepActivityPrivate {
118-
data, err := models.GetUserHeatmapDataByUser(ctxUser, ctx.User)
110+
data, err := models.GetUserHeatmapDataByUserTeam(ctxUser, ctx.Org.Team, ctx.User)
119111
if err != nil {
120-
ctx.ServerError("GetUserHeatmapDataByUser", err)
112+
ctx.ServerError("GetUserHeatmapDataByUserTeam", err)
121113
return
122114
}
123115
ctx.Data["HeatmapData"] = data
@@ -126,12 +118,16 @@ func Dashboard(ctx *context.Context) {
126118
var err error
127119
var mirrors []*models.Repository
128120
if ctxUser.IsOrganization() {
129-
env, err := ctxUser.AccessibleReposEnv(ctx.User.ID)
130-
if err != nil {
131-
ctx.ServerError("AccessibleReposEnv", err)
132-
return
121+
var env models.AccessibleReposEnvironment
122+
if ctx.Org.Team != nil {
123+
env = ctxUser.AccessibleTeamReposEnv(ctx.Org.Team)
124+
} else {
125+
env, err = ctxUser.AccessibleReposEnv(ctx.User.ID)
126+
if err != nil {
127+
ctx.ServerError("AccessibleReposEnv", err)
128+
return
129+
}
133130
}
134-
135131
mirrors, err = env.MirrorRepos()
136132
if err != nil {
137133
ctx.ServerError("env.MirrorRepos", err)
@@ -155,6 +151,7 @@ func Dashboard(ctx *context.Context) {
155151

156152
retrieveFeeds(ctx, models.GetFeedsOptions{
157153
RequestedUser: ctxUser,
154+
RequestedTeam: ctx.Org.Team,
158155
Actor: ctx.User,
159156
IncludePrivate: true,
160157
OnlyPerformedBy: false,
@@ -183,16 +180,20 @@ func Milestones(ctx *context.Context) {
183180
return
184181
}
185182

186-
var (
187-
repoOpts = models.SearchRepoOptions{
188-
Actor: ctxUser,
189-
OwnerID: ctxUser.ID,
190-
Private: true,
191-
AllPublic: false, // Include also all public repositories of users and public organisations
192-
AllLimited: false, // Include also all public repositories of limited organisations
193-
HasMilestones: util.OptionalBoolTrue, // Just needs display repos has milestones
194-
}
183+
repoOpts := models.SearchRepoOptions{
184+
Actor: ctxUser,
185+
OwnerID: ctxUser.ID,
186+
Private: true,
187+
AllPublic: false, // Include also all public repositories of users and public organisations
188+
AllLimited: false, // Include also all public repositories of limited organisations
189+
HasMilestones: util.OptionalBoolTrue, // Just needs display repos has milestones
190+
}
191+
192+
if ctxUser.IsOrganization() && ctx.Org.Team != nil {
193+
repoOpts.TeamID = ctx.Org.Team.ID
194+
}
195195

196+
var (
196197
userRepoCond = models.SearchRepositoryCondition(&repoOpts) // all repo condition user could visit
197198
repoCond = userRepoCond
198199
repoIDs []int64
@@ -412,10 +413,15 @@ func Issues(ctx *context.Context) {
412413
var err error
413414
var userRepoIDs []int64
414415
if ctxUser.IsOrganization() {
415-
env, err := ctxUser.AccessibleReposEnv(ctx.User.ID)
416-
if err != nil {
417-
ctx.ServerError("AccessibleReposEnv", err)
418-
return
416+
var env models.AccessibleReposEnvironment
417+
if ctx.Org.Team != nil {
418+
env = ctxUser.AccessibleTeamReposEnv(ctx.Org.Team)
419+
} else {
420+
env, err = ctxUser.AccessibleReposEnv(ctx.User.ID)
421+
if err != nil {
422+
ctx.ServerError("AccessibleReposEnv", err)
423+
return
424+
}
419425
}
420426
userRepoIDs, err = env.RepoIDs(1, ctxUser.NumRepos)
421427
if err != nil {

templates/swagger/v1_json.tmpl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2009,6 +2009,13 @@
20092009
"name": "priority_owner_id",
20102010
"in": "query"
20112011
},
2012+
{
2013+
"type": "integer",
2014+
"format": "int64",
2015+
"description": "search only for repos that belong to the given team id",
2016+
"name": "team_id",
2017+
"in": "query"
2018+
},
20122019
{
20132020
"type": "integer",
20142021
"format": "int64",

templates/user/dashboard/navbar.tmpl

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,51 @@
4444

4545
{{if .ContextUser.IsOrganization}}
4646
<div class="right stackable menu">
47-
<a class="{{if .PageIsNews}}active{{end}} item" style="margin-left: auto" href="{{AppSubUrl}}/org/{{.ContextUser.Name}}/dashboard">
47+
<div class="item">
48+
<div class="ui floating dropdown link jump">
49+
<span class="text">
50+
{{svg "octicon-people" 18}}
51+
{{if .Team}}
52+
{{.Team.Name}}
53+
{{else}}
54+
{{.i18n.Tr "org.teams"}}
55+
{{end}}
56+
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
57+
</span>
58+
<div class="context user overflow menu" tabindex="-1">
59+
<div class="ui header">
60+
{{.i18n.Tr "home.filter_by_team_repositories"}}
61+
</div>
62+
<div class="scrolling menu items">
63+
<a class="{{if not $.Team}}active selected{{end}} item" title="{{.i18n.Tr "all"}}" href="{{AppSubUrl}}/org/{{$.Org.Name}}/{{if $.PageIsIssues}}issues{{else if $.PageIsPulls}}pulls{{else if $.PageIsMilestonesDashboard}}milestones{{else}}dashboard{{end}}">
64+
{{.i18n.Tr "all"}}
65+
</a>
66+
{{range .Org.Teams}}
67+
{{if not .IncludesAllRepositories}}
68+
<a class="{{if $.Team}}{{if eq $.Team.ID .ID}}active selected{{end}}{{end}} item" title="{{.Name}}" href="{{AppSubUrl}}/org/{{$.Org.Name}}/{{if $.PageIsIssues}}issues{{else if $.PageIsPulls}}pulls{{else if $.PageIsMilestonesDashboard}}milestones{{else}}dashboard{{end}}/{{.Name}}">
69+
{{.Name}}
70+
</a>
71+
{{end}}
72+
{{end}}
73+
</div>
74+
</div>
75+
</div>
76+
</div>
77+
<a class="{{if .PageIsNews}}active{{end}} item" style="margin-left: auto" href="{{AppSubUrl}}/org/{{.ContextUser.Name}}/dashboard{{if .Team}}/{{.Team.Name}}{{end}}">
4878
{{svg "octicon-rss"}}&nbsp;{{.i18n.Tr "activities"}}
4979
</a>
5080
{{if not .UnitIssuesGlobalDisabled}}
51-
<a class="{{if .PageIsIssues}}active{{end}} item" href="{{AppSubUrl}}/org/{{.ContextUser.Name}}/issues">
81+
<a class="{{if .PageIsIssues}}active{{end}} item" href="{{AppSubUrl}}/org/{{.ContextUser.Name}}/issues{{if .Team}}/{{.Team.Name}}{{end}}">
5282
{{svg "octicon-issue-opened"}}&nbsp;{{.i18n.Tr "issues"}}
5383
</a>
5484
{{end}}
5585
{{if not .UnitPullsGlobalDisabled}}
56-
<a class="{{if .PageIsPulls}}active{{end}} item" href="{{AppSubUrl}}/org/{{.ContextUser.Name}}/pulls">
86+
<a class="{{if .PageIsPulls}}active{{end}} item" href="{{AppSubUrl}}/org/{{.ContextUser.Name}}/pulls{{if .Team}}/{{.Team.Name}}{{end}}">
5787
{{svg "octicon-git-pull-request"}}&nbsp;{{.i18n.Tr "pull_requests"}}
5888
</a>
5989
{{end}}
6090
{{if and .ShowMilestonesDashboardPage (not (and .UnitIssuesGlobalDisabled .UnitPullsGlobalDisabled))}}
61-
<a class="{{if .PageIsMilestonesDashboard}}active{{end}} item" href="{{AppSubUrl}}/org/{{.ContextUser.Name}}/milestones">
91+
<a class="{{if .PageIsMilestonesDashboard}}active{{end}} item" href="{{AppSubUrl}}/org/{{.ContextUser.Name}}/milestones{{if .Team}}/{{.Team.Name}}{{end}}">
6292
{{svg "octicon-milestone"}}&nbsp;{{.i18n.Tr "milestones"}}
6393
</a>
6494
{{end}}

templates/user/dashboard/repolist.tmpl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
:search-limit="searchLimit"
44
:suburl="suburl"
55
:uid="uid"
6+
{{if .Team}}
7+
:team-id="{{.Team.ID}}"
8+
{{end}}
69
:more-repos-link="'{{.ContextUser.HomeLink}}'"
710
{{if not .ContextUser.IsOrganization}}
811
:organizations="[

web_src/js/index.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2755,6 +2755,11 @@ function initVueComponents() {
27552755
type: Number,
27562756
required: true
27572757
},
2758+
teamId: {
2759+
type: Number,
2760+
required: false,
2761+
default: 0
2762+
},
27582763
organizations: {
27592764
type: Array,
27602765
default: () => [],
@@ -2853,7 +2858,7 @@ function initVueComponents() {
28532858
return this.repos.length > 0 && this.repos.length < this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`];
28542859
},
28552860
searchURL() {
2856-
return `${this.suburl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&q=${this.searchQuery
2861+
return `${this.suburl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=${this.searchQuery
28572862
}&page=${this.page}&limit=${this.searchLimit}&mode=${this.repoTypes[this.reposFilter].searchMode
28582863
}${this.reposFilter !== 'all' ? '&exclusive=1' : ''
28592864
}${this.archivedFilter === 'archived' ? '&archived=true' : ''}${this.archivedFilter === 'unarchived' ? '&archived=false' : ''
@@ -3034,7 +3039,7 @@ function initVueComponents() {
30343039
this.isLoading = true;
30353040

30363041
if (!this.reposTotalCount) {
3037-
const totalCountSearchURL = `${this.suburl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&q=&page=1&mode=`;
3042+
const totalCountSearchURL = `${this.suburl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`;
30383043
$.getJSON(totalCountSearchURL, (_result, _textStatus, request) => {
30393044
self.reposTotalCount = request.getResponseHeader('X-Total-Count');
30403045
});

0 commit comments

Comments
 (0)