From 88784a113482efafea1c772c66da9339b8419813 Mon Sep 17 00:00:00 2001 From: Jamie Schouten Date: Mon, 16 Sep 2024 23:26:20 +0200 Subject: [PATCH 1/3] Start implementing issue weights --- models/issues/comment.go | 38 +++++++++++++++++ models/issues/issue.go | 1 + models/issues/issue_update.go | 25 +++++++++++ models/issues/milestone.go | 23 ++++++++++ models/issues/milestone_list.go | 39 +++++++++++++++++ models/migrations/v1_23/v305.go | 13 ++++++ models/project/column.go | 14 ------- modules/structs/issue.go | 10 +++++ options/locale/locale_en-US.ini | 9 ++++ routers/api/v1/repo/issue.go | 2 +- routers/web/repo/issue.go | 31 +++++++++++++- routers/web/repo/milestone.go | 13 ++++++ routers/web/web.go | 1 + services/forms/repo_form.go | 1 + services/issue/issue.go | 9 +++- templates/projects/view.tmpl | 6 ++- templates/repo/issue/card.tmpl | 4 ++ templates/repo/issue/milestone_issues.tmpl | 6 +++ templates/repo/issue/milestones.tmpl | 6 +++ templates/repo/issue/new_form.tmpl | 9 ++++ .../repo/issue/view_content/comments.tmpl | 34 +++++++++++++++ .../repo/issue/view_content/sidebar.tmpl | 42 +++++++++++++++++++ templates/shared/issuelist.tmpl | 8 +++- templates/user/dashboard/milestones.tmpl | 6 +++ web_src/js/features/repo-issue.ts | 17 ++++++++ web_src/js/features/repo-projects.ts | 34 +++++++++++++++ web_src/js/index.ts | 2 + 27 files changed, 383 insertions(+), 20 deletions(-) create mode 100644 models/migrations/v1_23/v305.go diff --git a/models/issues/comment.go b/models/issues/comment.go index 48b8e335d48ef..6fb4cd3f9c185 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -114,6 +114,10 @@ const ( CommentTypePin // 36 pin Issue CommentTypeUnpin // 37 unpin Issue + + CommentTypeAddedWeight // 38 Added a weight + CommentTypeModifiedWeight // 39 Modified the weight + CommentTypeRemovedWeight // 40 Removed a weight ) var commentStrings = []string{ @@ -960,6 +964,40 @@ func createDeadlineComment(ctx context.Context, doer *user_model.User, issue *Is return comment, nil } +func CreateWeightComment(ctx context.Context, doer *user_model.User, issue *Issue, newWeight int) (*Comment, error) { + var content string + var commentType CommentType + + // weight = 0 means deleting + if newWeight == 0 { + commentType = CommentTypeRemovedWeight + content = fmt.Sprintf("%d", issue.Weight) + } else if issue.Weight == 0 || issue.Weight == newWeight { + commentType = CommentTypeAddedWeight + content = fmt.Sprintf("%d", newWeight) + } else { // Otherwise modified + commentType = CommentTypeModifiedWeight + content = fmt.Sprintf("%d|%d", newWeight, issue.Weight) + } + + if err := issue.LoadRepo(ctx); err != nil { + return nil, err + } + + opts := &CreateCommentOptions{ + Type: commentType, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + Content: content, + } + comment, err := CreateComment(ctx, opts) + if err != nil { + return nil, err + } + return comment, nil +} + // Creates issue dependency comment func createIssueDependencyComment(ctx context.Context, doer *user_model.User, issue, dependentIssue *Issue, add bool) (err error) { cType := CommentTypeAddDependency diff --git a/models/issues/issue.go b/models/issues/issue.go index 40462ed09dfd6..f5d2105a74221 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -127,6 +127,7 @@ type Issue struct { NumComments int Ref string PinOrder int `xorm:"DEFAULT 0"` + Weight int DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"` diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go index 31d76be5e0aea..3ed6ea0cac36a 100644 --- a/models/issues/issue_update.go +++ b/models/issues/issue_update.go @@ -459,6 +459,31 @@ func UpdateIssueDeadline(ctx context.Context, issue *Issue, deadlineUnix timeuti return committer.Commit() } +// UpdateIssueDeadline updates an issue deadline and adds comments. Setting a deadline to 0 means deleting it. +func UpdateIssueWeight(ctx context.Context, issue *Issue, weight int, doer *user_model.User) (err error) { + // if the weight hasn't changed do nothing + if issue.Weight == weight { + return nil + } + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + // Update the weight + if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, Weight: weight}, "weight"); err != nil { + return err + } + + // Make the comment + if _, err = CreateWeightComment(ctx, doer, issue, weight); err != nil { + return fmt.Errorf("createWeightComment: %w", err) + } + + return committer.Commit() +} + // FindAndUpdateIssueMentions finds users mentioned in the given content string, and saves them in the database. func FindAndUpdateIssueMentions(ctx context.Context, issue *Issue, doer *user_model.User, content string) (mentions []*user_model.User, err error) { rawMentions := references.FindAllMentionsMarkdown(content) diff --git a/models/issues/milestone.go b/models/issues/milestone.go index db0312adf0057..bd7f31240453a 100644 --- a/models/issues/milestone.go +++ b/models/issues/milestone.go @@ -65,6 +65,7 @@ type Milestone struct { DeadlineString string `xorm:"-"` TotalTrackedTime int64 `xorm:"-"` + TotalWeight int `xorm:"-"` } func init() { @@ -355,6 +356,28 @@ func (m *Milestone) LoadTotalTrackedTime(ctx context.Context) error { return nil } +// LoadTotalWeight loads the total weight for the milestone +func (m *Milestone) LoadTotalWeight(ctx context.Context) error { + type totalTimesByMilestone struct { + MilestoneID int64 + Weight int + } + totalTime := &totalTimesByMilestone{MilestoneID: m.ID} + has, err := db.GetEngine(ctx).Table("issue"). + Join("INNER", "milestone", "issue.milestone_id = milestone.id"). + Select("milestone_id, sum(weight) as weight"). + Where("milestone_id = ?", m.ID). + GroupBy("milestone_id"). + Get(totalTime) + if err != nil { + return err + } else if !has { + return nil + } + m.TotalWeight = totalTime.Weight + return nil +} + // InsertMilestones creates milestones of repository. func InsertMilestones(ctx context.Context, ms ...*Milestone) (err error) { if len(ms) == 0 { diff --git a/models/issues/milestone_list.go b/models/issues/milestone_list.go index 955ab2356df5e..49571ef5d83ac 100644 --- a/models/issues/milestone_list.go +++ b/models/issues/milestone_list.go @@ -130,6 +130,45 @@ func (milestones MilestoneList) LoadTotalTrackedTimes(ctx context.Context) error return nil } +// LoadTotalWeight loads for every milestone in the list the TotalWeight by a batch request +func (milestones MilestoneList) LoadTotalWeight(ctx context.Context) error { + type totalWeightByMilestone struct { + MilestoneID int64 + Weight int + } + if len(milestones) == 0 { + return nil + } + trackedTimes := make(map[int64]int, len(milestones)) + + // Get total tracked time by milestone_id + rows, err := db.GetEngine(ctx).Table("issue"). + Join("INNER", "milestone", "issue.milestone_id = milestone.id"). + Select("milestone_id, sum(weight) as weight"). + In("milestone_id", milestones.getMilestoneIDs()). + GroupBy("milestone_id"). + Rows(new(totalWeightByMilestone)) + if err != nil { + return err + } + + defer rows.Close() + + for rows.Next() { + var totalTime totalWeightByMilestone + err = rows.Scan(&totalTime) + if err != nil { + return err + } + trackedTimes[totalTime.MilestoneID] = totalTime.Weight + } + + for _, milestone := range milestones { + milestone.TotalWeight = trackedTimes[milestone.ID] + } + return nil +} + // CountMilestonesByRepoCondAndKw map from repo conditions and the keyword of milestones' name to number of milestones matching the options` func CountMilestonesMap(ctx context.Context, opts FindMilestoneOptions) (map[int64]int64, error) { sess := db.GetEngine(ctx).Where(opts.ToConds()) diff --git a/models/migrations/v1_23/v305.go b/models/migrations/v1_23/v305.go new file mode 100644 index 0000000000000..fdd2799da8892 --- /dev/null +++ b/models/migrations/v1_23/v305.go @@ -0,0 +1,13 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_23 //nolint + +import "xorm.io/xorm" + +func AssIssueWeight(x *xorm.Engine) error { + type Issue struct { + Weight int + } + return x.Sync(new(Issue)) +} diff --git a/models/project/column.go b/models/project/column.go index 222f44859928e..f6d6614004796 100644 --- a/models/project/column.go +++ b/models/project/column.go @@ -57,20 +57,6 @@ func (Column) TableName() string { return "project_board" // TODO: the legacy table name should be project_column } -// NumIssues return counter of all issues assigned to the column -func (c *Column) NumIssues(ctx context.Context) int { - total, err := db.GetEngine(ctx).Table("project_issue"). - Where("project_id=?", c.ProjectID). - And("project_board_id=?", c.ID). - GroupBy("issue_id"). - Cols("issue_id"). - Count() - if err != nil { - return 0 - } - return int(total) -} - func (c *Column) GetIssues(ctx context.Context) ([]*ProjectIssue, error) { issues := make([]*ProjectIssue, 0, 5) if err := db.GetEngine(ctx).Where("project_id=?", c.ProjectID). diff --git a/modules/structs/issue.go b/modules/structs/issue.go index 3682191be5751..98786190330c4 100644 --- a/modules/structs/issue.go +++ b/modules/structs/issue.go @@ -122,6 +122,11 @@ type EditDeadlineOption struct { Deadline *time.Time `json:"due_date"` } +type EditWeightOption struct { + // required:true + Weight int `json:"weight"` +} + // IssueDeadline represents an issue deadline // swagger:model type IssueDeadline struct { @@ -129,6 +134,11 @@ type IssueDeadline struct { Deadline *time.Time `json:"due_date"` } +// IssueWeight represents an issue weight +type IssueWeight struct { + Weight int `json:"weight"` +} + // IssueFormFieldType defines issue form field type, can be "markdown", "textarea", "input", "dropdown" or "checkboxes" type IssueFormFieldType string diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 957e73b171d32..48362f9945dd2 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -18,6 +18,7 @@ language = Language notifications = Notifications active_stopwatch = Active Time Tracker tracked_time_summary = Summary of tracked time based on filters of issue list +weight_summary = Total weight of all issues in the milestone create_new = Create… user_profile_and_more = Profile and Settings… signed_in_as = Signed in as @@ -1698,6 +1699,14 @@ issues.due_date_modified = "modified the due date from %[2]s to %[1]s %[3]s" issues.due_date_remove = "removed the due date %s %s" issues.due_date_overdue = "Overdue" issues.due_date_invalid = "The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'." +issues.weight_added = "added weight of %s %s" +issues.weight_modified = " changed weight to %s from %s %s" +issues.weight_removed = "removed weight of %s %s" + +issues.weight.title = Weight +issues.weight.no_weight = No Weight +issues.weight.remove_weight = remove weight + issues.dependency.title = Dependencies issues.dependency.issue_no_dependencies = No dependencies set. issues.dependency.pr_no_dependencies = No dependencies set. diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index c1218440e5958..3cac0463ec784 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -707,7 +707,7 @@ func CreateIssue(ctx *context.APIContext) { form.Labels = make([]int64, 0) } - if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, 0); err != nil { + if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, 0, 0); err != nil { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err) } else if errors.Is(err, user_model.ErrBlockedUser) { diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 596abb4b9ca5b..4f4f75aa4d05e 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -1277,9 +1277,10 @@ func NewIssuePost(ctx *context.Context) { MilestoneID: milestoneID, Content: content, Ref: form.Ref, + Weight: form.Weight, } - if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs, projectID); err != nil { + if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs, projectID, form.Weight); err != nil { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error()) } else if errors.Is(err, user_model.ErrBlockedUser) { @@ -2360,6 +2361,34 @@ func UpdateIssueDeadline(ctx *context.Context) { ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: &deadline}) } +// UpdateIssueWeight updates an issue weight +func UpdateIssueWeight(ctx *context.Context) { + form := web.GetForm(ctx).(*api.EditWeightOption) + + issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index")) + if err != nil { + if issues_model.IsErrIssueNotExist(err) { + ctx.NotFound("GetIssueByIndex", err) + } else { + ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error()) + } + return + } + + if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { + ctx.Error(http.StatusForbidden, "", "Not repo writer") + return + } + + if err := issues_model.UpdateIssueWeight(ctx, issue, form.Weight, ctx.Doer); err != nil { + ctx.Error(http.StatusInternalServerError, "UpdateIssueWeight", err.Error()) + return + } + + ctx.Redirect(issue.Link()) + // ctx.JSON(http.StatusCreated, api.IssueWeight{Weight: form.Weight}) +} + // UpdateIssueMilestone change issue's milestone func UpdateIssueMilestone(ctx *context.Context) { issues := getActionIssues(ctx) diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index e4ee025875ef6..e096af90a87a0 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -79,6 +79,12 @@ func Milestones(ctx *context.Context) { return } } + + if err := issues_model.MilestoneList(miles).LoadTotalWeight(ctx); err != nil { + ctx.ServerError("LoadTotalWeight", err) + return + } + for _, m := range miles { m.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ Links: markup.Links{ @@ -302,5 +308,12 @@ func MilestoneIssuesAndPulls(ctx *context.Context) { ctx.Data["CanWriteIssues"] = ctx.Repo.CanWriteIssuesOrPulls(false) ctx.Data["CanWritePulls"] = ctx.Repo.CanWriteIssuesOrPulls(true) + if err := milestone.LoadTotalWeight(ctx); err != nil { + ctx.ServerError("LoadTotalWeight", err) + return + } + + ctx.Data["TotalWeight"] = milestone.TotalWeight + ctx.HTML(http.StatusOK, tplMilestoneIssues) } diff --git a/routers/web/web.go b/routers/web/web.go index f1e941a84efcb..797e307bd42a8 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1204,6 +1204,7 @@ func registerRoutes(m *web.Router) { m.Post("/title", repo.UpdateIssueTitle) m.Post("/content", repo.UpdateIssueContent) m.Post("/deadline", web.Bind(structs.EditDeadlineOption{}), repo.UpdateIssueDeadline) + m.Post("/weight", web.Bind(structs.EditWeightOption{}), repo.UpdateIssueWeight) m.Post("/watch", repo.IssueWatch) m.Post("/ref", repo.UpdateIssueRef) m.Post("/pin", reqRepoAdmin, repo.IssuePinOrUnpin) diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 988e479a48138..2e7ef33352ea5 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -450,6 +450,7 @@ type CreateIssueForm struct { MilestoneID int64 ProjectID int64 AssigneeID int64 + Weight int Content string Files []string AllowMaintainerEdit bool diff --git a/services/issue/issue.go b/services/issue/issue.go index 72ea66c8d98c5..bb8366d7ac4da 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -9,6 +9,7 @@ import ( activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/db" + issue_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues" access_model "code.gitea.io/gitea/models/perm/access" project_model "code.gitea.io/gitea/models/project" @@ -23,7 +24,7 @@ import ( ) // NewIssue creates new issue with labels for repository. -func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64, projectID int64) error { +func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64, projectID int64, weight int) error { if err := issue.LoadPoster(ctx); err != nil { return err } @@ -46,6 +47,12 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_mo return err } } + + if weight != 0 { + if _, err := issue_model.CreateWeightComment(ctx, issue.Poster, issue, weight); err != nil { + return err + } + } return nil }); err != nil { return err diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index f5c1bb76703c4..816b4e11bd0ef 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -145,9 +145,13 @@
- {{.NumIssues ctx}} +
{{.Title}}
+
+ {{svg "octicon-briefcase"}} + +
{{if $canWriteProject}} {{end}} +
+ {{svg "octicon-briefcase" 16 "tw-mr-1 tw-align-middle"}} + {{.Weight}} +
{{if or .Labels .Assignees}} diff --git a/templates/repo/issue/milestone_issues.tmpl b/templates/repo/issue/milestone_issues.tmpl index 5bae6fc6d585e..8bb4db3d3d461 100644 --- a/templates/repo/issue/milestone_issues.tmpl +++ b/templates/repo/issue/milestone_issues.tmpl @@ -53,6 +53,12 @@ {{.TotalTrackedTime | Sec2Time}}
{{end}} + {{if .TotalWeight}} +
+ {{svg "octicon-briefcase"}} + {{.TotalWeight}} +
+ {{end}}
diff --git a/templates/repo/issue/milestones.tmpl b/templates/repo/issue/milestones.tmpl index bce7ad871719a..8e8dae25e9e89 100644 --- a/templates/repo/issue/milestones.tmpl +++ b/templates/repo/issue/milestones.tmpl @@ -44,6 +44,12 @@ {{.TotalTrackedTime|Sec2Time}} {{end}} + {{if .TotalWeight}} +
+ {{svg "octicon-briefcase"}} + {{.TotalWeight}} +
+ {{end}} {{if .UpdatedUnix}}
{{svg "octicon-clock"}} diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl index e56d1b9ecc7bd..b2d4b70f946e2 100644 --- a/templates/repo/issue/new_form.tmpl +++ b/templates/repo/issue/new_form.tmpl @@ -185,6 +185,15 @@
{{end}} +
+
+ + Weight + +
+
+ +
diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 57abbeb8f7960..f445ae3c6a103 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -700,6 +700,40 @@ {{else}}{{ctx.Locale.Tr "repo.issues.unpin_comment" $createdStr}}{{end}} + {{else if eq .Type 38}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.weight_added" .Content $createdStr}} + +
+ {{else if eq .Type 39}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{$splitWeight := StringUtils.Split .Content "|"}} + {{if eq (len $splitWeight) 2}} + {{$to := (index $splitWeight 0)}} + {{$from := (index $splitWeight 1)}} + + {{ctx.Locale.Tr "repo.issues.weight_modified" $to $from $createdStr}} + {{end}} + +
+ {{else if eq .Type 40}} +
+ {{svg "octicon-clock"}} + {{template "shared/user/avatarlink" dict "user" .Poster}} + + {{template "shared/user/authorlink" .Poster}} + {{ctx.Locale.Tr "repo.issues.weight_removed" .Content $createdStr}} + +
{{end}} + {{end}} {{end}} diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index 49c40be5a94d8..09a269fffde0e 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -256,6 +256,48 @@ {{end}} +
+ + + +
+ + {{ctx.Locale.Tr "repo.issues.weight.title"}} + {{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}} + {{svg "octicon-pencil" 16 "tw-ml-1"}} + {{end}} + +
+ + + +
+ {{if eq .Issue.Weight 0}} + {{ctx.Locale.Tr "repo.issues.weight.no_weight"}} + {{end}} + + {{if gt .Issue.Weight 0}} +
+ + {{ .Issue.Weight }} - +
+ {{$.CsrfTokenHtml}} + + +
+
+ {{end}} + +
+ {{$.CsrfTokenHtml}} + +
+
+ + +
diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl index 16c650ee3e45e..3e3907ba168e5 100644 --- a/templates/shared/issuelist.tmpl +++ b/templates/shared/issuelist.tmpl @@ -25,14 +25,18 @@ {{end}} - {{if or .TotalTrackedTime .Assignees .NumComments}} + {{if or .TotalTrackedTime .Assignees .NumComments .Weight}}
- {{if .TotalTrackedTime}} + {{if .TotalTrackedTime}}
{{svg "octicon-clock" 16}} {{.TotalTrackedTime | Sec2Time}}
{{end}} +
+ {{svg "octicon-briefcase" 16}} + {{.Weight}} +
{{if .Assignees}}
{{range .Assignees}} diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl index 71ff8dba3f4e7..fa56951b24284 100644 --- a/templates/user/dashboard/milestones.tmpl +++ b/templates/user/dashboard/milestones.tmpl @@ -101,6 +101,12 @@ {{.TotalTrackedTime|Sec2Time}}
{{end}} + {{if .TotalWeight}} +
+ {{svg "octicon-briefcase"}} + {{.TotalWeight}} +
+ {{end}} {{if .UpdatedUnix}}
{{svg "octicon-clock"}} diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts index 4377292a64da9..b4bf850c05002 100644 --- a/web_src/js/features/repo-issue.ts +++ b/web_src/js/features/repo-issue.ts @@ -87,6 +87,23 @@ export function initRepoIssueDue() { }); } +function focusAtEnd(selector) { + const element = document.querySelector(selector); + const value = element.value; + + element.value = ''; + element.focus(); + element.value = value; +} + +export function initRepoIssueWeight() { + $(document).on('click', '.issue-weight-edit', () => { + toggleElem('.issue-weight-form'); + toggleElem('.issue-weight-view'); + focusAtEnd('#issueWeight'); + }); +} + /** * @param {HTMLElement} item */ diff --git a/web_src/js/features/repo-projects.ts b/web_src/js/features/repo-projects.ts index bc2bb69a339b5..819b01d2edd5e 100644 --- a/web_src/js/features/repo-projects.ts +++ b/web_src/js/features/repo-projects.ts @@ -3,6 +3,25 @@ import {contrastColor} from '../utils/color.ts'; import {createSortable} from '../modules/sortable.ts'; import {POST, DELETE, PUT} from '../modules/fetch.ts'; +function updateWeight(cards) { + const parent = cards.parentElement; + const columnWeight = parent.querySelector('.project-column-weight'); + + let totalWeight = 0; + + for (const node of Array.from(cards.querySelectorAll('span[data-weight]'))) { + totalWeight += parseInt(node.getAttribute('data-weight')); + } + + if (totalWeight === 0) { + columnWeight.parentElement.classList.add('tw-hidden'); + } else { + columnWeight.parentElement.classList.remove('tw-hidden'); + } + + columnWeight.textContent = totalWeight; +} + function updateIssueCount(cards) { const parent = cards.parentElement; const cnt = parent.querySelectorAll('.issue-card').length; @@ -30,6 +49,14 @@ async function moveIssue({item, from, to, oldIndex}) { updateIssueCount(from); updateIssueCount(to); + let weight = item.querySelector('span[data-weight]').getAttribute('data-weight'); + + if (weight) { + weight = parseInt(weight); + updateWeight(from, -weight); + updateWeight(to, weight); + } + const columnSorting = { issues: Array.from(columnCards, (card, i) => ({ issueID: parseInt(card.getAttribute('data-issue')), @@ -186,3 +213,10 @@ export function initRepoProject() { createNewColumn(url, $columnTitle, $projectColorInput); }); } + +window.document.addEventListener('DOMContentLoaded', () => { + for (const card of document.querySelectorAll('.cards')) { + updateIssueCount(card); + updateWeight(card); + } +}); diff --git a/web_src/js/index.ts b/web_src/js/index.ts index db678a25ba388..a7963351eeaa3 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -27,6 +27,7 @@ import {initPdfViewer} from './render/pdf.ts'; import {initUserAuthOauth2} from './features/user-auth.ts'; import { initRepoIssueDue, + initRepoIssueWeight, initRepoIssueReferenceRepositorySearch, initRepoIssueTimeTracking, initRepoIssueWipTitle, @@ -189,6 +190,7 @@ onDomReady(() => { initRepoGraphGit, initRepoIssueContentHistory, initRepoIssueDue, + initRepoIssueWeight, initRepoIssueList, initRepoIssueSidebarList, initArchivedLabelHandler, From ef064c5fab65a4d41db3c68dd923670d6ecbdb39 Mon Sep 17 00:00:00 2001 From: Jamie Schouten Date: Thu, 19 Sep 2024 23:23:40 +0200 Subject: [PATCH 2/3] Add enable weight configuration options. Add weight based progress fore milestones. --- models/fixtures/issue.yml | 22 ++++++ models/issues/issue_test.go | 20 +++++ models/issues/milestone.go | 24 +++--- models/issues/milestone_list.go | 24 +++--- models/issues/weight.go | 57 ++++++++++++++ models/issues/weight_test.go | 33 +++++++++ models/migrations/v1_23/v305.go | 2 +- models/repo/issue.go | 18 +++++ models/repo/repo_unit.go | 1 + modules/setting/service.go | 7 ++ modules/structs/repo.go | 2 + options/locale/locale_en-US.ini | 3 +- routers/api/v1/repo/repo.go | 2 + routers/web/repo/issue.go | 12 +++ routers/web/repo/milestone.go | 15 ++-- routers/web/repo/projects.go | 1 + routers/web/repo/setting/setting.go | 1 + services/forms/repo_form.go | 1 + templates/projects/view.tmpl | 12 +-- templates/repo/issue/card.tmpl | 2 + templates/repo/issue/milestone_issues.tmpl | 10 ++- templates/repo/issue/milestones.tmpl | 8 +- .../repo/issue/view_content/sidebar.tmpl | 74 +++++++++---------- templates/repo/settings/options.tmpl | 8 ++ templates/shared/issuelist.tmpl | 14 ++-- web_src/js/features/repo-projects.ts | 5 +- 26 files changed, 294 insertions(+), 84 deletions(-) create mode 100644 models/issues/weight.go create mode 100644 models/issues/weight_test.go diff --git a/models/fixtures/issue.yml b/models/fixtures/issue.yml index ca5b1c6cd1df5..9764efe6c8278 100644 --- a/models/fixtures/issue.yml +++ b/models/fixtures/issue.yml @@ -14,6 +14,7 @@ created_unix: 946684800 updated_unix: 978307200 is_locked: false + weight: 10 - id: 2 @@ -31,6 +32,7 @@ created_unix: 946684810 updated_unix: 978307190 is_locked: false + weight: 12 - id: 3 @@ -48,6 +50,7 @@ created_unix: 946684820 updated_unix: 978307180 is_locked: false + weight: 5 - id: 4 @@ -65,6 +68,7 @@ created_unix: 946684830 updated_unix: 978307200 is_locked: false + weight: 10 - id: 5 @@ -82,6 +86,7 @@ created_unix: 946684840 updated_unix: 978307200 is_locked: false + weight: 20 - id: 6 @@ -99,6 +104,7 @@ created_unix: 946684850 updated_unix: 978307200 is_locked: false + weight: 0 - id: 7 @@ -116,6 +122,7 @@ created_unix: 946684830 updated_unix: 978307200 is_locked: false + weight: 10 - id: 8 @@ -133,6 +140,7 @@ created_unix: 946684820 updated_unix: 978307180 is_locked: false + weight: 0 - id: 9 @@ -150,6 +158,7 @@ created_unix: 946684820 updated_unix: 978307180 is_locked: false + weight: 10 - id: 10 @@ -168,6 +177,7 @@ created_unix: 946684830 updated_unix: 999307200 is_locked: false + weight: 4 - id: 11 @@ -185,6 +195,7 @@ created_unix: 1579194806 updated_unix: 1579194806 is_locked: false + weight: 0 - id: 12 @@ -202,6 +213,7 @@ created_unix: 1602935696 updated_unix: 1602935696 is_locked: false + weight: 22 - id: 13 @@ -219,6 +231,7 @@ created_unix: 1602935696 updated_unix: 1602935696 is_locked: false + weight: 13 - id: 14 @@ -236,6 +249,7 @@ created_unix: 1602935696 updated_unix: 1602935696 is_locked: false + weight: 8 - id: 15 @@ -253,6 +267,7 @@ created_unix: 1602935696 updated_unix: 1602935696 is_locked: false + weight: 52 - id: 16 @@ -270,6 +285,7 @@ created_unix: 1602935696 updated_unix: 1602935696 is_locked: false + weight: 10 - id: 17 @@ -287,6 +303,7 @@ created_unix: 1602935696 updated_unix: 1602935696 is_locked: false + weight: 0 - id: 18 @@ -304,6 +321,7 @@ created_unix: 946684830 updated_unix: 978307200 is_locked: false + weight: 20 - id: 19 @@ -321,6 +339,7 @@ created_unix: 946684830 updated_unix: 978307200 is_locked: false + weight: 10 - id: 20 @@ -338,6 +357,7 @@ created_unix: 978307210 updated_unix: 978307210 is_locked: false + weight: 24 - id: 21 @@ -355,6 +375,7 @@ created_unix: 1707270422 updated_unix: 1707270422 is_locked: false + weight: 40 - id: 22 @@ -372,3 +393,4 @@ created_unix: 1707270422 updated_unix: 1707270422 is_locked: false + weight: 2 diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go index 1bbc0eee564fd..111c93bcea225 100644 --- a/models/issues/issue_test.go +++ b/models/issues/issue_test.go @@ -375,6 +375,26 @@ func TestLoadTotalTrackedTime(t *testing.T) { assert.Equal(t, int64(3682), milestone.TotalTrackedTime) } +func TestMilestoneList_LoadTotalWeight(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + miles := issues_model.MilestoneList{ + unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}), + } + + assert.NoError(t, miles.LoadTotalWeight(db.DefaultContext)) + + assert.Equal(t, 12, miles[0].TotalWeight) +} + +func TestLoadTotalWeight(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}) + + assert.NoError(t, milestone.LoadTotalWeight(db.DefaultContext)) + + assert.Equal(t, 12, milestone.TotalWeight) +} + func TestCountIssues(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) count, err := issues_model.CountIssues(db.DefaultContext, &issues_model.IssuesOptions{}) diff --git a/models/issues/milestone.go b/models/issues/milestone.go index bd7f31240453a..3ec1a0b04d37a 100644 --- a/models/issues/milestone.go +++ b/models/issues/milestone.go @@ -64,8 +64,9 @@ type Milestone struct { ClosedDateUnix timeutil.TimeStamp DeadlineString string `xorm:"-"` - TotalTrackedTime int64 `xorm:"-"` - TotalWeight int `xorm:"-"` + TotalTrackedTime int64 `xorm:"-"` + TotalWeight int `xorm:"-"` + WeightedCompleteness int `xorm:"-"` } func init() { @@ -358,23 +359,28 @@ func (m *Milestone) LoadTotalTrackedTime(ctx context.Context) error { // LoadTotalWeight loads the total weight for the milestone func (m *Milestone) LoadTotalWeight(ctx context.Context) error { - type totalTimesByMilestone struct { - MilestoneID int64 - Weight int + type totalWeightByMilestone struct { + MilestoneID int64 + Weight int + WeightClosed int } - totalTime := &totalTimesByMilestone{MilestoneID: m.ID} + + totalWeight := &totalWeightByMilestone{MilestoneID: m.ID} has, err := db.GetEngine(ctx).Table("issue"). Join("INNER", "milestone", "issue.milestone_id = milestone.id"). - Select("milestone_id, sum(weight) as weight"). + Select("milestone_id, sum(weight) as weight, sum(CASE WHEN is_closed THEN 0 ELSE weight END) as weight_closed"). Where("milestone_id = ?", m.ID). GroupBy("milestone_id"). - Get(totalTime) + Get(totalWeight) if err != nil { return err } else if !has { return nil } - m.TotalWeight = totalTime.Weight + + m.TotalWeight = totalWeight.Weight + m.WeightedCompleteness = totalWeight.WeightClosed * 100 / totalWeight.Weight + return nil } diff --git a/models/issues/milestone_list.go b/models/issues/milestone_list.go index 49571ef5d83ac..bfc3d53cfc46b 100644 --- a/models/issues/milestone_list.go +++ b/models/issues/milestone_list.go @@ -133,39 +133,43 @@ func (milestones MilestoneList) LoadTotalTrackedTimes(ctx context.Context) error // LoadTotalWeight loads for every milestone in the list the TotalWeight by a batch request func (milestones MilestoneList) LoadTotalWeight(ctx context.Context) error { type totalWeightByMilestone struct { - MilestoneID int64 - Weight int + MilestoneID int64 + Weight int + WeightClosed int } if len(milestones) == 0 { return nil } - trackedTimes := make(map[int64]int, len(milestones)) - // Get total tracked time by milestone_id + weight := make(map[int64]int, len(milestones)) + closedWeight := make(map[int64]int, len(milestones)) + rows, err := db.GetEngine(ctx).Table("issue"). Join("INNER", "milestone", "issue.milestone_id = milestone.id"). - Select("milestone_id, sum(weight) as weight"). + Select("milestone_id, sum(weight) as weight, sum(CASE WHEN is_closed THEN 0 ELSE weight END) as weight_closed"). In("milestone_id", milestones.getMilestoneIDs()). GroupBy("milestone_id"). Rows(new(totalWeightByMilestone)) if err != nil { return err } - defer rows.Close() for rows.Next() { - var totalTime totalWeightByMilestone - err = rows.Scan(&totalTime) + var totalWeight totalWeightByMilestone + err = rows.Scan(&totalWeight) if err != nil { return err } - trackedTimes[totalTime.MilestoneID] = totalTime.Weight + weight[totalWeight.MilestoneID] = totalWeight.Weight + closedWeight[totalWeight.MilestoneID] = totalWeight.WeightClosed } for _, milestone := range milestones { - milestone.TotalWeight = trackedTimes[milestone.ID] + milestone.TotalWeight = weight[milestone.ID] + milestone.WeightedCompleteness = closedWeight[milestone.ID] * 100 / milestone.TotalWeight } + return nil } diff --git a/models/issues/weight.go b/models/issues/weight.go new file mode 100644 index 0000000000000..e412863aab8ef --- /dev/null +++ b/models/issues/weight.go @@ -0,0 +1,57 @@ +package issues + +import ( + "context" + + "code.gitea.io/gitea/models/db" +) + +// GetIssueTotalWeight returns the total weight for issues by given conditions. +func GetIssueTotalWeight(ctx context.Context, opts *IssuesOptions) (int, int, error) { + if len(opts.IssueIDs) <= MaxQueryParameters { + return getIssueTotalWeightChunk(ctx, opts, opts.IssueIDs) + } + + // If too long a list of IDs is provided, + // we get the statistics in smaller chunks and get accumulates + var weightSum int + var closedWeightSum int + for i := 0; i < len(opts.IssueIDs); { + chunk := i + MaxQueryParameters + if chunk > len(opts.IssueIDs) { + chunk = len(opts.IssueIDs) + } + weight, closedWeight, err := getIssueTotalWeightChunk(ctx, opts, opts.IssueIDs[i:chunk]) + if err != nil { + return 0, 0, err + } + weightSum += weight + closedWeightSum += closedWeight + i = chunk + } + + return weightSum, closedWeightSum, nil +} + +func getIssueTotalWeightChunk(ctx context.Context, opts *IssuesOptions, issueIDs []int64) (int, int, error) { + type totalWeight struct { + Weight int + WeightClosed int + } + + tw := &totalWeight{} + + session := db.GetEngine(ctx).Table("issue"). + Select("sum(weight) as weight, sum(CASE WHEN is_closed THEN 0 ELSE weight END) as weight_closed") + + has, err := applyIssuesOptions(session, opts, issueIDs). + Get(tw) + + if err != nil { + return 0, 0, err + } else if !has { + return 0, 0, nil + } + + return tw.Weight, tw.WeightClosed, nil +} diff --git a/models/issues/weight_test.go b/models/issues/weight_test.go new file mode 100644 index 0000000000000..c1e1d02c83c5a --- /dev/null +++ b/models/issues/weight_test.go @@ -0,0 +1,33 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issues_test + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestGetIssueTotalWeight(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + tw, twc, err := issues_model.GetIssueTotalWeight(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{1}}) + assert.NoError(t, err) + assert.EqualValues(t, 12, tw) + assert.EqualValues(t, 12, twc) + + tw, twc, err = issues_model.GetIssueTotalWeight(db.DefaultContext, &issues_model.IssuesOptions{MilestoneIDs: []int64{3}}) + assert.NoError(t, err) + assert.EqualValues(t, 5, tw) + assert.EqualValues(t, 5, twc) + + tw, twc, err = issues_model.GetIssueTotalWeight(db.DefaultContext, &issues_model.IssuesOptions{RepoIDs: []int64{2}}) + assert.NoError(t, err) + assert.EqualValues(t, 20, tw) + assert.EqualValues(t, 10, twc) +} diff --git a/models/migrations/v1_23/v305.go b/models/migrations/v1_23/v305.go index fdd2799da8892..76ba32d23bc1d 100644 --- a/models/migrations/v1_23/v305.go +++ b/models/migrations/v1_23/v305.go @@ -5,7 +5,7 @@ package v1_23 //nolint import "xorm.io/xorm" -func AssIssueWeight(x *xorm.Engine) error { +func AddIssueWeight(x *xorm.Engine) error { type Issue struct { Weight int } diff --git a/models/repo/issue.go b/models/repo/issue.go index 0dd4fd5ed480e..b131e1d209005 100644 --- a/models/repo/issue.go +++ b/models/repo/issue.go @@ -38,6 +38,24 @@ func (repo *Repository) IsTimetrackerEnabled(ctx context.Context) bool { return u.IssuesConfig().EnableTimetracker } +func (repo *Repository) CanEnableWeight() bool { + return setting.Service.EnableIssueWeight +} + +// IsTimetrackerEnabled returns whether or not the timetracker is enabled. It returns the default value from config if an error occurs. +func (repo *Repository) IsWeightEnabled(ctx context.Context) bool { + if !setting.Service.EnableIssueWeight { + return false + } + + var u *RepoUnit + var err error + if u, err = repo.GetUnit(ctx, unit.TypeIssues); err != nil { + return setting.Service.DefaultEnableIssueWeight + } + return u.IssuesConfig().EnableWeight +} + // AllowOnlyContributorsToTrackTime returns value of IssuesConfig or the default value func (repo *Repository) AllowOnlyContributorsToTrackTime(ctx context.Context) bool { var u *RepoUnit diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index cb52c2c9e2058..022cedbdc4cce 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -105,6 +105,7 @@ type IssuesConfig struct { EnableTimetracker bool AllowOnlyContributorsToTrackTime bool EnableDependencies bool + EnableWeight bool } // FromDB fills up a IssuesConfig from serialized format. diff --git a/modules/setting/service.go b/modules/setting/service.go index 3ea1501236dfd..681e7d39d9b62 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -70,6 +70,8 @@ var Service = struct { DefaultUserIsRestricted bool EnableTimetracking bool DefaultEnableTimetracking bool + EnableIssueWeight bool + DefaultEnableIssueWeight bool DefaultEnableDependencies bool AllowCrossRepositoryDependencies bool DefaultAllowOnlyContributorsToTrackTime bool @@ -184,6 +186,11 @@ func loadServiceFrom(rootCfg ConfigProvider) { if Service.EnableTimetracking { Service.DefaultEnableTimetracking = sec.Key("DEFAULT_ENABLE_TIMETRACKING").MustBool(true) } + Service.EnableIssueWeight = sec.Key("ENABLE_ISSUE_WEIGHT").MustBool(false) + if Service.EnableIssueWeight { + Service.DefaultEnableIssueWeight = sec.Key("DEFAULT_ENABLE_ISSUE_WEIGHT").MustBool(true) + } + Service.DefaultEnableDependencies = sec.Key("DEFAULT_ENABLE_DEPENDENCIES").MustBool(true) Service.AllowCrossRepositoryDependencies = sec.Key("ALLOW_CROSS_REPOSITORY_DEPENDENCIES").MustBool(true) Service.DefaultAllowOnlyContributorsToTrackTime = sec.Key("DEFAULT_ALLOW_ONLY_CONTRIBUTORS_TO_TRACK_TIME").MustBool(true) diff --git a/modules/structs/repo.go b/modules/structs/repo.go index fd27df384da2f..12495bf084a9b 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -24,6 +24,8 @@ type InternalTracker struct { AllowOnlyContributorsToTrackTime bool `json:"allow_only_contributors_to_track_time"` // Enable dependencies for issues and pull requests (Built-in issue tracker) EnableIssueDependencies bool `json:"enable_issue_dependencies"` + // Enable issue weight (Built-in issue tracker) + EnableWeight bool `json:"enable_issue_weight"` } // ExternalTracker represents settings for external tracker diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 48362f9945dd2..d5ac90925c5ae 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -18,7 +18,7 @@ language = Language notifications = Notifications active_stopwatch = Active Time Tracker tracked_time_summary = Summary of tracked time based on filters of issue list -weight_summary = Total weight of all issues in the milestone +weight_summary = Total weight based on filters of issue list create_new = Create… user_profile_and_more = Profile and Settings… signed_in_as = Signed in as @@ -2161,6 +2161,7 @@ settings.tracker_issue_style.regexp_pattern_desc = The first captured group will settings.tracker_url_format_desc = Use the placeholders {user}, {repo} and {index} for the username, repository name and issue index. settings.enable_timetracker = Enable Time Tracking settings.allow_only_contributors_to_track_time = Let Only Contributors Track Time +settings.enable_weight = Enable Weight settings.pulls_desc = Enable Repository Pull Requests settings.pulls.ignore_whitespace = Ignore Whitespace for Conflicts settings.pulls.enable_autodetect_manual_merge = Enable autodetect manual merge (Note: In some special cases, misjudgments can occur) diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 1bcec8fcf7e72..f04e2fb211c1e 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -799,6 +799,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { EnableTimetracker: opts.InternalTracker.EnableTimeTracker, AllowOnlyContributorsToTrackTime: opts.InternalTracker.AllowOnlyContributorsToTrackTime, EnableDependencies: opts.InternalTracker.EnableIssueDependencies, + EnableWeight: opts.InternalTracker.EnableWeight, } } else if unit, err := repo.GetUnit(ctx, unit_model.TypeIssues); err != nil { // Unit type doesn't exist so we make a new config file with default values @@ -806,6 +807,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { EnableTimetracker: true, AllowOnlyContributorsToTrackTime: true, EnableDependencies: true, + EnableWeight: false, } } else { config = unit.IssuesConfig() diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 4f4f75aa4d05e..c534acce97080 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -265,6 +265,18 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt ctx.Data["TotalTrackedTime"] = totalTrackedTime } + if repo.IsWeightEnabled(ctx) { + totalWeight, totalWeightClosed, err := issues_model.GetIssueTotalWeight(ctx, statsOpts, isShowClosed) + if err != nil { + ctx.ServerError("GetIssueTotalWeight", err) + return + } + + fmt.Println(totalWeight, totalWeightClosed) + ctx.Data["TotalWeight"] = totalWeight + ctx.Data["WeightedCompleteness"] = totalWeightClosed * 100 / totalWeight + } + archived := ctx.FormBool("archived") page := ctx.FormInt("page") diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index e096af90a87a0..68f1aad8de297 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -80,9 +80,11 @@ func Milestones(ctx *context.Context) { } } - if err := issues_model.MilestoneList(miles).LoadTotalWeight(ctx); err != nil { - ctx.ServerError("LoadTotalWeight", err) - return + if ctx.Repo.Repository.IsWeightEnabled(ctx) { + if err := issues_model.MilestoneList(miles).LoadTotalWeight(ctx); err != nil { + ctx.ServerError("LoadTotalWeight", err) + return + } } for _, m := range miles { @@ -308,12 +310,5 @@ func MilestoneIssuesAndPulls(ctx *context.Context) { ctx.Data["CanWriteIssues"] = ctx.Repo.CanWriteIssuesOrPulls(false) ctx.Data["CanWritePulls"] = ctx.Repo.CanWriteIssuesOrPulls(true) - if err := milestone.LoadTotalWeight(ctx); err != nil { - ctx.ServerError("LoadTotalWeight", err) - return - } - - ctx.Data["TotalWeight"] = milestone.TotalWeight - ctx.HTML(http.StatusOK, tplMilestoneIssues) } diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 664ea7eb76d79..af299bd7e05cc 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -441,6 +441,7 @@ func ViewProject(ctx *context.Context) { ctx.Data["IsProjectsPage"] = true ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) + ctx.Data["IsWeightEnabled"] = ctx.Repo.Repository.IsWeightEnabled(ctx) ctx.Data["Project"] = project ctx.Data["IssuesMap"] = issuesMap ctx.Data["Columns"] = columns diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 485bd927fa932..01cdc9afa4bcd 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -523,6 +523,7 @@ func SettingsPost(ctx *context.Context) { EnableTimetracker: form.EnableTimetracker, AllowOnlyContributorsToTrackTime: form.AllowOnlyContributorsToTrackTime, EnableDependencies: form.EnableIssueDependencies, + EnableWeight: form.EnableIssueWeight, }, }) deleteUnitTypes = append(deleteUnitTypes, unit_model.TypeExternalTracker) diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 2e7ef33352ea5..debb15e113c5b 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -167,6 +167,7 @@ type RepoSettingForm struct { EnableTimetracker bool AllowOnlyContributorsToTrackTime bool EnableIssueDependencies bool + EnableIssueWeight bool IsArchived bool // Signing Settings diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 816b4e11bd0ef..9488c18dd0083 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -145,13 +145,15 @@
- + {{len (index $.IssuesMap .ID)}}
{{.Title}}
-
- {{svg "octicon-briefcase"}} - -
+ {{if $.IsWeightEnabled }} +
+ {{svg "octicon-briefcase"}} + +
+ {{end}} {{if $canWriteProject}} {{end}} + {{if .Repo.IsWeightEnabled ctx}}
{{svg "octicon-briefcase" 16 "tw-mr-1 tw-align-middle"}} {{.Weight}}
+ {{end}}
{{if or .Labels .Assignees}} diff --git a/templates/repo/issue/milestone_issues.tmpl b/templates/repo/issue/milestone_issues.tmpl index 8bb4db3d3d461..4ef59fd1f8b55 100644 --- a/templates/repo/issue/milestone_issues.tmpl +++ b/templates/repo/issue/milestone_issues.tmpl @@ -27,7 +27,11 @@
{{end}}
- + {{ $completeness := .Milestone.Completeness }} + {{ if .Repository.IsWeightEnabled ctx }} + {{ $completeness = .WeightedCompleteness }} + {{ end }} +
{{$closedDate:= TimeSinceUnix .Milestone.ClosedDateUnix ctx.Locale}} @@ -46,7 +50,9 @@ {{end}} {{end}}
-
{{ctx.Locale.Tr "repo.milestones.completeness" .Milestone.Completeness}}
+
+ {{ctx.Locale.Tr "repo.milestones.completeness" $completeness}} +
{{if .TotalTrackedTime}}
{{svg "octicon-clock"}} diff --git a/templates/repo/issue/milestones.tmpl b/templates/repo/issue/milestones.tmpl index 8e8dae25e9e89..84fc428ab8003 100644 --- a/templates/repo/issue/milestones.tmpl +++ b/templates/repo/issue/milestones.tmpl @@ -18,14 +18,18 @@
{{range .Milestones}}
  • + {{ $completeness := .Completeness }} + {{ if $.Repository.IsWeightEnabled ctx }} + {{ $completeness = .WeightedCompleteness }} + {{ end }}

    {{svg "octicon-milestone" 16}} {{.Name}}

    - {{.Completeness}}% - + {{$completeness}}% +
    diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index 09a269fffde0e..dde93f2ee6210 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -256,45 +256,45 @@ {{end}}
  • -
    - - - - - -
    - {{if eq .Issue.Weight 0}} - {{ctx.Locale.Tr "repo.issues.weight.no_weight"}} - {{end}} - - {{if gt .Issue.Weight 0}} -
    - - {{ .Issue.Weight }} - -
    - {{$.CsrfTokenHtml}} - - -
    -
    - {{end}} - -
    - {{$.CsrfTokenHtml}} - -
    -
    + {{if .Repository.IsWeightEnabled $.Context}} +
    + + + +
    + {{if eq .Issue.Weight 0}} + {{ctx.Locale.Tr "repo.issues.weight.no_weight"}} + {{end}} + + {{if gt .Issue.Weight 0}} +
    + + {{ .Issue.Weight }} - +
    + {{$.CsrfTokenHtml}} + + +
    +
    + {{end}} + +
    + {{$.CsrfTokenHtml}} + +
    +
    + {{end}} diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index 8f71f0020f1a0..1d763fb31bb3f 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -388,6 +388,14 @@
    {{end}} + {{if .Repository.CanEnableWeight}} +
    +
    + + +
    +
    + {{end}}
    diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl index 3e3907ba168e5..bb601e6d9a447 100644 --- a/templates/shared/issuelist.tmpl +++ b/templates/shared/issuelist.tmpl @@ -29,14 +29,16 @@
    {{if .TotalTrackedTime}}
    - {{svg "octicon-clock" 16}} - {{.TotalTrackedTime | Sec2Time}} + {{svg "octicon-clock" 16}} + {{.TotalTrackedTime | Sec2Time}}
    {{end}} -
    - {{svg "octicon-briefcase" 16}} - {{.Weight}} -
    + {{if .Repo.IsWeightEnabled ctx}} +
    + {{svg "octicon-briefcase" 16}} + {{.Weight}} +
    + {{end}} {{if .Assignees}}
    {{range .Assignees}} diff --git a/web_src/js/features/repo-projects.ts b/web_src/js/features/repo-projects.ts index 819b01d2edd5e..584f111a28154 100644 --- a/web_src/js/features/repo-projects.ts +++ b/web_src/js/features/repo-projects.ts @@ -7,6 +7,10 @@ function updateWeight(cards) { const parent = cards.parentElement; const columnWeight = parent.querySelector('.project-column-weight'); + if (!columnWeight) { + return; + } + let totalWeight = 0; for (const node of Array.from(cards.querySelectorAll('span[data-weight]'))) { @@ -216,7 +220,6 @@ export function initRepoProject() { window.document.addEventListener('DOMContentLoaded', () => { for (const card of document.querySelectorAll('.cards')) { - updateIssueCount(card); updateWeight(card); } }); From 6693437a9faae67b49712908c83a982d950f20d0 Mon Sep 17 00:00:00 2001 From: Jamie Schouten Date: Fri, 20 Sep 2024 00:20:01 +0200 Subject: [PATCH 3/3] Add weight to api Only show comments when weight is enabled --- modules/structs/issue.go | 3 +++ routers/api/v1/repo/issue.go | 9 ++++++++- routers/web/repo/issue.go | 3 +-- services/convert/issue.go | 1 + services/issue/issue.go | 13 +++++++++++++ templates/repo/issue/view_content/comments.tmpl | 7 ++++--- 6 files changed, 30 insertions(+), 6 deletions(-) diff --git a/modules/structs/issue.go b/modules/structs/issue.go index 98786190330c4..c9d3368d55520 100644 --- a/modules/structs/issue.go +++ b/modules/structs/issue.go @@ -80,6 +80,7 @@ type Issue struct { Repo *RepositoryMeta `json:"repository"` PinOrder int `json:"pin_order"` + Weight int `json:"weight"` } // CreateIssueOption options to create one issue @@ -98,6 +99,7 @@ type CreateIssueOption struct { // list of label ids Labels []int64 `json:"labels"` Closed bool `json:"closed"` + Weight int `json:"weight` } // EditIssueOption options for editing an issue @@ -113,6 +115,7 @@ type EditIssueOption struct { // swagger:strfmt date-time Deadline *time.Time `json:"due_date"` RemoveDeadline *bool `json:"unset_due_date"` + Weight *int `json:"weight"` } // EditDeadlineOption options for creating a deadline diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 3cac0463ec784..8616bd20bbb74 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -707,7 +707,7 @@ func CreateIssue(ctx *context.APIContext) { form.Labels = make([]int64, 0) } - if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, 0, 0); err != nil { + if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, 0, form.Weight); err != nil { if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) { ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err) } else if errors.Is(err, user_model.ErrBlockedUser) { @@ -829,6 +829,13 @@ func EditIssue(ctx *context.APIContext) { } } + if form.Weight != nil { + if err := issue_service.ChangeIssueWeight(ctx, issue, ctx.Doer, *form.Weight); err != nil { + ctx.Error(http.StatusInternalServerError, "ChangeWeight", err) + return + } + } + // Update or remove the deadline, only if set and allowed if (form.Deadline != nil || form.RemoveDeadline != nil) && canWrite { var deadlineUnix timeutil.TimeStamp diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index c534acce97080..48de37955ea4c 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -266,7 +266,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt } if repo.IsWeightEnabled(ctx) { - totalWeight, totalWeightClosed, err := issues_model.GetIssueTotalWeight(ctx, statsOpts, isShowClosed) + totalWeight, totalWeightClosed, err := issues_model.GetIssueTotalWeight(ctx, statsOpts) if err != nil { ctx.ServerError("GetIssueTotalWeight", err) return @@ -2398,7 +2398,6 @@ func UpdateIssueWeight(ctx *context.Context) { } ctx.Redirect(issue.Link()) - // ctx.JSON(http.StatusCreated, api.IssueWeight{Weight: form.Weight}) } // UpdateIssueMilestone change issue's milestone diff --git a/services/convert/issue.go b/services/convert/issue.go index f514dc431351e..5bd90b186ea14 100644 --- a/services/convert/issue.go +++ b/services/convert/issue.go @@ -55,6 +55,7 @@ func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Iss Created: issue.CreatedUnix.AsTime(), Updated: issue.UpdatedUnix.AsTime(), PinOrder: issue.PinOrder, + Weight: issue.Weight, } if issue.Repo != nil { diff --git a/services/issue/issue.go b/services/issue/issue.go index bb8366d7ac4da..81ee2af74601e 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -126,6 +126,19 @@ func ChangeIssueRef(ctx context.Context, issue *issues_model.Issue, doer *user_m return nil } +// ChangeIssueWeight changes the weight of this issue, as the given user. +func ChangeIssueWeight(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, weight int) error { + if issue.Weight == weight { + return nil + } + + if err := issues_model.UpdateIssueWeight(ctx, issue, weight, doer); err != nil { + return err + } + + return nil +} + // UpdateAssignees is a helper function to add or delete one or multiple issue assignee(s) // Deleting is done the GitHub way (quote from their api documentation): // https://developer.github.com/v3/issues/#edit-an-issue diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index f445ae3c6a103..e106f9b8f6743 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -2,6 +2,7 @@ {{range .Issue.Comments}} {{if call $.ShouldShowCommentType .Type}} {{$createdStr:= TimeSinceUnix .CreatedUnix ctx.Locale}} + {{$isWeightEnabled:= $.Repository.IsWeightEnabled ctx}}