mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-21 12:44:49 -05:00
Add /milestones endpoint (#8733)
Create a /milestones endpoint which basically serves as a dashboard view for milestones, very similar to the /issues or /pulls page. Closes #8232
This commit is contained in:
parent
7cc16740a5
commit
f6b29012e0
14 changed files with 568 additions and 7 deletions
|
@ -511,6 +511,8 @@ DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME = true
|
||||||
NO_REPLY_ADDRESS = noreply.%(DOMAIN)s
|
NO_REPLY_ADDRESS = noreply.%(DOMAIN)s
|
||||||
; Show Registration button
|
; Show Registration button
|
||||||
SHOW_REGISTRATION_BUTTON = true
|
SHOW_REGISTRATION_BUTTON = true
|
||||||
|
; Show milestones dashboard page - a view of all the user's milestones
|
||||||
|
SHOW_MILESTONES_DASHBOARD_PAGE = true
|
||||||
; Default value for AutoWatchNewRepos
|
; Default value for AutoWatchNewRepos
|
||||||
; When adding a repo to a team or creating a new repo all team members will watch the
|
; When adding a repo to a team or creating a new repo all team members will watch the
|
||||||
; repo automatically if enabled
|
; repo automatically if enabled
|
||||||
|
|
|
@ -310,6 +310,7 @@ relation to port exhaustion.
|
||||||
- `EMAIL_DOMAIN_WHITELIST`: **\<empty\>**: If non-empty, list of domain names that can only be used to register
|
- `EMAIL_DOMAIN_WHITELIST`: **\<empty\>**: If non-empty, list of domain names that can only be used to register
|
||||||
on this instance.
|
on this instance.
|
||||||
- `SHOW_REGISTRATION_BUTTON`: **! DISABLE\_REGISTRATION**: Show Registration Button
|
- `SHOW_REGISTRATION_BUTTON`: **! DISABLE\_REGISTRATION**: Show Registration Button
|
||||||
|
- `SHOW_MILESTONES_DASHBOARD_PAGE`: **true** Enable this to show the milestones dashboard page - a view of all the user's milestones
|
||||||
- `AUTO_WATCH_NEW_REPOS`: **true**: Enable this to let all organisation users watch new repos when they are created
|
- `AUTO_WATCH_NEW_REPOS`: **true**: Enable this to let all organisation users watch new repos when they are created
|
||||||
- `AUTO_WATCH_ON_CHANGES`: **false**: Enable this to make users watch a repository after their first commit to it
|
- `AUTO_WATCH_ON_CHANGES`: **false**: Enable this to make users watch a repository after their first commit to it
|
||||||
- `DEFAULT_ORG_VISIBILITY`: **public**: Set default visibility mode for organisations, either "public", "limited" or "private".
|
- `DEFAULT_ORG_VISIBILITY`: **public**: Set default visibility mode for organisations, either "public", "limited" or "private".
|
||||||
|
|
|
@ -86,6 +86,12 @@ func testLinksAsUser(userName string, t *testing.T) {
|
||||||
"/pulls?type=your_repositories&repos=[0]&sort=&state=closed",
|
"/pulls?type=your_repositories&repos=[0]&sort=&state=closed",
|
||||||
"/pulls?type=assigned&repos=[0]&sort=&state=closed",
|
"/pulls?type=assigned&repos=[0]&sort=&state=closed",
|
||||||
"/pulls?type=created_by&repos=[0]&sort=&state=closed",
|
"/pulls?type=created_by&repos=[0]&sort=&state=closed",
|
||||||
|
"/milestones",
|
||||||
|
"/milestones?sort=mostcomplete&state=closed",
|
||||||
|
"/milestones?type=your_repositories&sort=mostcomplete&state=closed",
|
||||||
|
"/milestones?sort=&repos=[1]&state=closed",
|
||||||
|
"/milestones?sort=&repos=[1]&state=open",
|
||||||
|
"/milestones?repos=[0]&sort=mostissues&state=open",
|
||||||
"/notifications",
|
"/notifications",
|
||||||
"/repo/create",
|
"/repo/create",
|
||||||
"/repo/migrate",
|
"/repo/migrate",
|
||||||
|
|
|
@ -17,8 +17,9 @@ import (
|
||||||
|
|
||||||
// Milestone represents a milestone of repository.
|
// Milestone represents a milestone of repository.
|
||||||
type Milestone struct {
|
type Milestone struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
RepoID int64 `xorm:"INDEX"`
|
RepoID int64 `xorm:"INDEX"`
|
||||||
|
Repo *Repository `xorm:"-"`
|
||||||
Name string
|
Name string
|
||||||
Content string `xorm:"TEXT"`
|
Content string `xorm:"TEXT"`
|
||||||
RenderedContent string `xorm:"-"`
|
RenderedContent string `xorm:"-"`
|
||||||
|
@ -177,11 +178,38 @@ func (milestones MilestoneList) loadTotalTrackedTimes(e Engine) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Milestone) loadTotalTrackedTime(e Engine) error {
|
||||||
|
type totalTimesByMilestone struct {
|
||||||
|
MilestoneID int64
|
||||||
|
Time int64
|
||||||
|
}
|
||||||
|
totalTime := &totalTimesByMilestone{MilestoneID: m.ID}
|
||||||
|
has, err := e.Table("issue").
|
||||||
|
Join("INNER", "milestone", "issue.milestone_id = milestone.id").
|
||||||
|
Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id").
|
||||||
|
Select("milestone_id, sum(time) as time").
|
||||||
|
Where("milestone_id = ?", m.ID).
|
||||||
|
GroupBy("milestone_id").
|
||||||
|
Get(totalTime)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !has {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
m.TotalTrackedTime = totalTime.Time
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request
|
// LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request
|
||||||
func (milestones MilestoneList) LoadTotalTrackedTimes() error {
|
func (milestones MilestoneList) LoadTotalTrackedTimes() error {
|
||||||
return milestones.loadTotalTrackedTimes(x)
|
return milestones.loadTotalTrackedTimes(x)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadTotalTrackedTime loads the tracked time for the milestone
|
||||||
|
func (m *Milestone) LoadTotalTrackedTime() error {
|
||||||
|
return m.loadTotalTrackedTime(x)
|
||||||
|
}
|
||||||
|
|
||||||
func (milestones MilestoneList) getMilestoneIDs() []int64 {
|
func (milestones MilestoneList) getMilestoneIDs() []int64 {
|
||||||
var ids = make([]int64, 0, len(milestones))
|
var ids = make([]int64, 0, len(milestones))
|
||||||
for _, ms := range milestones {
|
for _, ms := range milestones {
|
||||||
|
@ -465,3 +493,78 @@ func DeleteMilestoneByRepoID(repoID, id int64) error {
|
||||||
}
|
}
|
||||||
return sess.Commit()
|
return sess.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CountMilestonesByRepoIDs map from repoIDs to number of milestones matching the options`
|
||||||
|
func CountMilestonesByRepoIDs(repoIDs []int64, isClosed bool) (map[int64]int64, error) {
|
||||||
|
sess := x.Where("is_closed = ?", isClosed)
|
||||||
|
sess.In("repo_id", repoIDs)
|
||||||
|
|
||||||
|
countsSlice := make([]*struct {
|
||||||
|
RepoID int64
|
||||||
|
Count int64
|
||||||
|
}, 0, 10)
|
||||||
|
if err := sess.GroupBy("repo_id").
|
||||||
|
Select("repo_id AS repo_id, COUNT(*) AS count").
|
||||||
|
Table("milestone").
|
||||||
|
Find(&countsSlice); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
countMap := make(map[int64]int64, len(countsSlice))
|
||||||
|
for _, c := range countsSlice {
|
||||||
|
countMap[c.RepoID] = c.Count
|
||||||
|
}
|
||||||
|
return countMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMilestonesByRepoIDs returns a list of milestones of given repositories and status.
|
||||||
|
func GetMilestonesByRepoIDs(repoIDs []int64, page int, isClosed bool, sortType string) (MilestoneList, error) {
|
||||||
|
miles := make([]*Milestone, 0, setting.UI.IssuePagingNum)
|
||||||
|
sess := x.Where("is_closed = ?", isClosed)
|
||||||
|
sess.In("repo_id", repoIDs)
|
||||||
|
if page > 0 {
|
||||||
|
sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch sortType {
|
||||||
|
case "furthestduedate":
|
||||||
|
sess.Desc("deadline_unix")
|
||||||
|
case "leastcomplete":
|
||||||
|
sess.Asc("completeness")
|
||||||
|
case "mostcomplete":
|
||||||
|
sess.Desc("completeness")
|
||||||
|
case "leastissues":
|
||||||
|
sess.Asc("num_issues")
|
||||||
|
case "mostissues":
|
||||||
|
sess.Desc("num_issues")
|
||||||
|
default:
|
||||||
|
sess.Asc("deadline_unix")
|
||||||
|
}
|
||||||
|
return miles, sess.Find(&miles)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MilestonesStats represents milestone statistic information.
|
||||||
|
type MilestonesStats struct {
|
||||||
|
OpenCount, ClosedCount int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMilestonesStats returns milestone statistic information for dashboard by given conditions.
|
||||||
|
func GetMilestonesStats(userRepoIDs []int64) (*MilestonesStats, error) {
|
||||||
|
var err error
|
||||||
|
stats := &MilestonesStats{}
|
||||||
|
|
||||||
|
stats.OpenCount, err = x.Where("is_closed = ?", false).
|
||||||
|
And(builder.In("repo_id", userRepoIDs)).
|
||||||
|
Count(new(Milestone))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stats.ClosedCount, err = x.Where("is_closed = ?", true).
|
||||||
|
And(builder.In("repo_id", userRepoIDs)).
|
||||||
|
Count(new(Milestone))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|
|
@ -289,3 +289,88 @@ func TestMilestoneList_LoadTotalTrackedTimes(t *testing.T) {
|
||||||
|
|
||||||
assert.Equal(t, miles[0].TotalTrackedTime, int64(3662))
|
assert.Equal(t, miles[0].TotalTrackedTime, int64(3662))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCountMilestonesByRepoIDs(t *testing.T) {
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
milestonesCount := func(repoID int64) (int, int) {
|
||||||
|
repo := AssertExistsAndLoadBean(t, &Repository{ID: repoID}).(*Repository)
|
||||||
|
return repo.NumOpenMilestones, repo.NumClosedMilestones
|
||||||
|
}
|
||||||
|
repo1OpenCount, repo1ClosedCount := milestonesCount(1)
|
||||||
|
repo2OpenCount, repo2ClosedCount := milestonesCount(2)
|
||||||
|
|
||||||
|
openCounts, err := CountMilestonesByRepoIDs([]int64{1, 2}, false)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, repo1OpenCount, openCounts[1])
|
||||||
|
assert.EqualValues(t, repo2OpenCount, openCounts[2])
|
||||||
|
|
||||||
|
closedCounts, err := CountMilestonesByRepoIDs([]int64{1, 2}, true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, repo1ClosedCount, closedCounts[1])
|
||||||
|
assert.EqualValues(t, repo2ClosedCount, closedCounts[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetMilestonesByRepoIDs(t *testing.T) {
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
|
||||||
|
repo2 := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository)
|
||||||
|
test := func(sortType string, sortCond func(*Milestone) int) {
|
||||||
|
for _, page := range []int{0, 1} {
|
||||||
|
openMilestones, err := GetMilestonesByRepoIDs([]int64{repo1.ID, repo2.ID}, page, false, sortType)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, openMilestones, repo1.NumOpenMilestones+repo2.NumOpenMilestones)
|
||||||
|
values := make([]int, len(openMilestones))
|
||||||
|
for i, milestone := range openMilestones {
|
||||||
|
values[i] = sortCond(milestone)
|
||||||
|
}
|
||||||
|
assert.True(t, sort.IntsAreSorted(values))
|
||||||
|
|
||||||
|
closedMilestones, err := GetMilestonesByRepoIDs([]int64{repo1.ID, repo2.ID}, page, true, sortType)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, closedMilestones, repo1.NumClosedMilestones+repo2.NumClosedMilestones)
|
||||||
|
values = make([]int, len(closedMilestones))
|
||||||
|
for i, milestone := range closedMilestones {
|
||||||
|
values[i] = sortCond(milestone)
|
||||||
|
}
|
||||||
|
assert.True(t, sort.IntsAreSorted(values))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
test("furthestduedate", func(milestone *Milestone) int {
|
||||||
|
return -int(milestone.DeadlineUnix)
|
||||||
|
})
|
||||||
|
test("leastcomplete", func(milestone *Milestone) int {
|
||||||
|
return milestone.Completeness
|
||||||
|
})
|
||||||
|
test("mostcomplete", func(milestone *Milestone) int {
|
||||||
|
return -milestone.Completeness
|
||||||
|
})
|
||||||
|
test("leastissues", func(milestone *Milestone) int {
|
||||||
|
return milestone.NumIssues
|
||||||
|
})
|
||||||
|
test("mostissues", func(milestone *Milestone) int {
|
||||||
|
return -milestone.NumIssues
|
||||||
|
})
|
||||||
|
test("soonestduedate", func(milestone *Milestone) int {
|
||||||
|
return int(milestone.DeadlineUnix)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadTotalTrackedTime(t *testing.T) {
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
milestone := AssertExistsAndLoadBean(t, &Milestone{ID: 1}).(*Milestone)
|
||||||
|
|
||||||
|
assert.NoError(t, milestone.LoadTotalTrackedTime())
|
||||||
|
|
||||||
|
assert.Equal(t, milestone.TotalTrackedTime, int64(3662))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetMilestonesStats(t *testing.T) {
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
|
||||||
|
repo2 := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository)
|
||||||
|
|
||||||
|
milestoneStats, err := GetMilestonesStats([]int64{repo1.ID, repo2.ID})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, repo1.NumOpenMilestones+repo2.NumOpenMilestones, milestoneStats.OpenCount)
|
||||||
|
assert.EqualValues(t, repo1.NumClosedMilestones+repo2.NumClosedMilestones, milestoneStats.ClosedCount)
|
||||||
|
}
|
||||||
|
|
|
@ -334,6 +334,7 @@ func Contexter() macaron.Handler {
|
||||||
ctx.Data["IsLandingPageOrganizations"] = setting.LandingPageURL == setting.LandingPageOrganizations
|
ctx.Data["IsLandingPageOrganizations"] = setting.LandingPageURL == setting.LandingPageOrganizations
|
||||||
|
|
||||||
ctx.Data["ShowRegistrationButton"] = setting.Service.ShowRegistrationButton
|
ctx.Data["ShowRegistrationButton"] = setting.Service.ShowRegistrationButton
|
||||||
|
ctx.Data["ShowMilestonesDashboardPage"] = setting.Service.ShowMilestonesDashboardPage
|
||||||
ctx.Data["ShowFooterBranding"] = setting.ShowFooterBranding
|
ctx.Data["ShowFooterBranding"] = setting.ShowFooterBranding
|
||||||
ctx.Data["ShowFooterVersion"] = setting.ShowFooterVersion
|
ctx.Data["ShowFooterVersion"] = setting.ShowFooterVersion
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ var Service struct {
|
||||||
DisableRegistration bool
|
DisableRegistration bool
|
||||||
AllowOnlyExternalRegistration bool
|
AllowOnlyExternalRegistration bool
|
||||||
ShowRegistrationButton bool
|
ShowRegistrationButton bool
|
||||||
|
ShowMilestonesDashboardPage bool
|
||||||
RequireSignInView bool
|
RequireSignInView bool
|
||||||
EnableNotifyMail bool
|
EnableNotifyMail bool
|
||||||
EnableBasicAuth bool
|
EnableBasicAuth bool
|
||||||
|
@ -62,6 +63,7 @@ func newService() {
|
||||||
Service.AllowOnlyExternalRegistration = sec.Key("ALLOW_ONLY_EXTERNAL_REGISTRATION").MustBool()
|
Service.AllowOnlyExternalRegistration = sec.Key("ALLOW_ONLY_EXTERNAL_REGISTRATION").MustBool()
|
||||||
Service.EmailDomainWhitelist = sec.Key("EMAIL_DOMAIN_WHITELIST").Strings(",")
|
Service.EmailDomainWhitelist = sec.Key("EMAIL_DOMAIN_WHITELIST").Strings(",")
|
||||||
Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration))
|
Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration))
|
||||||
|
Service.ShowMilestonesDashboardPage = sec.Key("SHOW_MILESTONES_DASHBOARD_PAGE").MustBool(true)
|
||||||
Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool()
|
Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool()
|
||||||
Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true)
|
Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true)
|
||||||
Service.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool()
|
Service.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool()
|
||||||
|
|
|
@ -66,6 +66,7 @@ forks = Forks
|
||||||
activities = Activities
|
activities = Activities
|
||||||
pull_requests = Pull Requests
|
pull_requests = Pull Requests
|
||||||
issues = Issues
|
issues = Issues
|
||||||
|
milestones = Milestones
|
||||||
|
|
||||||
cancel = Cancel
|
cancel = Cancel
|
||||||
add = Add
|
add = Add
|
||||||
|
|
|
@ -254,6 +254,13 @@ func RegisterRoutes(m *macaron.Macaron) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reqMilestonesDashboardPageEnabled := func(ctx *context.Context) {
|
||||||
|
if !setting.Service.ShowMilestonesDashboardPage {
|
||||||
|
ctx.Error(403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m.Use(user.GetNotificationCount)
|
m.Use(user.GetNotificationCount)
|
||||||
|
|
||||||
// FIXME: not all routes need go through same middlewares.
|
// FIXME: not all routes need go through same middlewares.
|
||||||
|
@ -276,6 +283,7 @@ func RegisterRoutes(m *macaron.Macaron) {
|
||||||
m.Combo("/install", routers.InstallInit).Get(routers.Install).
|
m.Combo("/install", routers.InstallInit).Get(routers.Install).
|
||||||
Post(bindIgnErr(auth.InstallForm{}), routers.InstallPost)
|
Post(bindIgnErr(auth.InstallForm{}), routers.InstallPost)
|
||||||
m.Get("/^:type(issues|pulls)$", reqSignIn, user.Issues)
|
m.Get("/^:type(issues|pulls)$", reqSignIn, user.Issues)
|
||||||
|
m.Get("/milestones", reqSignIn, reqMilestonesDashboardPageEnabled, user.Milestones)
|
||||||
|
|
||||||
// ***** START: User *****
|
// ***** START: User *****
|
||||||
m.Group("/user", func() {
|
m.Group("/user", func() {
|
||||||
|
@ -556,6 +564,7 @@ func RegisterRoutes(m *macaron.Macaron) {
|
||||||
m.Group("/:org", func() {
|
m.Group("/:org", func() {
|
||||||
m.Get("/dashboard", user.Dashboard)
|
m.Get("/dashboard", user.Dashboard)
|
||||||
m.Get("/^:type(issues|pulls)$", user.Issues)
|
m.Get("/^:type(issues|pulls)$", user.Issues)
|
||||||
|
m.Get("/milestones", reqMilestonesDashboardPageEnabled, user.Milestones)
|
||||||
m.Get("/members", org.Members)
|
m.Get("/members", org.Members)
|
||||||
m.Get("/members/action/:action", org.MembersAction)
|
m.Get("/members/action/:action", org.MembersAction)
|
||||||
|
|
||||||
|
|
|
@ -18,17 +18,20 @@ import (
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/markup/markdown"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/keybase/go-crypto/openpgp"
|
"github.com/keybase/go-crypto/openpgp"
|
||||||
"github.com/keybase/go-crypto/openpgp/armor"
|
"github.com/keybase/go-crypto/openpgp/armor"
|
||||||
|
"github.com/unknwon/com"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
tplDashboard base.TplName = "user/dashboard/dashboard"
|
tplDashboard base.TplName = "user/dashboard/dashboard"
|
||||||
tplIssues base.TplName = "user/dashboard/issues"
|
tplIssues base.TplName = "user/dashboard/issues"
|
||||||
tplProfile base.TplName = "user/profile"
|
tplMilestones base.TplName = "user/dashboard/milestones"
|
||||||
|
tplProfile base.TplName = "user/profile"
|
||||||
)
|
)
|
||||||
|
|
||||||
// getDashboardContextUser finds out dashboard is viewing as which context user.
|
// getDashboardContextUser finds out dashboard is viewing as which context user.
|
||||||
|
@ -150,6 +153,190 @@ func Dashboard(ctx *context.Context) {
|
||||||
ctx.HTML(200, tplDashboard)
|
ctx.HTML(200, tplDashboard)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Milestones render the user milestones page
|
||||||
|
func Milestones(ctx *context.Context) {
|
||||||
|
ctx.Data["Title"] = ctx.Tr("milestones")
|
||||||
|
ctx.Data["PageIsMilestonesDashboard"] = true
|
||||||
|
|
||||||
|
ctxUser := getDashboardContextUser(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sortType := ctx.Query("sort")
|
||||||
|
page := ctx.QueryInt("page")
|
||||||
|
if page <= 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
reposQuery := ctx.Query("repos")
|
||||||
|
isShowClosed := ctx.Query("state") == "closed"
|
||||||
|
|
||||||
|
// Get repositories.
|
||||||
|
var err error
|
||||||
|
var userRepoIDs []int64
|
||||||
|
if ctxUser.IsOrganization() {
|
||||||
|
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 {
|
||||||
|
ctx.ServerError("env.RepoIDs", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unitType := models.UnitTypeIssues
|
||||||
|
userRepoIDs, err = ctxUser.GetAccessRepoIDs(unitType)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("ctxUser.GetAccessRepoIDs", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(userRepoIDs) == 0 {
|
||||||
|
userRepoIDs = []int64{-1}
|
||||||
|
}
|
||||||
|
|
||||||
|
var repoIDs []int64
|
||||||
|
if issueReposQueryPattern.MatchString(reposQuery) {
|
||||||
|
// remove "[" and "]" from string
|
||||||
|
reposQuery = reposQuery[1 : len(reposQuery)-1]
|
||||||
|
//for each ID (delimiter ",") add to int to repoIDs
|
||||||
|
reposSet := false
|
||||||
|
for _, rID := range strings.Split(reposQuery, ",") {
|
||||||
|
// Ensure nonempty string entries
|
||||||
|
if rID != "" && rID != "0" {
|
||||||
|
reposSet = true
|
||||||
|
rIDint64, err := strconv.ParseInt(rID, 10, 64)
|
||||||
|
if err == nil && com.IsSliceContainsInt64(userRepoIDs, rIDint64) {
|
||||||
|
repoIDs = append(repoIDs, rIDint64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if reposSet && len(repoIDs) == 0 {
|
||||||
|
// force an empty result
|
||||||
|
repoIDs = []int64{-1}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Error("issueReposQueryPattern not match with query")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(repoIDs) == 0 {
|
||||||
|
repoIDs = userRepoIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
counts, err := models.CountMilestonesByRepoIDs(userRepoIDs, isShowClosed)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("CountMilestonesByRepoIDs", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
milestones, err := models.GetMilestonesByRepoIDs(repoIDs, page, isShowClosed, sortType)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetMilestonesByRepoIDs", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showReposMap := make(map[int64]*models.Repository, len(counts))
|
||||||
|
for rID := range counts {
|
||||||
|
if rID == -1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
repo, err := models.GetRepositoryByID(rID)
|
||||||
|
if err != nil {
|
||||||
|
if models.IsErrRepoNotExist(err) {
|
||||||
|
ctx.NotFound("GetRepositoryByID", err)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
ctx.ServerError("GetRepositoryByID", fmt.Errorf("[%d]%v", rID, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showReposMap[rID] = repo
|
||||||
|
|
||||||
|
// Check if user has access to given repository.
|
||||||
|
perm, err := models.GetUserRepoPermission(repo, ctxUser)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetUserRepoPermission", fmt.Errorf("[%d]%v", rID, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !perm.CanRead(models.UnitTypeIssues) {
|
||||||
|
if log.IsTrace() {
|
||||||
|
log.Trace("Permission Denied: User %-v cannot read %-v of repo %-v\n"+
|
||||||
|
"User in repo has Permissions: %-+v",
|
||||||
|
ctxUser,
|
||||||
|
models.UnitTypeIssues,
|
||||||
|
repo,
|
||||||
|
perm)
|
||||||
|
}
|
||||||
|
ctx.Status(404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showRepos := models.RepositoryListOfMap(showReposMap)
|
||||||
|
sort.Sort(showRepos)
|
||||||
|
if err = showRepos.LoadAttributes(); err != nil {
|
||||||
|
ctx.ServerError("LoadAttributes", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range milestones {
|
||||||
|
m.Repo = showReposMap[m.RepoID]
|
||||||
|
m.RenderedContent = string(markdown.Render([]byte(m.Content), m.Repo.Link(), m.Repo.ComposeMetas()))
|
||||||
|
if m.Repo.IsTimetrackerEnabled() {
|
||||||
|
err := m.LoadTotalTrackedTime()
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("LoadTotalTrackedTime", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
milestoneStats, err := models.GetMilestonesStats(repoIDs)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetMilestoneStats", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
totalMilestoneStats, err := models.GetMilestonesStats(userRepoIDs)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetMilestoneStats", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var pagerCount int
|
||||||
|
if isShowClosed {
|
||||||
|
ctx.Data["State"] = "closed"
|
||||||
|
ctx.Data["Total"] = totalMilestoneStats.ClosedCount
|
||||||
|
pagerCount = int(milestoneStats.ClosedCount)
|
||||||
|
} else {
|
||||||
|
ctx.Data["State"] = "open"
|
||||||
|
ctx.Data["Total"] = totalMilestoneStats.OpenCount
|
||||||
|
pagerCount = int(milestoneStats.OpenCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["Milestones"] = milestones
|
||||||
|
ctx.Data["Repos"] = showRepos
|
||||||
|
ctx.Data["Counts"] = counts
|
||||||
|
ctx.Data["MilestoneStats"] = milestoneStats
|
||||||
|
ctx.Data["SortType"] = sortType
|
||||||
|
if len(repoIDs) != len(userRepoIDs) {
|
||||||
|
ctx.Data["RepoIDs"] = repoIDs
|
||||||
|
}
|
||||||
|
ctx.Data["IsShowClosed"] = isShowClosed
|
||||||
|
|
||||||
|
pager := context.NewPagination(pagerCount, setting.UI.IssuePagingNum, page, 5)
|
||||||
|
pager.AddParam(ctx, "repos", "RepoIDs")
|
||||||
|
pager.AddParam(ctx, "sort", "SortType")
|
||||||
|
pager.AddParam(ctx, "state", "State")
|
||||||
|
ctx.Data["Page"] = pager
|
||||||
|
|
||||||
|
ctx.HTML(200, tplMilestones)
|
||||||
|
}
|
||||||
|
|
||||||
// Regexp for repos query
|
// Regexp for repos query
|
||||||
var issueReposQueryPattern = regexp.MustCompile(`^\[\d+(,\d+)*,?\]$`)
|
var issueReposQueryPattern = regexp.MustCompile(`^\[\d+(,\d+)*,?\]$`)
|
||||||
|
|
||||||
|
|
|
@ -31,3 +31,42 @@ func TestIssues(t *testing.T) {
|
||||||
assert.Len(t, ctx.Data["Issues"], 1)
|
assert.Len(t, ctx.Data["Issues"], 1)
|
||||||
assert.Len(t, ctx.Data["Repos"], 1)
|
assert.Len(t, ctx.Data["Repos"], 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMilestones(t *testing.T) {
|
||||||
|
setting.UI.IssuePagingNum = 1
|
||||||
|
assert.NoError(t, models.LoadFixtures())
|
||||||
|
|
||||||
|
ctx := test.MockContext(t, "milestones")
|
||||||
|
test.LoadUser(t, ctx, 2)
|
||||||
|
ctx.SetParams("sort", "issues")
|
||||||
|
ctx.Req.Form.Set("state", "closed")
|
||||||
|
ctx.Req.Form.Set("sort", "furthestduedate")
|
||||||
|
Milestones(ctx)
|
||||||
|
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
|
||||||
|
assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"])
|
||||||
|
assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
|
||||||
|
assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"])
|
||||||
|
assert.EqualValues(t, 1, ctx.Data["Total"])
|
||||||
|
assert.Len(t, ctx.Data["Milestones"], 1)
|
||||||
|
assert.Len(t, ctx.Data["Repos"], 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMilestonesForSpecificRepo(t *testing.T) {
|
||||||
|
setting.UI.IssuePagingNum = 1
|
||||||
|
assert.NoError(t, models.LoadFixtures())
|
||||||
|
|
||||||
|
ctx := test.MockContext(t, "milestones")
|
||||||
|
test.LoadUser(t, ctx, 2)
|
||||||
|
ctx.SetParams("sort", "issues")
|
||||||
|
ctx.SetParams("repo", "1")
|
||||||
|
ctx.Req.Form.Set("state", "closed")
|
||||||
|
ctx.Req.Form.Set("sort", "furthestduedate")
|
||||||
|
Milestones(ctx)
|
||||||
|
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status())
|
||||||
|
assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"])
|
||||||
|
assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
|
||||||
|
assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"])
|
||||||
|
assert.EqualValues(t, 1, ctx.Data["Total"])
|
||||||
|
assert.Len(t, ctx.Data["Milestones"], 1)
|
||||||
|
assert.Len(t, ctx.Data["Repos"], 1)
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
<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>
|
<a class="item {{if .PageIsIssues}}active{{end}}" href="{{AppSubUrl}}/issues">{{.i18n.Tr "issues"}}</a>
|
||||||
<a class="item {{if .PageIsPulls}}active{{end}}" href="{{AppSubUrl}}/pulls">{{.i18n.Tr "pull_requests"}}</a>
|
<a class="item {{if .PageIsPulls}}active{{end}}" href="{{AppSubUrl}}/pulls">{{.i18n.Tr "pull_requests"}}</a>
|
||||||
|
{{if .ShowMilestonesDashboardPage}}<a class="item {{if .PageIsMilestonesDashboard}}active{{end}}" href="{{AppSubUrl}}/milestones">{{.i18n.Tr "milestones"}}</a>{{end}}
|
||||||
<a class="item {{if .PageIsExplore}}active{{end}}" href="{{AppSubUrl}}/explore/repos">{{.i18n.Tr "explore"}}</a>
|
<a class="item {{if .PageIsExplore}}active{{end}}" href="{{AppSubUrl}}/explore/repos">{{.i18n.Tr "explore"}}</a>
|
||||||
{{else if .IsLandingPageHome}}
|
{{else if .IsLandingPageHome}}
|
||||||
<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>
|
||||||
|
|
119
templates/user/dashboard/milestones.tmpl
Normal file
119
templates/user/dashboard/milestones.tmpl
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
{{template "base/head" .}}
|
||||||
|
<div class="dashboard issues repository milestones">
|
||||||
|
{{template "user/dashboard/navbar" .}}
|
||||||
|
<div class="ui container">
|
||||||
|
<div class="ui stackable grid">
|
||||||
|
<div class="four wide column">
|
||||||
|
<div class="ui secondary vertical filter menu">
|
||||||
|
<a class="item" href="{{.Link}}?type=your_repositories&sort={{$.SortType}}&state={{.State}}">
|
||||||
|
{{.i18n.Tr "home.issues.in_your_repos"}}
|
||||||
|
<strong class="ui right">{{.Total}}</strong>
|
||||||
|
</a>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
{{range .Repos}}
|
||||||
|
{{with $Repo := .}}
|
||||||
|
<a class="{{range $.RepoIDs}}{{if eq . $Repo.ID}}ui basic blue button{{end}}{{end}} repo name item" href="{{$.Link}}?repos=[
|
||||||
|
{{with $include := true}}
|
||||||
|
{{range $.RepoIDs}}
|
||||||
|
{{if eq . $Repo.ID}}
|
||||||
|
{{$include = false}}
|
||||||
|
{{else}}
|
||||||
|
{{.}}%2C
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{if eq $include true}}
|
||||||
|
{{$Repo.ID}}%2C
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
]&sort={{$.SortType}}&state={{$.State}}" title="{{.FullName}}">
|
||||||
|
<span class="text truncate">{{$Repo.FullName}}</span>
|
||||||
|
<div class="floating ui {{if $.IsShowClosed}}red{{else}}green{{end}} label">{{index $.Counts $Repo.ID}}</div>
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="twelve wide column content">
|
||||||
|
<div class="ui tiny basic status buttons">
|
||||||
|
<a class="ui {{if not .IsShowClosed}}green active{{end}} basic button" href="{{.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=open">
|
||||||
|
<i class="octicon octicon-issue-opened"></i>
|
||||||
|
{{.i18n.Tr "repo.milestones.open_tab" .MilestoneStats.OpenCount}}
|
||||||
|
</a>
|
||||||
|
<a class="ui {{if .IsShowClosed}}red active{{end}} basic button" href="{{.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=closed">
|
||||||
|
<i class="octicon octicon-issue-closed"></i>
|
||||||
|
{{.i18n.Tr "repo.milestones.close_tab" .MilestoneStats.ClosedCount}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="ui right floated secondary filter menu">
|
||||||
|
<!-- Sort -->
|
||||||
|
<div class="ui dropdown type jump item">
|
||||||
|
<span class="text">
|
||||||
|
{{.i18n.Tr "repo.issues.filter_sort"}}
|
||||||
|
<i class="dropdown icon"></i>
|
||||||
|
</span>
|
||||||
|
<div class="menu">
|
||||||
|
<a class="{{if or (eq .SortType "closestduedate") (not .SortType)}}active{{end}} item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=closestduedate&state={{$.State}}">{{.i18n.Tr "repo.milestones.filter_sort.closest_due_date"}}</a>
|
||||||
|
<a class="{{if eq .SortType "furthestduedate"}}active{{end}} item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=furthestduedate&state={{$.State}}">{{.i18n.Tr "repo.milestones.filter_sort.furthest_due_date"}}</a>
|
||||||
|
<a class="{{if eq .SortType "leastcomplete"}}active{{end}} item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastcomplete&state={{$.State}}">{{.i18n.Tr "repo.milestones.filter_sort.least_complete"}}</a>
|
||||||
|
<a class="{{if eq .SortType "mostcomplete"}}active{{end}} item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostcomplete&state={{$.State}}">{{.i18n.Tr "repo.milestones.filter_sort.most_complete"}}</a>
|
||||||
|
<a class="{{if eq .SortType "mostissues"}}active{{end}} item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostissues&state={{$.State}}">{{.i18n.Tr "repo.milestones.filter_sort.most_issues"}}</a>
|
||||||
|
<a class="{{if eq .SortType "leastissues"}}active{{end}} item" href="{{$.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastissues&state={{$.State}}">{{.i18n.Tr "repo.milestones.filter_sort.least_issues"}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="milestone list">
|
||||||
|
{{range .Milestones}}
|
||||||
|
<li class="item">
|
||||||
|
<div class="ui label">{{if not $.RepoIDs}}{{.Repo.FullName}}{{end}}</div>
|
||||||
|
<i class="octicon octicon-milestone"></i> <a href="{{.Repo.Link }}/milestone/{{.ID}}">{{.Name}}</a>
|
||||||
|
<div class="ui right green progress" data-percent="{{.Completeness}}">
|
||||||
|
<div class="bar" {{if not .Completeness}}style="background-color: transparent"{{end}}>
|
||||||
|
<div class="progress"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
{{ $closedDate:= TimeSinceUnix .ClosedDateUnix $.Lang }}
|
||||||
|
{{if .IsClosed}}
|
||||||
|
<span class="octicon octicon-clock"></span> {{$.i18n.Tr "repo.milestones.closed" $closedDate|Str2html}}
|
||||||
|
{{else}}
|
||||||
|
<span class="octicon octicon-calendar"></span>
|
||||||
|
{{if .DeadlineString}}
|
||||||
|
<span {{if .IsOverdue}}class="overdue"{{end}}>{{.DeadlineString}}</span>
|
||||||
|
{{else}}
|
||||||
|
{{$.i18n.Tr "repo.milestones.no_due_date"}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
<span class="issue-stats">
|
||||||
|
<i class="octicon octicon-issue-opened"></i> {{$.i18n.Tr "repo.milestones.open_tab" .NumOpenIssues}}
|
||||||
|
<i class="octicon octicon-issue-closed"></i> {{$.i18n.Tr "repo.milestones.close_tab" .NumClosedIssues}}
|
||||||
|
{{if .TotalTrackedTime}}<i class="octicon octicon-clock"></i> {{.TotalTrackedTime|Sec2Time}}{{end}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{if and (or $.CanWriteIssues $.CanWritePulls) (not $.Repository.IsArchived)}}
|
||||||
|
<div class="ui right operate">
|
||||||
|
<a href="{{$.Link}}/{{.ID}}/edit" data-id={{.ID}} data-title={{.Name}}><i class="octicon octicon-pencil"></i> {{$.i18n.Tr "repo.issues.label_edit"}}</a>
|
||||||
|
{{if .IsClosed}}
|
||||||
|
<a href="{{$.Link}}/{{.ID}}/open" data-id={{.ID}} data-title={{.Name}}><i class="octicon octicon-check"></i> {{$.i18n.Tr "repo.milestones.open"}}</a>
|
||||||
|
{{else}}
|
||||||
|
<a href="{{$.Link}}/{{.ID}}/close" data-id={{.ID}} data-title={{.Name}}><i class="octicon octicon-x"></i> {{$.i18n.Tr "repo.milestones.close"}}</a>
|
||||||
|
{{end}}
|
||||||
|
<a class="delete-button" href="#" data-url="{{$.RepoLink}}/milestones/delete" data-id="{{.ID}}"><i class="octicon octicon-trashcan"></i> {{$.i18n.Tr "repo.issues.label_delete"}}</a>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{if .Content}}
|
||||||
|
<div class="content">
|
||||||
|
{{.RenderedContent|Str2html}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{template "base/paginate" .}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{template "base/footer" .}}
|
|
@ -12,12 +12,12 @@
|
||||||
{{.i18n.Tr "home.switch_dashboard_context"}}
|
{{.i18n.Tr "home.switch_dashboard_context"}}
|
||||||
</div>
|
</div>
|
||||||
<div class="scrolling menu items">
|
<div class="scrolling menu items">
|
||||||
<a class="{{if eq .ContextUser.ID .SignedUser.ID}}active selected{{end}} item" href="{{AppSubUrl}}/{{if .PageIsIssues}}issues{{else if .PageIsPulls}}pulls{{end}}">
|
<a class="{{if eq .ContextUser.ID .SignedUser.ID}}active selected{{end}} item" href="{{AppSubUrl}}/{{if .PageIsIssues}}issues{{else if .PageIsPulls}}pulls{{else if .PageIsMilestonesDashboard}}milestones{{end}}">
|
||||||
<img class="ui avatar image" src="{{.SignedUser.RelAvatarLink}}">
|
<img class="ui avatar image" src="{{.SignedUser.RelAvatarLink}}">
|
||||||
{{.SignedUser.Name}}
|
{{.SignedUser.Name}}
|
||||||
</a>
|
</a>
|
||||||
{{range .Orgs}}
|
{{range .Orgs}}
|
||||||
<a class="{{if eq $.ContextUser.ID .ID}}active selected{{end}} item" title="{{.Name}}" href="{{AppSubUrl}}/org/{{.Name}}/{{if $.PageIsIssues}}issues{{else if $.PageIsPulls}}pulls{{else}}dashboard{{end}}">
|
<a class="{{if eq $.ContextUser.ID .ID}}active selected{{end}} item" title="{{.Name}}" href="{{AppSubUrl}}/org/{{.Name}}/{{if $.PageIsIssues}}issues{{else if $.PageIsPulls}}pulls{{else if $.PageIsMilestonesDashboard}}milestones{{else}}dashboard{{end}}">
|
||||||
<img class="ui avatar image" src="{{.RelAvatarLink}}">
|
<img class="ui avatar image" src="{{.RelAvatarLink}}">
|
||||||
{{.ShortName 20}}
|
{{.ShortName 20}}
|
||||||
</a>
|
</a>
|
||||||
|
@ -43,6 +43,11 @@
|
||||||
<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">
|
||||||
<i class="octicon octicon-git-pull-request"></i> {{.i18n.Tr "pull_requests"}}
|
<i class="octicon octicon-git-pull-request"></i> {{.i18n.Tr "pull_requests"}}
|
||||||
</a>
|
</a>
|
||||||
|
{{if .ShowMilestonesDashboardPage}}
|
||||||
|
<a class="{{if .PageIsMilestonesDashboard}}active{{end}} item" href="{{AppSubUrl}}/org/{{.ContextUser.Name}}/milestones">
|
||||||
|
<i class="octicon octicon-milestone"></i> {{.i18n.Tr "milestones"}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<a class="ui blue basic button" href="{{.ContextUser.HomeLink}}" title='{{.i18n.Tr "home.view_home" .ContextUser.Name}}'>
|
<a class="ui blue basic button" href="{{.ContextUser.HomeLink}}" title='{{.i18n.Tr "home.view_home" .ContextUser.Name}}'>
|
||||||
{{.i18n.Tr "home.view_home" (.ContextUser.ShortName 10)}}
|
{{.i18n.Tr "home.view_home" (.ContextUser.ShortName 10)}}
|
||||||
|
|
Loading…
Reference in a new issue