]> source.dussan.org Git - gitea.git/commitdiff
Team dashboards (#14159)
authorJimmy Praet <jimmy.praet@telenet.be>
Sun, 27 Dec 2020 19:58:03 +0000 (20:58 +0100)
committerGitHub <noreply@github.com>
Sun, 27 Dec 2020 19:58:03 +0000 (21:58 +0200)
12 files changed:
models/action.go
models/org.go
models/repo_list.go
models/user_heatmap.go
options/locale/locale_en-US.ini
routers/api/v1/repo/repo.go
routers/routes/macaron.go
routers/user/home.go
templates/swagger/v1_json.tmpl
templates/user/dashboard/navbar.tmpl
templates/user/dashboard/repolist.tmpl
web_src/js/index.js

index ccf161192e334daf47fc300ab077aad9dee84172..2fdab7f4e9cac5e129a2d1c6932a4e435151a576 100644 (file)
@@ -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 {
index 84f2892e4a170e182bdffc49f058497669717de8..c93a30fd771fb2aae53e90a426dfd570701c28ea 100644 (file)
@@ -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)})
index 355b801a7ef05356228d3a520ecdfb0dcc749889..de3562a2abb6e4ad2668855b21350ce3d328bdba 100644 (file)
@@ -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()
index 425817e6d1495d1d026c28e6f649d7b4b0c0a29f..f518249111d64930a12b431f9832389939798eaa 100644 (file)
@@ -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,
index 6b772d2392544248c8ac86f3ea0c0b80988066ef..3aff43c0a8e766df835442d16481f17952dcc2b1 100644 (file)
@@ -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
index 048f7d6b1f58b962d05c106361fe7fa23cf8742c..f1df31ccac4b89aae531373d2ed5fb2bdc953372 100644 (file)
@@ -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")),
index 16977b9470923a866812789e75cfaeba5623c76e..019b476e717696473b595d484e513e046523cc61 100644 (file)
@@ -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)
index 92a9138475c55145244a6fd013a5e819934a20a4..27b7f3c29bca19ea6d05abe228dca923523615d1 100644 (file)
@@ -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 {
index 72665e2b6d2d57bc2f4125c8c45b41006a51e6dc..5de056f3c74902e0365ed854f9b06eb2bb70312a 100644 (file)
             "name": "priority_owner_id",
             "in": "query"
           },
+          {
+            "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",
index 890b192f9a8c2b87304fbe87c0644a79d0b18179..70eb7cce7f1ccfc9f014daf6104c82523b291299 100644 (file)
 
                {{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"}}&nbsp;{{.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"}}&nbsp;{{.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"}}&nbsp;{{.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"}}&nbsp;{{.i18n.Tr "milestones"}}
                                </a>
                                {{end}}
index 005e8756ff16b3a647c5f1fec1f839036eafbd7f..9115c62ecdf92c562b01fa8dc85fe6a62471336e 100644 (file)
@@ -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="[
index c3a70d756fc7e41373bbb9c8970c8f251b984cd5..93708e4fdc939ace58f2b7808fb14f9dddd5cf2d 100644 (file)
@@ -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');
           });