| @@ -124,6 +124,8 @@ collaborative_repos = Collaborative Repositories | |||||
| my_orgs = My Organizations | my_orgs = My Organizations | ||||
| my_mirrors = My Mirrors | my_mirrors = My Mirrors | ||||
| issues.in_your_repos = In your repositories | |||||
| [explore] | [explore] | ||||
| repos = Repositories | repos = Repositories | ||||
| @@ -275,6 +275,26 @@ | |||||
| "strictMath": 0, | "strictMath": 0, | ||||
| "strictUnits": 0 | "strictUnits": 0 | ||||
| }, | }, | ||||
| "\/public\/less\/_dashboard.less": { | |||||
| "allowInsecureImports": 0, | |||||
| "createSourceMap": 0, | |||||
| "disableJavascript": 0, | |||||
| "fileType": 1, | |||||
| "ieCompatibility": 1, | |||||
| "ignore": 1, | |||||
| "ignoreWasSetByUser": 0, | |||||
| "inputAbbreviatedPath": "\/public\/less\/_dashboard.less", | |||||
| "outputAbbreviatedPath": "\/public\/css\/_dashboard.css", | |||||
| "outputPathIsOutsideProject": 0, | |||||
| "outputPathIsSetByUser": 0, | |||||
| "outputStyle": 0, | |||||
| "relativeURLS": 0, | |||||
| "shouldRunAutoprefixer": 0, | |||||
| "shouldRunBless": 0, | |||||
| "strictImports": 0, | |||||
| "strictMath": 0, | |||||
| "strictUnits": 0 | |||||
| }, | |||||
| "\/public\/less\/_form.less": { | "\/public\/less\/_form.less": { | ||||
| "allowInsecureImports": 0, | "allowInsecureImports": 0, | ||||
| "createSourceMap": 0, | "createSourceMap": 0, | ||||
| @@ -641,9 +641,8 @@ func parseCountResult(results []map[string][]byte) int64 { | |||||
| } | } | ||||
| // GetIssueStats returns issue statistic information by given conditions. | // GetIssueStats returns issue statistic information by given conditions. | ||||
| func GetIssueStats(repoID, uid, labelID, milestoneID, assigneeID int64, isShowClosed bool, filterMode int) *IssueStats { | |||||
| func GetIssueStats(repoID, uid, labelID, milestoneID, assigneeID int64, filterMode int) *IssueStats { | |||||
| stats := &IssueStats{} | stats := &IssueStats{} | ||||
| // issue := new(Issue) | |||||
| queryStr := "SELECT COUNT(*) FROM `issue` " | queryStr := "SELECT COUNT(*) FROM `issue` " | ||||
| if labelID > 0 { | if labelID > 0 { | ||||
| @@ -659,38 +658,75 @@ func GetIssueStats(repoID, uid, labelID, milestoneID, assigneeID int64, isShowCl | |||||
| } | } | ||||
| switch filterMode { | switch filterMode { | ||||
| case FM_ALL, FM_ASSIGN: | case FM_ALL, FM_ASSIGN: | ||||
| resutls, _ := x.Query(queryStr+baseCond, repoID, false) | |||||
| stats.OpenCount = parseCountResult(resutls) | |||||
| resutls, _ = x.Query(queryStr+baseCond, repoID, true) | |||||
| stats.ClosedCount = parseCountResult(resutls) | |||||
| results, _ := x.Query(queryStr+baseCond, repoID, false) | |||||
| stats.OpenCount = parseCountResult(results) | |||||
| results, _ = x.Query(queryStr+baseCond, repoID, true) | |||||
| stats.ClosedCount = parseCountResult(results) | |||||
| case FM_CREATE: | case FM_CREATE: | ||||
| baseCond += " AND poster_id=?" | baseCond += " AND poster_id=?" | ||||
| resutls, _ := x.Query(queryStr+baseCond, repoID, false, uid) | |||||
| stats.OpenCount = parseCountResult(resutls) | |||||
| resutls, _ = x.Query(queryStr+baseCond, repoID, true, uid) | |||||
| stats.ClosedCount = parseCountResult(resutls) | |||||
| results, _ := x.Query(queryStr+baseCond, repoID, false, uid) | |||||
| stats.OpenCount = parseCountResult(results) | |||||
| results, _ = x.Query(queryStr+baseCond, repoID, true, uid) | |||||
| stats.ClosedCount = parseCountResult(results) | |||||
| case FM_MENTION: | case FM_MENTION: | ||||
| queryStr += " INNER JOIN `issue_user` ON `issue`.id=`issue_user`.issue_id" | queryStr += " INNER JOIN `issue_user` ON `issue`.id=`issue_user`.issue_id" | ||||
| baseCond += " AND `issue_user`.uid=? AND `issue_user`.is_mentioned=?" | baseCond += " AND `issue_user`.uid=? AND `issue_user`.is_mentioned=?" | ||||
| resutls, _ := x.Query(queryStr+baseCond, repoID, false, uid, true) | |||||
| stats.OpenCount = parseCountResult(resutls) | |||||
| resutls, _ = x.Query(queryStr+baseCond, repoID, true, uid, true) | |||||
| stats.ClosedCount = parseCountResult(resutls) | |||||
| results, _ := x.Query(queryStr+baseCond, repoID, false, uid, true) | |||||
| stats.OpenCount = parseCountResult(results) | |||||
| results, _ = x.Query(queryStr+baseCond, repoID, true, uid, true) | |||||
| stats.ClosedCount = parseCountResult(results) | |||||
| } | } | ||||
| return stats | return stats | ||||
| } | } | ||||
| // GetUserIssueStats returns issue statistic information for dashboard by given conditions. | // GetUserIssueStats returns issue statistic information for dashboard by given conditions. | ||||
| func GetUserIssueStats(uid int64, filterMode int) *IssueStats { | |||||
| func GetUserIssueStats(repoID, uid int64, filterMode int) *IssueStats { | |||||
| stats := &IssueStats{} | stats := &IssueStats{} | ||||
| issue := new(Issue) | issue := new(Issue) | ||||
| stats.AssignCount, _ = x.Where("assignee_id=?", uid).And("is_closed=?", false).Count(issue) | stats.AssignCount, _ = x.Where("assignee_id=?", uid).And("is_closed=?", false).Count(issue) | ||||
| stats.CreateCount, _ = x.Where("poster_id=?", uid).And("is_closed=?", false).Count(issue) | stats.CreateCount, _ = x.Where("poster_id=?", uid).And("is_closed=?", false).Count(issue) | ||||
| queryStr := "SELECT COUNT(*) FROM `issue` " | |||||
| baseCond := " WHERE issue.is_closed=?" | |||||
| if repoID > 0 { | |||||
| baseCond += " AND issue.repo_id=" + com.ToStr(repoID) | |||||
| } | |||||
| switch filterMode { | |||||
| case FM_ASSIGN: | |||||
| baseCond += " AND assignee_id=" + com.ToStr(uid) | |||||
| case FM_CREATE: | |||||
| baseCond += " AND poster_id=" + com.ToStr(uid) | |||||
| } | |||||
| results, _ := x.Query(queryStr+baseCond, false) | |||||
| stats.OpenCount = parseCountResult(results) | |||||
| results, _ = x.Query(queryStr+baseCond, true) | |||||
| stats.ClosedCount = parseCountResult(results) | |||||
| return stats | return stats | ||||
| } | } | ||||
| // GetRepoIssueStats returns number of open and closed repository issues by given filter mode. | |||||
| func GetRepoIssueStats(repoID, uid int64, filterMode int) (numOpen int64, numClosed int64) { | |||||
| queryStr := "SELECT COUNT(*) FROM `issue` " | |||||
| baseCond := " WHERE issue.repo_id=? AND issue.is_closed=?" | |||||
| switch filterMode { | |||||
| case FM_ASSIGN: | |||||
| baseCond += " AND assignee_id=" + com.ToStr(uid) | |||||
| case FM_CREATE: | |||||
| baseCond += " AND poster_id=" + com.ToStr(uid) | |||||
| } | |||||
| results, _ := x.Query(queryStr+baseCond, repoID, false) | |||||
| numOpen = parseCountResult(results) | |||||
| results, _ = x.Query(queryStr+baseCond, repoID, true) | |||||
| numClosed = parseCountResult(results) | |||||
| return numOpen, numClosed | |||||
| } | |||||
| func updateIssue(e Engine, issue *Issue) error { | func updateIssue(e Engine, issue *Issue) error { | ||||
| _, err := e.Id(issue.ID).AllCols().Update(issue) | _, err := e.Id(issue.ID).AllCols().Update(issue) | ||||
| return err | return err | ||||
| @@ -221,6 +221,11 @@ func (repo *Repository) GetMilestoneByID(milestoneID int64) (*Milestone, error) | |||||
| return GetRepoMilestoneByID(repo.ID, milestoneID) | return GetRepoMilestoneByID(repo.ID, milestoneID) | ||||
| } | } | ||||
| // IssueStats returns number of open and closed repository issues by given filter mode. | |||||
| func (repo *Repository) IssueStats(uid int64, filterMode int) (int64, int64) { | |||||
| return GetRepoIssueStats(repo.ID, uid, filterMode) | |||||
| } | |||||
| func (repo *Repository) GetMirror() (err error) { | func (repo *Repository) GetMirror() (err error) { | ||||
| repo.Mirror, err = GetMirror(repo.ID) | repo.Mirror, err = GetMirror(repo.ID) | ||||
| return err | return err | ||||
| @@ -0,0 +1,20 @@ | |||||
| .dashboard { | |||||
| padding-top: 15px; | |||||
| padding-bottom: @footer-margin * 2; | |||||
| &.issues { | |||||
| .context.user.menu { | |||||
| min-width: 200px; | |||||
| .ui.header { | |||||
| font-size: 1rem; | |||||
| text-transform: none; | |||||
| } | |||||
| } | |||||
| .filter.menu { | |||||
| .item.active { | |||||
| background-color: #4183c4; | |||||
| color: #FFF; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -79,47 +79,6 @@ | |||||
| } | } | ||||
| } | } | ||||
| .page.buttons { | |||||
| padding-top: 15px; | |||||
| } | |||||
| .issue.list { | |||||
| list-style: none; | |||||
| padding-top: 15px; | |||||
| >.item { | |||||
| padding-top: 15px; | |||||
| padding-bottom: 10px; | |||||
| border-bottom: 1px dashed #AAA; | |||||
| .title { | |||||
| color: #444; | |||||
| font-size: 15px; | |||||
| font-weight: bold; | |||||
| margin: 0 6px; | |||||
| &:hover { | |||||
| color: #000; | |||||
| } | |||||
| } | |||||
| .comment { | |||||
| padding-right: 10px; | |||||
| color: #666; | |||||
| } | |||||
| .desc { | |||||
| padding-top: 5px; | |||||
| color: #999; | |||||
| a.milestone { | |||||
| padding-left: 5px; | |||||
| color: #999!important; | |||||
| &:hover { | |||||
| color: #000!important; | |||||
| } | |||||
| } | |||||
| .assignee { | |||||
| margin-top: -5px; | |||||
| margin-right: 5px; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @comment-avatar-width: 3em; | @comment-avatar-width: 3em; | ||||
| &.new.issue { | &.new.issue { | ||||
| .comment.form { | .comment.form { | ||||
| @@ -607,6 +566,48 @@ | |||||
| } | } | ||||
| } | } | ||||
| .issue.list { | |||||
| list-style: none; | |||||
| padding-top: 15px; | |||||
| >.item { | |||||
| padding-top: 15px; | |||||
| padding-bottom: 10px; | |||||
| border-bottom: 1px dashed #AAA; | |||||
| .title { | |||||
| color: #444; | |||||
| font-size: 15px; | |||||
| font-weight: bold; | |||||
| margin: 0 6px; | |||||
| &:hover { | |||||
| color: #000; | |||||
| } | |||||
| } | |||||
| .comment { | |||||
| padding-right: 10px; | |||||
| color: #666; | |||||
| } | |||||
| .desc { | |||||
| padding-top: 5px; | |||||
| color: #999; | |||||
| a.milestone { | |||||
| padding-left: 5px; | |||||
| color: #999!important; | |||||
| &:hover { | |||||
| color: #000!important; | |||||
| } | |||||
| } | |||||
| .assignee { | |||||
| margin-top: -5px; | |||||
| margin-right: 5px; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| .page.buttons { | |||||
| padding-top: 15px; | |||||
| } | |||||
| .ui.comments { | .ui.comments { | ||||
| .dropzone { | .dropzone { | ||||
| width: 100%; | width: 100%; | ||||
| @@ -6,4 +6,5 @@ | |||||
| @import "_form"; | @import "_form"; | ||||
| @import "_repository"; | @import "_repository"; | ||||
| @import "_user"; | @import "_user"; | ||||
| @import "_dashboard"; | |||||
| @import "_admin"; | @import "_admin"; | ||||
| @@ -79,11 +79,11 @@ func Issues(ctx *middleware.Context) { | |||||
| filterMode := models.FM_ALL | filterMode := models.FM_ALL | ||||
| switch viewType { | switch viewType { | ||||
| case "assigned": | case "assigned": | ||||
| assigneeID = ctx.User.Id | |||||
| filterMode = models.FM_ASSIGN | filterMode = models.FM_ASSIGN | ||||
| assigneeID = ctx.User.Id | |||||
| case "created_by": | case "created_by": | ||||
| posterID = ctx.User.Id | |||||
| filterMode = models.FM_CREATE | filterMode = models.FM_CREATE | ||||
| posterID = ctx.User.Id | |||||
| case "mentioned": | case "mentioned": | ||||
| filterMode = models.FM_MENTION | filterMode = models.FM_MENTION | ||||
| } | } | ||||
| @@ -97,7 +97,7 @@ func Issues(ctx *middleware.Context) { | |||||
| selectLabels := ctx.Query("labels") | selectLabels := ctx.Query("labels") | ||||
| milestoneID := ctx.QueryInt64("milestone") | milestoneID := ctx.QueryInt64("milestone") | ||||
| isShowClosed := ctx.Query("state") == "closed" | isShowClosed := ctx.Query("state") == "closed" | ||||
| issueStats := models.GetIssueStats(repo.ID, uid, com.StrTo(selectLabels).MustInt64(), milestoneID, assigneeID, isShowClosed, filterMode) | |||||
| issueStats := models.GetIssueStats(repo.ID, uid, com.StrTo(selectLabels).MustInt64(), milestoneID, assigneeID, filterMode) | |||||
| page := ctx.QueryInt("page") | page := ctx.QueryInt("page") | ||||
| if page <= 1 { | if page <= 1 { | ||||
| @@ -10,10 +10,10 @@ import ( | |||||
| "strings" | "strings" | ||||
| "github.com/Unknwon/com" | "github.com/Unknwon/com" | ||||
| "github.com/Unknwon/paginater" | |||||
| "github.com/gogits/gogs/models" | "github.com/gogits/gogs/models" | ||||
| "github.com/gogits/gogs/modules/base" | "github.com/gogits/gogs/modules/base" | ||||
| "github.com/gogits/gogs/modules/log" | |||||
| "github.com/gogits/gogs/modules/middleware" | "github.com/gogits/gogs/modules/middleware" | ||||
| "github.com/gogits/gogs/modules/setting" | "github.com/gogits/gogs/modules/setting" | ||||
| ) | ) | ||||
| @@ -21,18 +21,13 @@ import ( | |||||
| const ( | const ( | ||||
| DASHBOARD base.TplName = "user/dashboard/dashboard" | DASHBOARD base.TplName = "user/dashboard/dashboard" | ||||
| PULLS base.TplName = "user/dashboard/pulls" | PULLS base.TplName = "user/dashboard/pulls" | ||||
| ISSUES base.TplName = "user/issues" | |||||
| ISSUES base.TplName = "user/dashboard/issues" | |||||
| STARS base.TplName = "user/stars" | STARS base.TplName = "user/stars" | ||||
| PROFILE base.TplName = "user/profile" | PROFILE base.TplName = "user/profile" | ||||
| ) | ) | ||||
| func Dashboard(ctx *middleware.Context) { | |||||
| ctx.Data["Title"] = ctx.Tr("dashboard") | |||||
| ctx.Data["PageIsDashboard"] = true | |||||
| ctx.Data["PageIsNews"] = true | |||||
| var ctxUser *models.User | |||||
| // Check context type. | |||||
| func getDashboardContextUser(ctx *middleware.Context) *models.User { | |||||
| ctxUser := ctx.User | |||||
| orgName := ctx.Params(":org") | orgName := ctx.Params(":org") | ||||
| if len(orgName) > 0 { | if len(orgName) > 0 { | ||||
| // Organization. | // Organization. | ||||
| @@ -43,10 +38,33 @@ func Dashboard(ctx *middleware.Context) { | |||||
| } else { | } else { | ||||
| ctx.Handle(500, "GetUserByName", err) | ctx.Handle(500, "GetUserByName", err) | ||||
| } | } | ||||
| return | |||||
| return nil | |||||
| } | } | ||||
| ctxUser = org | ctxUser = org | ||||
| } else { | |||||
| } | |||||
| ctx.Data["ContextUser"] = ctxUser | |||||
| if err := ctx.User.GetOrganizations(); err != nil { | |||||
| ctx.Handle(500, "GetOrganizations", err) | |||||
| return nil | |||||
| } | |||||
| ctx.Data["Orgs"] = ctx.User.Orgs | |||||
| return ctxUser | |||||
| } | |||||
| func Dashboard(ctx *middleware.Context) { | |||||
| ctx.Data["Title"] = ctx.Tr("dashboard") | |||||
| ctx.Data["PageIsDashboard"] = true | |||||
| ctx.Data["PageIsNews"] = true | |||||
| ctxUser := getDashboardContextUser(ctx) | |||||
| if ctx.Written() { | |||||
| return | |||||
| } | |||||
| // Check context type. | |||||
| if !ctxUser.IsOrganization() { | |||||
| // Normal user. | // Normal user. | ||||
| ctxUser = ctx.User | ctxUser = ctx.User | ||||
| collaborates, err := ctx.User.GetAccessibleRepositories() | collaborates, err := ctx.User.GetAccessibleRepositories() | ||||
| @@ -63,13 +81,6 @@ func Dashboard(ctx *middleware.Context) { | |||||
| ctx.Data["CollaborateCount"] = len(repositories) | ctx.Data["CollaborateCount"] = len(repositories) | ||||
| ctx.Data["CollaborativeRepos"] = repositories | ctx.Data["CollaborativeRepos"] = repositories | ||||
| } | } | ||||
| ctx.Data["ContextUser"] = ctxUser | |||||
| if err := ctx.User.GetOrganizations(); err != nil { | |||||
| ctx.Handle(500, "GetOrganizations", err) | |||||
| return | |||||
| } | |||||
| ctx.Data["Orgs"] = ctx.User.Orgs | |||||
| repos, err := models.GetRepositories(ctxUser.Id, true) | repos, err := models.GetRepositories(ctxUser.Id, true) | ||||
| if err != nil { | if err != nil { | ||||
| @@ -142,6 +153,138 @@ func Pulls(ctx *middleware.Context) { | |||||
| ctx.HTML(200, PULLS) | ctx.HTML(200, PULLS) | ||||
| } | } | ||||
| func Issues(ctx *middleware.Context) { | |||||
| ctx.Data["Title"] = ctx.Tr("issues") | |||||
| ctx.Data["PageIsIssues"] = true | |||||
| ctxUser := getDashboardContextUser(ctx) | |||||
| if ctx.Written() { | |||||
| return | |||||
| } | |||||
| // Organization does not have view type and filter mode. | |||||
| var ( | |||||
| viewType string | |||||
| filterMode = models.FM_ALL | |||||
| assigneeID int64 | |||||
| posterID int64 | |||||
| ) | |||||
| if ctxUser.IsOrganization() { | |||||
| viewType = "all" | |||||
| } else { | |||||
| viewType = ctx.Query("type") | |||||
| types := []string{"assigned", "created_by"} | |||||
| if !com.IsSliceContainsStr(types, viewType) { | |||||
| viewType = "all" | |||||
| } | |||||
| switch viewType { | |||||
| case "assigned": | |||||
| filterMode = models.FM_ASSIGN | |||||
| assigneeID = ctxUser.Id | |||||
| case "created_by": | |||||
| filterMode = models.FM_CREATE | |||||
| posterID = ctxUser.Id | |||||
| } | |||||
| } | |||||
| repoID := ctx.QueryInt64("repo") | |||||
| isShowClosed := ctx.Query("state") == "closed" | |||||
| issueStats := models.GetUserIssueStats(repoID, ctxUser.Id, filterMode) | |||||
| page := ctx.QueryInt("page") | |||||
| if page <= 1 { | |||||
| page = 1 | |||||
| } | |||||
| var total int | |||||
| if !isShowClosed { | |||||
| total = int(issueStats.OpenCount) | |||||
| } else { | |||||
| total = int(issueStats.ClosedCount) | |||||
| } | |||||
| ctx.Data["Page"] = paginater.New(total, setting.IssuePagingNum, page, 5) | |||||
| // Get repositories. | |||||
| repos, err := models.GetRepositories(ctxUser.Id, true) | |||||
| if err != nil { | |||||
| ctx.Handle(500, "GetRepositories", err) | |||||
| return | |||||
| } | |||||
| repoIDs := make([]int64, 0, len(repos)) | |||||
| showRepos := make([]*models.Repository, 0, len(repos)) | |||||
| for _, repo := range repos { | |||||
| if repo.NumIssues == 0 { | |||||
| continue | |||||
| } | |||||
| repoIDs = append(repoIDs, repo.ID) | |||||
| repo.NumOpenIssues = repo.NumIssues - repo.NumClosedIssues | |||||
| issueStats.AllCount += int64(repo.NumOpenIssues) | |||||
| if repo.ID == repoID { | |||||
| repo.NumOpenIssues = int(issueStats.OpenCount) | |||||
| repo.NumClosedIssues = int(issueStats.ClosedCount) | |||||
| } else if filterMode != models.FM_ALL && repo.NumIssues > 0 { | |||||
| // Calculate repository issue count with filter mode. | |||||
| numOpen, numClosed := repo.IssueStats(ctxUser.Id, filterMode) | |||||
| repo.NumOpenIssues, repo.NumClosedIssues = int(numOpen), int(numClosed) | |||||
| } | |||||
| if repo.ID == repoID || | |||||
| (isShowClosed && repo.NumClosedIssues > 0) || | |||||
| (!isShowClosed && repo.NumOpenIssues > 0) { | |||||
| showRepos = append(showRepos, repo) | |||||
| } | |||||
| } | |||||
| ctx.Data["Repos"] = showRepos | |||||
| if repoID > 0 { | |||||
| repoIDs = []int64{repoID} | |||||
| } | |||||
| // Get issues. | |||||
| issues, err := models.Issues(ctxUser.Id, assigneeID, repoID, posterID, 0, | |||||
| page, isShowClosed, false, "", "") | |||||
| if err != nil { | |||||
| ctx.Handle(500, "Issues: %v", err) | |||||
| return | |||||
| } | |||||
| // Get posters and repository. | |||||
| for i := range issues { | |||||
| issues[i].Repo, err = models.GetRepositoryByID(issues[i].RepoID) | |||||
| if err != nil { | |||||
| ctx.Handle(500, "GetRepositoryByID", fmt.Errorf("[#%d]%v", issues[i].ID, err)) | |||||
| return | |||||
| } | |||||
| if err = issues[i].Repo.GetOwner(); err != nil { | |||||
| ctx.Handle(500, "GetOwner", fmt.Errorf("[#%d]%v", issues[i].ID, err)) | |||||
| return | |||||
| } | |||||
| if err = issues[i].GetPoster(); err != nil { | |||||
| ctx.Handle(500, "GetPoster", fmt.Errorf("[#%d]%v", issues[i].ID, err)) | |||||
| return | |||||
| } | |||||
| } | |||||
| ctx.Data["Issues"] = issues | |||||
| ctx.Data["IssueStats"] = issueStats | |||||
| ctx.Data["ViewType"] = viewType | |||||
| ctx.Data["RepoID"] = repoID | |||||
| ctx.Data["IsShowClosed"] = isShowClosed | |||||
| if isShowClosed { | |||||
| ctx.Data["State"] = "closed" | |||||
| } else { | |||||
| ctx.Data["State"] = "open" | |||||
| } | |||||
| ctx.HTML(200, ISSUES) | |||||
| } | |||||
| func ShowSSHKeys(ctx *middleware.Context, uid int64) { | func ShowSSHKeys(ctx *middleware.Context, uid int64) { | ||||
| keys, err := models.ListPublicKeys(uid) | keys, err := models.ListPublicKeys(uid) | ||||
| if err != nil { | if err != nil { | ||||
| @@ -256,136 +399,3 @@ func Email2User(ctx *middleware.Context) { | |||||
| } | } | ||||
| ctx.Redirect(setting.AppSubUrl + "/user/" + u.Name) | ctx.Redirect(setting.AppSubUrl + "/user/" + u.Name) | ||||
| } | } | ||||
| func Issues(ctx *middleware.Context) { | |||||
| ctx.Data["Title"] = ctx.Tr("issues") | |||||
| ctx.Data["PageIsDashboard"] = true | |||||
| ctx.Data["PageIsIssues"] = true | |||||
| viewType := ctx.Query("type") | |||||
| types := []string{"assigned", "created_by"} | |||||
| if !com.IsSliceContainsStr(types, viewType) { | |||||
| viewType = "all" | |||||
| } | |||||
| isShowClosed := ctx.Query("state") == "closed" | |||||
| var filterMode int | |||||
| switch viewType { | |||||
| case "assigned": | |||||
| filterMode = models.FM_ASSIGN | |||||
| case "created_by": | |||||
| filterMode = models.FM_CREATE | |||||
| } | |||||
| repoId, _ := com.StrTo(ctx.Query("repoid")).Int64() | |||||
| issueStats := models.GetUserIssueStats(ctx.User.Id, filterMode) | |||||
| // Get all repositories. | |||||
| repos, err := models.GetRepositories(ctx.User.Id, true) | |||||
| if err != nil { | |||||
| ctx.Handle(500, "user.Issues(GetRepositories)", err) | |||||
| return | |||||
| } | |||||
| repoIds := make([]int64, 0, len(repos)) | |||||
| showRepos := make([]*models.Repository, 0, len(repos)) | |||||
| for _, repo := range repos { | |||||
| if repo.NumIssues == 0 { | |||||
| continue | |||||
| } | |||||
| repoIds = append(repoIds, repo.ID) | |||||
| repo.NumOpenIssues = repo.NumIssues - repo.NumClosedIssues | |||||
| issueStats.AllCount += int64(repo.NumOpenIssues) | |||||
| if isShowClosed { | |||||
| if repo.NumClosedIssues > 0 { | |||||
| if filterMode == models.FM_CREATE { | |||||
| repo.NumClosedIssues = int(models.GetIssueCountByPoster(ctx.User.Id, repo.ID, isShowClosed)) | |||||
| } | |||||
| showRepos = append(showRepos, repo) | |||||
| } | |||||
| } else { | |||||
| if repo.NumOpenIssues > 0 { | |||||
| if filterMode == models.FM_CREATE { | |||||
| repo.NumOpenIssues = int(models.GetIssueCountByPoster(ctx.User.Id, repo.ID, isShowClosed)) | |||||
| } | |||||
| showRepos = append(showRepos, repo) | |||||
| } | |||||
| } | |||||
| } | |||||
| if repoId > 0 { | |||||
| repoIds = []int64{repoId} | |||||
| } | |||||
| page, _ := com.StrTo(ctx.Query("page")).Int() | |||||
| // Get all issues. | |||||
| var ius []*models.IssueUser | |||||
| switch viewType { | |||||
| case "assigned": | |||||
| fallthrough | |||||
| case "created_by": | |||||
| ius, err = models.GetIssueUserPairsByMode(ctx.User.Id, repoId, isShowClosed, page, filterMode) | |||||
| default: | |||||
| ius, err = models.GetIssueUserPairsByRepoIds(repoIds, isShowClosed, page) | |||||
| } | |||||
| if err != nil { | |||||
| ctx.Handle(500, "user.Issues(GetAllIssueUserPairs)", err) | |||||
| return | |||||
| } | |||||
| issues := make([]*models.Issue, len(ius)) | |||||
| for i := range ius { | |||||
| issues[i], err = models.GetIssueByID(ius[i].IssueID) | |||||
| if err != nil { | |||||
| if models.IsErrIssueNotExist(err) { | |||||
| log.Warn("user.Issues(GetIssueById #%d): issue not exist", ius[i].IssueID) | |||||
| continue | |||||
| } else { | |||||
| ctx.Handle(500, fmt.Sprintf("user.Issues(GetIssueById #%d)", ius[i].IssueID), err) | |||||
| return | |||||
| } | |||||
| } | |||||
| issues[i].Repo, err = models.GetRepositoryByID(issues[i].RepoID) | |||||
| if err != nil { | |||||
| if models.IsErrRepoNotExist(err) { | |||||
| log.Warn("GetRepositoryById[%d]: repository not exist", issues[i].RepoID) | |||||
| continue | |||||
| } else { | |||||
| ctx.Handle(500, fmt.Sprintf("GetRepositoryById[%d]", issues[i].RepoID), err) | |||||
| return | |||||
| } | |||||
| } | |||||
| if err = issues[i].Repo.GetOwner(); err != nil { | |||||
| ctx.Handle(500, "user.Issues(GetOwner)", err) | |||||
| return | |||||
| } | |||||
| if err = issues[i].GetPoster(); err != nil { | |||||
| ctx.Handle(500, "user.Issues(GetUserById)", err) | |||||
| return | |||||
| } | |||||
| } | |||||
| ctx.Data["RepoId"] = repoId | |||||
| ctx.Data["Repos"] = showRepos | |||||
| ctx.Data["Issues"] = issues | |||||
| ctx.Data["ViewType"] = viewType | |||||
| ctx.Data["IssueStats"] = issueStats | |||||
| ctx.Data["IsShowClosed"] = isShowClosed | |||||
| if isShowClosed { | |||||
| ctx.Data["State"] = "closed" | |||||
| ctx.Data["ShowCount"] = issueStats.ClosedCount | |||||
| } else { | |||||
| ctx.Data["ShowCount"] = issueStats.OpenCount | |||||
| } | |||||
| ctx.Data["ContextUser"] = ctx.User | |||||
| ctx.HTML(200, ISSUES) | |||||
| } | |||||
| @@ -62,6 +62,7 @@ | |||||
| {{if .IsSigned}} | {{if .IsSigned}} | ||||
| <a class="item{{if .PageIsDashboard}} active{{end}}" href="{{AppSubUrl}}/">{{.i18n.Tr "dashboard"}}</a> | <a class="item{{if .PageIsDashboard}} active{{end}}" href="{{AppSubUrl}}/">{{.i18n.Tr "dashboard"}}</a> | ||||
| <a class="item{{if .PageIsIssues}} active{{end}}" href="{{AppSubUrl}}/issues">{{.i18n.Tr "issues"}}</a> | |||||
| {{else}} | {{else}} | ||||
| <a class="item{{if .PageIsHome}} active{{end}}" href="{{AppSubUrl}}/">{{.i18n.Tr "home"}}</a> | <a class="item{{if .PageIsHome}} active{{end}}" href="{{AppSubUrl}}/">{{.i18n.Tr "home"}}</a> | ||||
| {{end}} | {{end}} | ||||
| @@ -75,7 +76,6 @@ | |||||
| </div> --> | </div> --> | ||||
| {{if .IsSigned}} | {{if .IsSigned}} | ||||
| <a class="item{{if .PageIsIssues}} active{{end}}" href="{{AppSubUrl}}/issues">{{.i18n.Tr "issues"}}</a> | |||||
| <div class="right menu"> | <div class="right menu"> | ||||
| <div class="ui dropdown head link jump item poping up" data-content="{{.i18n.Tr "create_new"}}" data-variation="tiny inverted"> | <div class="ui dropdown head link jump item poping up" data-content="{{.i18n.Tr "create_new"}}" data-variation="tiny inverted"> | ||||
| <span class="text"> | <span class="text"> | ||||
| @@ -10,9 +10,11 @@ | |||||
| <a class="{{if .PageIsSettingsHooks}}active{{end}} item" href="{{.RepoLink}}/settings/hooks"> | <a class="{{if .PageIsSettingsHooks}}active{{end}} item" href="{{.RepoLink}}/settings/hooks"> | ||||
| {{.i18n.Tr "repo.settings.hooks"}} | {{.i18n.Tr "repo.settings.hooks"}} | ||||
| </a> | </a> | ||||
| {{if or .SignedUser.AllowGitHook .SignedUser.IsAdmin}} | |||||
| <a class="{{if .PageIsSettingsGitHooks}}active{{end}} item" href="{{.RepoLink}}/settings/hooks/git"> | <a class="{{if .PageIsSettingsGitHooks}}active{{end}} item" href="{{.RepoLink}}/settings/hooks/git"> | ||||
| {{.i18n.Tr "repo.settings.githooks"}} | {{.i18n.Tr "repo.settings.githooks"}} | ||||
| </a> | </a> | ||||
| {{end}} | |||||
| <a class="{{if .PageIsSettingsKeys}}active{{end}} item" href="{{.RepoLink}}/settings/keys"> | <a class="{{if .PageIsSettingsKeys}}active{{end}} item" href="{{.RepoLink}}/settings/keys"> | ||||
| {{.i18n.Tr "repo.settings.deploy_keys"}} | {{.i18n.Tr "repo.settings.deploy_keys"}} | ||||
| </a> | </a> | ||||
| @@ -0,0 +1,86 @@ | |||||
| {{template "base/head" .}} | |||||
| <div class="dashboard issues"> | |||||
| {{template "user/dashboard/navbar" .}} | |||||
| <div class="ui container"> | |||||
| <div class="ui grid"> | |||||
| <div class="four wide column"> | |||||
| <div class="ui secondary vertical filter menu"> | |||||
| <a class="{{if eq .ViewType "all"}}active{{end}} item" href="{{.Link}}?repo={{.RepoID}}&state={{.State}}"> | |||||
| {{.i18n.Tr "home.issues.in_your_repos"}} | |||||
| <strong class="ui right">{{.IssueStats.AllCount}}</strong> | |||||
| </a> | |||||
| <a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{.Link}}?type=assigned&repo={{.RepoID}}&state={{.State}}"> | |||||
| {{.i18n.Tr "repo.issues.filter_type.assigned_to_you"}} | |||||
| <strong class="ui right">{{.IssueStats.AssignCount}}</strong> | |||||
| </a> | |||||
| <a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{.Link}}?type=created_by&repo={{.RepoID}}&state={{.State}}"> | |||||
| {{.i18n.Tr "repo.issues.filter_type.created_by_you"}} | |||||
| <strong class="ui right">{{.IssueStats.CreateCount}}</strong> | |||||
| </a> | |||||
| <div class="ui divider"></div> | |||||
| {{range .Repos}} | |||||
| <a class="{{if eq $.RepoID .ID}}active{{end}} item" href="{{$.Link}}?type={{$.ViewType}}{{if not (eq $.RepoID .ID)}}&repo={{.ID}}{{end}}&state={{$.State}}">{{$.SignedUser.Name}}/{{.Name}} <strong class="ui right">{{if $.IsShowClosed}}{{.NumClosedIssues}}{{else}}{{.NumOpenIssues}}{{end}}</strong></a> | |||||
| {{end}} | |||||
| </div> | |||||
| </div> | |||||
| <div class="twelve wide column content"> | |||||
| <div class="ui tiny buttons"> | |||||
| <a class="ui green basic button {{if not .IsShowClosed}}active{{end}}" href="{{.Link}}?type={{$.ViewType}}&repo={{.RepoID}}&state=open"> | |||||
| <i class="octicon octicon-issue-opened"></i> | |||||
| {{.i18n.Tr "repo.issues.open_tab" .IssueStats.OpenCount}} | |||||
| </a> | |||||
| <a class="ui red basic button {{if .IsShowClosed}}active{{end}}" href="{{.Link}}?type={{$.ViewType}}&repo={{.RepoID}}&state=closed"> | |||||
| <i class="octicon octicon-issue-closed"></i> | |||||
| {{.i18n.Tr "repo.issues.close_tab" .IssueStats.ClosedCount}} | |||||
| </a> | |||||
| </div> | |||||
| <div class="issue list"> | |||||
| {{range .Issues}} | |||||
| {{ $timeStr:= TimeSince .Created $.Lang }} | |||||
| <li class="item"> | |||||
| <div class="ui label">#{{.ID}}</div> | |||||
| <a class="title" href="{{AppSubUrl}}/{{.Repo.Owner.Name}}/{{.Repo.Name}}/issues/{{.Index}}">{{.Name}}</a> | |||||
| {{if .NumComments}} | |||||
| <span class="comment ui right"><i class="octicon octicon-comment"></i> {{.NumComments}}</span> | |||||
| {{end}} | |||||
| <p class="desc"> | |||||
| {{$.i18n.Tr "repo.issues.opened_by" $timeStr .Poster.Name | Safe}} | |||||
| {{if .Assignee}} | |||||
| <a class="ui right assignee poping up" href="{{.Assignee.HomeLink}}" data-content="{{.Assignee.Name}}" data-variation="inverted" data-position="left center"> | |||||
| <img class="ui avatar image" src="{{.Assignee.AvatarLink}}"> | |||||
| </a> | |||||
| {{end}} | |||||
| </p> | |||||
| </li> | |||||
| {{end}} | |||||
| {{with .Page}} | |||||
| {{if gt .TotalPages 1}} | |||||
| <div class="center page buttons"> | |||||
| <div class="ui borderless pagination menu"> | |||||
| <a class="{{if not .HasPrevious}}disabled{{end}} item" {{if .HasPrevious}}href="{{$.Link}}?type={{$.ViewType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}&page={{.Previous}}"{{end}}> | |||||
| <i class="left arrow icon"></i> {{$.i18n.Tr "repo.issues.previous"}} | |||||
| </a> | |||||
| {{range .Pages}} | |||||
| {{if eq .Num -1}} | |||||
| <a class="disabled item">...</a> | |||||
| {{else}} | |||||
| <a class="{{if .IsCurrent}}active{{end}} item" {{if not .IsCurrent}}href="{{$.Link}}?type={{$.ViewType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}&page={{.Num}}"{{end}}>{{.Num}}</a> | |||||
| {{end}} | |||||
| {{end}} | |||||
| <a class="{{if not .HasNext}}disabled{{end}} item" {{if .HasNext}}href="{{$.Link}}?type={{$.ViewType}}&state={{$.State}}&labels={{$.SelectLabels}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}&page={{.Next}}"{{end}}> | |||||
| {{$.i18n.Tr "repo.issues.next"}} <i class="icon right arrow"></i> | |||||
| </a> | |||||
| </div> | |||||
| </div> | |||||
| {{end}} | |||||
| {{end}} | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| {{template "base/footer" .}} | |||||
| @@ -0,0 +1,30 @@ | |||||
| <div class="ui container"> | |||||
| <div class="ui floating dropdown link jump"> | |||||
| <span class="text"> | |||||
| <img class="ui avatar image" src="{{.ContextUser.AvatarLink}}"> | |||||
| {{.ContextUser.Name}} | |||||
| <i class="dropdown icon"></i> | |||||
| </span> | |||||
| <div class="context user menu" tabindex="-1"> | |||||
| <div class="ui header"> | |||||
| {{.i18n.Tr "home.switch_dashboard_context"}} | |||||
| </div> | |||||
| <a class="{{if eq .ContextUser.Id .SignedUser.Id}}active selected{{end}} item" href="{{AppSubUrl}}/issues"> | |||||
| <img class="ui image" src="{{.SignedUser.AvatarLink}}"> | |||||
| {{.SignedUser.Name}} | |||||
| </a> | |||||
| {{range .Orgs}} | |||||
| {{if .IsOwnedBy $.SignedUser.Id}} | |||||
| <a class="{{if eq $.ContextUser.Id .Id}}active selected{{end}} item" href="{{AppSubUrl}}/org/{{.Name}}/issues"> | |||||
| <img class="ui image" src="{{.AvatarLink}}"> | |||||
| {{.Name}} | |||||
| </a> | |||||
| {{end}} | |||||
| {{end}} | |||||
| <a class="item" href="{{AppSubUrl}}/org/create"> | |||||
| <i class="octicon octicon-repo-create"></i> {{.i18n.Tr "new_org"}} | |||||
| </a> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <div class="ui divider"></div> | |||||
| @@ -1,44 +0,0 @@ | |||||
| {{template "ng/base/head" .}} | |||||
| {{template "ng/base/header" .}} | |||||
| {{template "user/dashboard/nav" .}} | |||||
| <div id="dashboard-wrapper"> | |||||
| <div id="dashboard" class="container" data-page="user"> | |||||
| {{if .HasInfo}}<div class="alert alert-info">{{.InfoMsg}}</div>{{end}} | |||||
| <div id="issue"> | |||||
| <div class="left grid-1-5 filter-list"> | |||||
| <ul class="list-unstyled menu menu-vertical"> | |||||
| <li><a href="{{AppSubUrl}}/issues?state={{.State}}&repoid={{.RepoId}}" class="radius{{if eq .ViewType "all"}} active{{end}}" >In your repositories <strong class="pull-right">{{.IssueStats.AllCount}}</strong></a></li> | |||||
| <li><a href="{{AppSubUrl}}/issues?type=assigned&repoid={{.RepoId}}&state={{.State}}" class="radius{{if eq .ViewType "assigned"}} active{{end}}">Assigned to you <strong class="pull-right">{{.IssueStats.AssignCount}}</strong></a></li> | |||||
| <li><a href="{{AppSubUrl}}/issues?type=created_by&repoid={{.RepoId}}&state={{.State}}" class="radius{{if eq .ViewType "created_by"}} active{{end}}">Created by you <strong class="pull-right">{{.IssueStats.CreateCount}}</strong></a></li> | |||||
| <li><hr/></li> | |||||
| {{range .Repos}} | |||||
| <li><a href="{{AppSubUrl}}/issues?type={{$.ViewType}}{{if eq $.RepoId .ID}}{{else}}&repoid={{.ID}}{{end}}&state={{$.State}}" class="radius{{if eq $.RepoId .ID}} active{{end}}">{{$.SignedUser.Name}}/{{.Name}} <strong class="pull-right">{{if $.IsShowClosed}}{{.NumClosedIssues}}{{else}}{{.NumOpenIssues}}{{end}}</strong></a></li> | |||||
| {{end}} | |||||
| </ul> | |||||
| </div> | |||||
| <div class="right grid-3-4"> | |||||
| <div class="filter-option"> | |||||
| <div class="btn-group"> | |||||
| <a class="btn btn-white btn-small issue-open{{if not .IsShowClosed}} active{{end}}" href="{{AppSubUrl}}/issues?type={{.ViewType}}&repoid={{.RepoId}}">Open</a> | |||||
| <a class="btn btn-white btn-small issue-close{{if .IsShowClosed}} active{{end}}" href="{{AppSubUrl}}/issues?type={{.ViewType}}&repoid={{.RepoId}}&state=closed">Closed</a> | |||||
| </div> | |||||
| </div> | |||||
| <div class="issues list-group"> | |||||
| {{range .Issues}}{{if .}} | |||||
| <div class="list-group-item issue-item" id="issue-{{.ID}}" onclick="window.location.href='{{AppSubUrl}}/{{.Repo.Owner.Name}}/{{.Repo.Name}}/issues/{{.Index}}'"> | |||||
| <span class="number pull-right">#{{.Index}}</span> | |||||
| <h5 class="title"><a href="{{AppSubUrl}}/{{.Repo.Owner.Name}}/{{.Repo.Name}}/issues/{{.Index}}">{{.Name}}</a></h5> | |||||
| <p class="info"> | |||||
| <span class="author"><img class="avatar" src="{{.Poster.AvatarLink}}" alt="" width="20"/> | |||||
| <a href="{{AppSubUrl}}/{{.Poster.Name}}">{{.Poster.Name}}</a></span> | |||||
| <span class="time">{{TimeSince .Created $.Lang}}</span> | |||||
| <span class="comment"><i class="fa fa-comments"></i> {{.NumComments}}</span> | |||||
| </p> | |||||
| </div> | |||||
| {{end}}{{end}} | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| {{template "ng/base/footer" .}} | |||||