@@ -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 { |
@@ -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)}) |
@@ -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() |
@@ -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, |
@@ -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 |
@@ -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")), |
@@ -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) |
@@ -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 { |
@@ -2009,6 +2009,13 @@ | |||
"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", |
@@ -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}} |
@@ -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="[ |
@@ -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'); | |||
}); |