diff options
author | Jimmy Praet <jimmy.praet@telenet.be> | 2020-12-27 20:58:03 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-12-27 21:58:03 +0200 |
commit | 40274b4a935fff50e223751ce3653c2549352b10 (patch) | |
tree | a526f098018a04bbb12bccbebc6122ca209fa732 | |
parent | 25f8970b2cc5d2c9fd357ef2c60a54886154f6c9 (diff) | |
download | gitea-40274b4a935fff50e223751ce3653c2549352b10.tar.gz gitea-40274b4a935fff50e223751ce3653c2549352b10.zip |
Team dashboards (#14159)
-rw-r--r-- | models/action.go | 10 | ||||
-rw-r--r-- | models/org.go | 32 | ||||
-rw-r--r-- | models/repo_list.go | 5 | ||||
-rw-r--r-- | models/user_heatmap.go | 10 | ||||
-rw-r--r-- | options/locale/locale_en-US.ini | 1 | ||||
-rw-r--r-- | routers/api/v1/repo/repo.go | 6 | ||||
-rw-r--r-- | routers/routes/macaron.go | 6 | ||||
-rw-r--r-- | routers/user/home.go | 68 | ||||
-rw-r--r-- | templates/swagger/v1_json.tmpl | 7 | ||||
-rw-r--r-- | templates/user/dashboard/navbar.tmpl | 38 | ||||
-rw-r--r-- | templates/user/dashboard/repolist.tmpl | 3 | ||||
-rw-r--r-- | web_src/js/index.js | 9 |
12 files changed, 148 insertions, 47 deletions
diff --git a/models/action.go b/models/action.go index ccf161192e..2fdab7f4e9 100644 --- a/models/action.go +++ b/models/action.go @@ -289,6 +289,7 @@ func (a *Action) GetIssueContent() string { // GetFeedsOptions options for retrieving feeds type GetFeedsOptions struct { RequestedUser *User // the user we want activity for + RequestedTeam *Team // the team we want activity for Actor *User // the user viewing the activity IncludePrivate bool // include private actions OnlyPerformedBy bool // only actions performed by requested user @@ -357,6 +358,15 @@ func activityQueryCondition(opts GetFeedsOptions) (builder.Cond, error) { } } + if opts.RequestedTeam != nil { + env := opts.RequestedUser.AccessibleTeamReposEnv(opts.RequestedTeam) + teamRepoIDs, err := env.RepoIDs(1, opts.RequestedUser.NumRepos) + if err != nil { + return nil, fmt.Errorf("GetTeamRepositories: %v", err) + } + cond = cond.And(builder.In("repo_id", teamRepoIDs)) + } + cond = cond.And(builder.Eq{"user_id": opts.RequestedUser.ID}) if opts.OnlyPerformedBy { diff --git a/models/org.go b/models/org.go index 84f2892e4a..c93a30fd77 100644 --- a/models/org.go +++ b/models/org.go @@ -746,6 +746,7 @@ type AccessibleReposEnvironment interface { type accessibleReposEnv struct { org *User user *User + team *Team teamIDs []int64 e Engine keyword string @@ -782,16 +783,31 @@ func (org *User) accessibleReposEnv(e Engine, userID int64) (AccessibleReposEnvi }, nil } +// AccessibleTeamReposEnv an AccessibleReposEnvironment for the repositories in `org` +// that are accessible to the specified team. +func (org *User) AccessibleTeamReposEnv(team *Team) AccessibleReposEnvironment { + return &accessibleReposEnv{ + org: org, + team: team, + e: x, + orderBy: SearchOrderByRecentUpdated, + } +} + func (env *accessibleReposEnv) cond() builder.Cond { var cond = builder.NewCond() - if env.user == nil || !env.user.IsRestricted { - cond = cond.Or(builder.Eq{ - "`repository`.owner_id": env.org.ID, - "`repository`.is_private": false, - }) - } - if len(env.teamIDs) > 0 { - cond = cond.Or(builder.In("team_repo.team_id", env.teamIDs)) + if env.team != nil { + cond = cond.And(builder.Eq{"team_repo.team_id": env.team.ID}) + } else { + if env.user == nil || !env.user.IsRestricted { + cond = cond.Or(builder.Eq{ + "`repository`.owner_id": env.org.ID, + "`repository`.is_private": false, + }) + } + if len(env.teamIDs) > 0 { + cond = cond.Or(builder.In("team_repo.team_id", env.teamIDs)) + } } if env.keyword != "" { cond = cond.And(builder.Like{"`repository`.lower_name", strings.ToLower(env.keyword)}) diff --git a/models/repo_list.go b/models/repo_list.go index 355b801a7e..de3562a2ab 100644 --- a/models/repo_list.go +++ b/models/repo_list.go @@ -138,6 +138,7 @@ type SearchRepoOptions struct { Keyword string OwnerID int64 PriorityOwnerID int64 + TeamID int64 OrderBy SearchOrderBy Private bool // Include private repositories in results StarredByID int64 @@ -294,6 +295,10 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { cond = cond.And(accessCond) } + if opts.TeamID > 0 { + 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}))) + } + if opts.Keyword != "" { // separate keyword var subQueryCond = builder.NewCond() diff --git a/models/user_heatmap.go b/models/user_heatmap.go index 425817e6d1..f518249111 100644 --- a/models/user_heatmap.go +++ b/models/user_heatmap.go @@ -17,6 +17,15 @@ type UserHeatmapData struct { // GetUserHeatmapDataByUser returns an array of UserHeatmapData func GetUserHeatmapDataByUser(user *User, doer *User) ([]*UserHeatmapData, error) { + return getUserHeatmapData(user, nil, doer) +} + +// GetUserHeatmapDataByUserTeam returns an array of UserHeatmapData +func GetUserHeatmapDataByUserTeam(user *User, team *Team, doer *User) ([]*UserHeatmapData, error) { + return getUserHeatmapData(user, team, doer) +} + +func getUserHeatmapData(user *User, team *Team, doer *User) ([]*UserHeatmapData, error) { hdata := make([]*UserHeatmapData, 0) if !activityReadable(user, doer) { @@ -39,6 +48,7 @@ func GetUserHeatmapDataByUser(user *User, doer *User) ([]*UserHeatmapData, error cond, err := activityQueryCondition(GetFeedsOptions{ RequestedUser: user, + RequestedTeam: team, Actor: doer, IncludePrivate: true, // don't filter by private, as we already filter by repo access IncludeDeleted: true, diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 6b772d2392..3aff43c0a8 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -216,6 +216,7 @@ my_mirrors = My Mirrors view_home = View %s search_repos = Find a repository… filter = Other Filters +filter_by_team_repositories = Filter by team repositories show_archived = Archived show_both_archived_unarchived = Showing both archived and unarchived diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 048f7d6b1f..f1df31ccac 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -70,6 +70,11 @@ func Search(ctx *context.APIContext) { // description: repo owner to prioritize in the results // type: integer // format: int64 + // - name: team_id + // in: query + // description: search only for repos that belong to the given team id + // type: integer + // format: int64 // - name: starredBy // in: query // description: search only for repos that the user with the given id has starred @@ -131,6 +136,7 @@ func Search(ctx *context.APIContext) { Keyword: strings.Trim(ctx.Query("q"), " "), OwnerID: ctx.QueryInt64("uid"), PriorityOwnerID: ctx.QueryInt64("priority_owner_id"), + TeamID: ctx.QueryInt64("team_id"), TopicOnly: ctx.QueryBool("topic"), Collaborate: util.OptionalBoolNone, Private: ctx.IsSigned && (ctx.Query("private") == "" || ctx.QueryBool("private")), diff --git a/routers/routes/macaron.go b/routers/routes/macaron.go index 16977b9470..019b476e71 100644 --- a/routers/routes/macaron.go +++ b/routers/routes/macaron.go @@ -444,13 +444,15 @@ func RegisterMacaronRoutes(m *macaron.Macaron) { m.Group("/:org", func() { m.Get("/dashboard", user.Dashboard) + m.Get("/dashboard/:team", user.Dashboard) m.Get("/^:type(issues|pulls)$", user.Issues) + m.Get("/^:type(issues|pulls)$/:team", user.Issues) m.Get("/milestones", reqMilestonesDashboardPageEnabled, user.Milestones) + m.Get("/milestones/:team", reqMilestonesDashboardPageEnabled, user.Milestones) m.Get("/members", org.Members) m.Post("/members/action/:action", org.MembersAction) - m.Get("/teams", org.Teams) - }, context.OrgAssignment(true)) + }, context.OrgAssignment(true, false, true)) m.Group("/:org", func() { m.Get("/teams/:team", org.TeamMembers) diff --git a/routers/user/home.go b/routers/user/home.go index 92a9138475..27b7f3c29b 100644 --- a/routers/user/home.go +++ b/routers/user/home.go @@ -42,17 +42,8 @@ func getDashboardContextUser(ctx *context.Context) *models.User { ctxUser := ctx.User orgName := ctx.Params(":org") if len(orgName) > 0 { - // Organization. - org, err := models.GetUserByName(orgName) - if err != nil { - if models.IsErrUserNotExist(err) { - ctx.NotFound("GetUserByName", err) - } else { - ctx.ServerError("GetUserByName", err) - } - return nil - } - ctxUser = org + ctxUser = ctx.Org.Organization + ctx.Data["Teams"] = ctx.Org.Organization.Teams } ctx.Data["ContextUser"] = ctxUser @@ -112,12 +103,13 @@ func Dashboard(ctx *context.Context) { ctx.Data["PageIsDashboard"] = true ctx.Data["PageIsNews"] = true ctx.Data["SearchLimit"] = setting.UI.User.RepoPagingNum + // no heatmap access for admins; GetUserHeatmapDataByUser ignores the calling user // so everyone would get the same empty heatmap if setting.Service.EnableUserHeatmap && !ctxUser.KeepActivityPrivate { - data, err := models.GetUserHeatmapDataByUser(ctxUser, ctx.User) + data, err := models.GetUserHeatmapDataByUserTeam(ctxUser, ctx.Org.Team, ctx.User) if err != nil { - ctx.ServerError("GetUserHeatmapDataByUser", err) + ctx.ServerError("GetUserHeatmapDataByUserTeam", err) return } ctx.Data["HeatmapData"] = data @@ -126,12 +118,16 @@ func Dashboard(ctx *context.Context) { var err error var mirrors []*models.Repository if ctxUser.IsOrganization() { - env, err := ctxUser.AccessibleReposEnv(ctx.User.ID) - if err != nil { - ctx.ServerError("AccessibleReposEnv", err) - return + var env models.AccessibleReposEnvironment + if ctx.Org.Team != nil { + env = ctxUser.AccessibleTeamReposEnv(ctx.Org.Team) + } else { + env, err = ctxUser.AccessibleReposEnv(ctx.User.ID) + if err != nil { + ctx.ServerError("AccessibleReposEnv", err) + return + } } - mirrors, err = env.MirrorRepos() if err != nil { ctx.ServerError("env.MirrorRepos", err) @@ -155,6 +151,7 @@ func Dashboard(ctx *context.Context) { retrieveFeeds(ctx, models.GetFeedsOptions{ RequestedUser: ctxUser, + RequestedTeam: ctx.Org.Team, Actor: ctx.User, IncludePrivate: true, OnlyPerformedBy: false, @@ -183,16 +180,20 @@ func Milestones(ctx *context.Context) { return } - var ( - repoOpts = models.SearchRepoOptions{ - Actor: ctxUser, - OwnerID: ctxUser.ID, - Private: true, - AllPublic: false, // Include also all public repositories of users and public organisations - AllLimited: false, // Include also all public repositories of limited organisations - HasMilestones: util.OptionalBoolTrue, // Just needs display repos has milestones - } + repoOpts := models.SearchRepoOptions{ + Actor: ctxUser, + OwnerID: ctxUser.ID, + Private: true, + AllPublic: false, // Include also all public repositories of users and public organisations + AllLimited: false, // Include also all public repositories of limited organisations + HasMilestones: util.OptionalBoolTrue, // Just needs display repos has milestones + } + + if ctxUser.IsOrganization() && ctx.Org.Team != nil { + repoOpts.TeamID = ctx.Org.Team.ID + } + var ( userRepoCond = models.SearchRepositoryCondition(&repoOpts) // all repo condition user could visit repoCond = userRepoCond repoIDs []int64 @@ -412,10 +413,15 @@ func Issues(ctx *context.Context) { var err error var userRepoIDs []int64 if ctxUser.IsOrganization() { - env, err := ctxUser.AccessibleReposEnv(ctx.User.ID) - if err != nil { - ctx.ServerError("AccessibleReposEnv", err) - return + var env models.AccessibleReposEnvironment + if ctx.Org.Team != nil { + env = ctxUser.AccessibleTeamReposEnv(ctx.Org.Team) + } else { + env, err = ctxUser.AccessibleReposEnv(ctx.User.ID) + if err != nil { + ctx.ServerError("AccessibleReposEnv", err) + return + } } userRepoIDs, err = env.RepoIDs(1, ctxUser.NumRepos) if err != nil { diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 72665e2b6d..5de056f3c7 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -2012,6 +2012,13 @@ { "type": "integer", "format": "int64", + "description": "search only for repos that belong to the given team id", + "name": "team_id", + "in": "query" + }, + { + "type": "integer", + "format": "int64", "description": "search only for repos that the user with the given id has starred", "name": "starredBy", "in": "query" diff --git a/templates/user/dashboard/navbar.tmpl b/templates/user/dashboard/navbar.tmpl index 890b192f9a..70eb7cce7f 100644 --- a/templates/user/dashboard/navbar.tmpl +++ b/templates/user/dashboard/navbar.tmpl @@ -44,21 +44,51 @@ {{if .ContextUser.IsOrganization}} <div class="right stackable menu"> - <a class="{{if .PageIsNews}}active{{end}} item" style="margin-left: auto" href="{{AppSubUrl}}/org/{{.ContextUser.Name}}/dashboard"> + <div class="item"> + <div class="ui floating dropdown link jump"> + <span class="text"> + {{svg "octicon-people" 18}} + {{if .Team}} + {{.Team.Name}} + {{else}} + {{.i18n.Tr "org.teams"}} + {{end}} + {{svg "octicon-triangle-down" 14 "dropdown icon"}} + </span> + <div class="context user overflow menu" tabindex="-1"> + <div class="ui header"> + {{.i18n.Tr "home.filter_by_team_repositories"}} + </div> + <div class="scrolling menu items"> + <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}}"> + {{.i18n.Tr "all"}} + </a> + {{range .Org.Teams}} + {{if not .IncludesAllRepositories}} + <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}}"> + {{.Name}} + </a> + {{end}} + {{end}} + </div> + </div> + </div> + </div> + <a class="{{if .PageIsNews}}active{{end}} item" style="margin-left: auto" href="{{AppSubUrl}}/org/{{.ContextUser.Name}}/dashboard{{if .Team}}/{{.Team.Name}}{{end}}"> {{svg "octicon-rss"}} {{.i18n.Tr "activities"}} </a> {{if not .UnitIssuesGlobalDisabled}} - <a class="{{if .PageIsIssues}}active{{end}} item" href="{{AppSubUrl}}/org/{{.ContextUser.Name}}/issues"> + <a class="{{if .PageIsIssues}}active{{end}} item" href="{{AppSubUrl}}/org/{{.ContextUser.Name}}/issues{{if .Team}}/{{.Team.Name}}{{end}}"> {{svg "octicon-issue-opened"}} {{.i18n.Tr "issues"}} </a> {{end}} {{if not .UnitPullsGlobalDisabled}} - <a class="{{if .PageIsPulls}}active{{end}} item" href="{{AppSubUrl}}/org/{{.ContextUser.Name}}/pulls"> + <a class="{{if .PageIsPulls}}active{{end}} item" href="{{AppSubUrl}}/org/{{.ContextUser.Name}}/pulls{{if .Team}}/{{.Team.Name}}{{end}}"> {{svg "octicon-git-pull-request"}} {{.i18n.Tr "pull_requests"}} </a> {{end}} {{if and .ShowMilestonesDashboardPage (not (and .UnitIssuesGlobalDisabled .UnitPullsGlobalDisabled))}} - <a class="{{if .PageIsMilestonesDashboard}}active{{end}} item" href="{{AppSubUrl}}/org/{{.ContextUser.Name}}/milestones"> + <a class="{{if .PageIsMilestonesDashboard}}active{{end}} item" href="{{AppSubUrl}}/org/{{.ContextUser.Name}}/milestones{{if .Team}}/{{.Team.Name}}{{end}}"> {{svg "octicon-milestone"}} {{.i18n.Tr "milestones"}} </a> {{end}} diff --git a/templates/user/dashboard/repolist.tmpl b/templates/user/dashboard/repolist.tmpl index 005e8756ff..9115c62ecd 100644 --- a/templates/user/dashboard/repolist.tmpl +++ b/templates/user/dashboard/repolist.tmpl @@ -3,6 +3,9 @@ :search-limit="searchLimit" :suburl="suburl" :uid="uid" + {{if .Team}} + :team-id="{{.Team.ID}}" + {{end}} :more-repos-link="'{{.ContextUser.HomeLink}}'" {{if not .ContextUser.IsOrganization}} :organizations="[ diff --git a/web_src/js/index.js b/web_src/js/index.js index c3a70d756f..93708e4fdc 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -2755,6 +2755,11 @@ function initVueComponents() { type: Number, required: true }, + teamId: { + type: Number, + required: false, + default: 0 + }, organizations: { type: Array, default: () => [], @@ -2853,7 +2858,7 @@ function initVueComponents() { return this.repos.length > 0 && this.repos.length < this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`]; }, searchURL() { - return `${this.suburl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&q=${this.searchQuery + return `${this.suburl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=${this.searchQuery }&page=${this.page}&limit=${this.searchLimit}&mode=${this.repoTypes[this.reposFilter].searchMode }${this.reposFilter !== 'all' ? '&exclusive=1' : '' }${this.archivedFilter === 'archived' ? '&archived=true' : ''}${this.archivedFilter === 'unarchived' ? '&archived=false' : '' @@ -3034,7 +3039,7 @@ function initVueComponents() { this.isLoading = true; if (!this.reposTotalCount) { - const totalCountSearchURL = `${this.suburl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&q=&page=1&mode=`; + const totalCountSearchURL = `${this.suburl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`; $.getJSON(totalCountSearchURL, (_result, _textStatus, request) => { self.reposTotalCount = request.getResponseHeader('X-Total-Count'); }); |