From cafce3b4b5afb3f254a48e87f1516d7b5dc209b6 Mon Sep 17 00:00:00 2001 From: puni9869 <80308335+puni9869@users.noreply.github.com> Date: Mon, 14 Aug 2023 15:26:14 +0530 Subject: [PATCH] Allow to archive labels (#26478) ## Archived labels This adds the structure to allow for archived labels. Archived labels are, just like closed milestones or projects, a medium to hide information without deleting it. It is especially useful if there are outdated labels that should no longer be used without deleting the label entirely. ## Changes 1. UI and API have been equipped with the support to mark a label as archived 2. The time when a label has been archived will be stored in the DB ## Outsourced for the future There's no special handling for archived labels at the moment. This will be done in the future. ## Screenshots ![image](https://github.com/go-gitea/gitea/assets/80308335/208f95cd-42e4-4ed7-9a1f-cd2050a645d4) ![image](https://github.com/go-gitea/gitea/assets/80308335/746428e0-40bb-45b3-b992-85602feb371d) Part of https://github.com/go-gitea/gitea/issues/25237 --------- Co-authored-by: delvh Co-authored-by: wxiaoguang --- models/fixtures/label.yml | 9 +++++++++ models/issues/label.go | 18 +++++++++++++++++- models/issues/label_test.go | 13 ++++++++----- models/migrations/migrations.go | 2 ++ models/migrations/v1_21/v271.go | 16 ++++++++++++++++ modules/structs/issue_label.go | 6 ++++++ options/locale/locale_en-US.ini | 2 ++ routers/api/v1/org/label.go | 1 + routers/api/v1/repo/label.go | 3 ++- routers/web/org/org_labels.go | 1 + routers/web/repo/issue_label.go | 15 +++++++++------ routers/web/repo/issue_label_test.go | 7 ++++--- services/convert/issue.go | 1 + services/forms/repo_form.go | 1 + .../repo/issue/labels/edit_delete_label.tmpl | 10 ++++++++++ templates/repo/issue/labels/label_list.tmpl | 4 ++-- templates/swagger/v1_json.tmpl | 15 +++++++++++++++ web_src/js/features/comp/LabelEdit.js | 7 +++++-- 18 files changed, 111 insertions(+), 20 deletions(-) create mode 100644 models/migrations/v1_21/v271.go diff --git a/models/fixtures/label.yml b/models/fixtures/label.yml index ab4d5ef9440..2242b90dcdc 100644 --- a/models/fixtures/label.yml +++ b/models/fixtures/label.yml @@ -7,6 +7,7 @@ exclusive: false num_issues: 2 num_closed_issues: 0 + archived_unix: 0 - id: 2 @@ -17,6 +18,7 @@ exclusive: false num_issues: 1 num_closed_issues: 1 + archived_unix: 0 - id: 3 @@ -27,6 +29,7 @@ exclusive: false num_issues: 0 num_closed_issues: 0 + archived_unix: 0 - id: 4 @@ -37,6 +40,7 @@ exclusive: false num_issues: 1 num_closed_issues: 0 + archived_unix: 0 - id: 5 @@ -47,6 +51,7 @@ exclusive: false num_issues: 0 num_closed_issues: 0 + archived_unix: 0 - id: 6 @@ -57,6 +62,7 @@ exclusive: false num_issues: 0 num_closed_issues: 0 + archived_unix: 0 - id: 7 @@ -67,6 +73,7 @@ exclusive: true num_issues: 0 num_closed_issues: 0 + archived_unix: 0 - id: 8 @@ -77,6 +84,7 @@ exclusive: true num_issues: 0 num_closed_issues: 0 + archived_unix: 0 - id: 9 @@ -87,3 +95,4 @@ exclusive: true num_issues: 0 num_closed_issues: 0 + archived_unix: 0 diff --git a/models/issues/label.go b/models/issues/label.go index 57a2e67f8cd..70906efb47d 100644 --- a/models/issues/label.go +++ b/models/issues/label.go @@ -97,6 +97,8 @@ type Label struct { QueryString string `xorm:"-"` IsSelected bool `xorm:"-"` IsExcluded bool `xorm:"-"` + + ArchivedUnix timeutil.TimeStamp `xorm:"DEFAULT NULL"` } func init() { @@ -109,6 +111,15 @@ func (l *Label) CalOpenIssues() { l.NumOpenIssues = l.NumIssues - l.NumClosedIssues } +// SetArchived set the label as archived +func (l *Label) SetArchived(isArchived bool) { + if isArchived && l.ArchivedUnix.IsZero() { + l.ArchivedUnix = timeutil.TimeStampNow() + } else { + l.ArchivedUnix = timeutil.TimeStamp(0) + } +} + // CalOpenOrgIssues calculates the open issues of a label for a specific repo func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) { counts, _ := CountIssuesByRepo(ctx, &IssuesOptions{ @@ -153,6 +164,11 @@ func (l *Label) BelongsToOrg() bool { return l.OrgID > 0 } +// IsArchived returns true if label is an archived +func (l *Label) IsArchived() bool { + return l.ArchivedUnix > 0 +} + // BelongsToRepo returns true if label is a repository label func (l *Label) BelongsToRepo() bool { return l.RepoID > 0 @@ -211,7 +227,7 @@ func UpdateLabel(l *Label) error { } l.Color = color - return updateLabelCols(db.DefaultContext, l, "name", "description", "color", "exclusive") + return updateLabelCols(db.DefaultContext, l, "name", "description", "color", "exclusive", "archived_unix") } // DeleteLabel delete a label diff --git a/models/issues/label_test.go b/models/issues/label_test.go index 1bc5a1a9354..3f0e980b318 100644 --- a/models/issues/label_test.go +++ b/models/issues/label_test.go @@ -11,6 +11,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" ) @@ -259,11 +260,12 @@ func TestUpdateLabel(t *testing.T) { label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}) // make sure update wont overwrite it update := &issues_model.Label{ - ID: label.ID, - Color: "#ffff00", - Name: "newLabelName", - Description: label.Description, - Exclusive: false, + ID: label.ID, + Color: "#ffff00", + Name: "newLabelName", + Description: label.Description, + Exclusive: false, + ArchivedUnix: timeutil.TimeStamp(0), } label.Color = update.Color label.Name = update.Name @@ -273,6 +275,7 @@ func TestUpdateLabel(t *testing.T) { assert.EqualValues(t, label.Color, newLabel.Color) assert.EqualValues(t, label.Name, newLabel.Name) assert.EqualValues(t, label.Description, newLabel.Description) + assert.EqualValues(t, newLabel.ArchivedUnix, 0) unittest.CheckConsistencyFor(t, &issues_model.Label{}, &repo_model.Repository{}) } diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 55107439b04..7a126593d14 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -522,6 +522,8 @@ var migrations = []Migration{ NewMigration("Drop deleted branch table", v1_21.DropDeletedBranchTable), // v270 -> v271 NewMigration("Fix PackageProperty typo", v1_21.FixPackagePropertyTypo), + // v271 -> v272 + NewMigration("Allow archiving labels", v1_21.AddArchivedUnixColumInLabelTable), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v1_21/v271.go b/models/migrations/v1_21/v271.go new file mode 100644 index 00000000000..098f6499d57 --- /dev/null +++ b/models/migrations/v1_21/v271.go @@ -0,0 +1,16 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_21 //nolint +import ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func AddArchivedUnixColumInLabelTable(x *xorm.Engine) error { + type Label struct { + ArchivedUnix timeutil.TimeStamp `xorm:"DEFAULT NULL"` + } + return x.Sync(new(Label)) +} diff --git a/modules/structs/issue_label.go b/modules/structs/issue_label.go index 2610d3e93f1..bf68726d793 100644 --- a/modules/structs/issue_label.go +++ b/modules/structs/issue_label.go @@ -11,6 +11,8 @@ type Label struct { Name string `json:"name"` // example: false Exclusive bool `json:"exclusive"` + // example: false + IsArchived bool `json:"is_archived"` // example: 00aabb Color string `json:"color"` Description string `json:"description"` @@ -27,6 +29,8 @@ type CreateLabelOption struct { // example: #00aabb Color string `json:"color" binding:"Required"` Description string `json:"description"` + // example: false + IsArchived bool `json:"is_archived"` } // EditLabelOption options for editing a label @@ -37,6 +41,8 @@ type EditLabelOption struct { // example: #00aabb Color *string `json:"color"` Description *string `json:"description"` + // example: false + IsArchived *bool `json:"is_archived"` } // IssueLabelsOption a collection of labels diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 30fa899c9d8..daf22d9fea8 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1491,6 +1491,8 @@ issues.label_title = Name issues.label_description = Description issues.label_color = Color issues.label_exclusive = Exclusive +issues.label_archive = Archive Label +issues.label_archive_tooltip= Archived labels are excluded from the label search when applying labels to an issue. Existing labels on issues remain unaffected, allowing you to retire obsolete labels without losing information. issues.label_exclusive_desc = Name the label scope/item to make it mutually exclusive with other scope/ labels. issues.label_exclusive_warning = Any conflicting scoped labels will be removed when editing the labels of an issue or pull request. issues.label_count = %d labels diff --git a/routers/api/v1/org/label.go b/routers/api/v1/org/label.go index 183c1e6cc8a..9ef28d4db92 100644 --- a/routers/api/v1/org/label.go +++ b/routers/api/v1/org/label.go @@ -209,6 +209,7 @@ func EditLabel(ctx *context.APIContext) { if form.Description != nil { l.Description = *form.Description } + l.SetArchived(form.IsArchived != nil && *form.IsArchived) if err := issues_model.UpdateLabel(l); err != nil { ctx.Error(http.StatusInternalServerError, "UpdateLabel", err) return diff --git a/routers/api/v1/repo/label.go b/routers/api/v1/repo/label.go index 6cb231f596c..fc9a16b58aa 100644 --- a/routers/api/v1/repo/label.go +++ b/routers/api/v1/repo/label.go @@ -151,7 +151,6 @@ func CreateLabel(ctx *context.APIContext) { return } form.Color = color - l := &issues_model.Label{ Name: form.Name, Exclusive: form.Exclusive, @@ -159,6 +158,7 @@ func CreateLabel(ctx *context.APIContext) { RepoID: ctx.Repo.Repository.ID, Description: form.Description, } + l.SetArchived(form.IsArchived) if err := issues_model.NewLabel(ctx, l); err != nil { ctx.Error(http.StatusInternalServerError, "NewLabel", err) return @@ -231,6 +231,7 @@ func EditLabel(ctx *context.APIContext) { if form.Description != nil { l.Description = *form.Description } + l.SetArchived(form.IsArchived != nil && *form.IsArchived) if err := issues_model.UpdateLabel(l); err != nil { ctx.Error(http.StatusInternalServerError, "UpdateLabel", err) return diff --git a/routers/web/org/org_labels.go b/routers/web/org/org_labels.go index a9f9e963d49..2c7725e38da 100644 --- a/routers/web/org/org_labels.go +++ b/routers/web/org/org_labels.go @@ -75,6 +75,7 @@ func UpdateLabel(ctx *context.Context) { l.Exclusive = form.Exclusive l.Description = form.Description l.Color = form.Color + l.SetArchived(form.IsArchived) if err := issues_model.UpdateLabel(l); err != nil { ctx.ServerError("UpdateLabel", err) return diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go index 5d326bab580..257610d3af5 100644 --- a/routers/web/repo/issue_label.go +++ b/routers/web/repo/issue_label.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/label" "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/forms" issue_service "code.gitea.io/gitea/services/issue" @@ -111,11 +112,12 @@ func NewLabel(ctx *context.Context) { } l := &issues_model.Label{ - RepoID: ctx.Repo.Repository.ID, - Name: form.Title, - Exclusive: form.Exclusive, - Description: form.Description, - Color: form.Color, + RepoID: ctx.Repo.Repository.ID, + Name: form.Title, + Exclusive: form.Exclusive, + Description: form.Description, + Color: form.Color, + ArchivedUnix: timeutil.TimeStamp(0), } if err := issues_model.NewLabel(ctx, l); err != nil { ctx.ServerError("NewLabel", err) @@ -137,11 +139,12 @@ func UpdateLabel(ctx *context.Context) { } return } - l.Name = form.Title l.Exclusive = form.Exclusive l.Description = form.Description l.Color = form.Color + + l.SetArchived(form.IsArchived) if err := issues_model.UpdateLabel(l); err != nil { ctx.ServerError("UpdateLabel", err) return diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go index 4c9a3594380..e29582f9687 100644 --- a/routers/web/repo/issue_label_test.go +++ b/routers/web/repo/issue_label_test.go @@ -97,9 +97,10 @@ func TestUpdateLabel(t *testing.T) { test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) web.SetForm(ctx, &forms.CreateLabelForm{ - ID: 2, - Title: "newnameforlabel", - Color: "#abcdef", + ID: 2, + Title: "newnameforlabel", + Color: "#abcdef", + IsArchived: true, }) UpdateLabel(ctx) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) diff --git a/services/convert/issue.go b/services/convert/issue.go index d81840f0252..33fad31d48a 100644 --- a/services/convert/issue.go +++ b/services/convert/issue.go @@ -208,6 +208,7 @@ func ToLabel(label *issues_model.Label, repo *repo_model.Repository, org *user_m Exclusive: label.Exclusive, Color: strings.TrimLeft(label.Color, "#"), Description: label.Description, + IsArchived: label.IsArchived(), } // calculate URL diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 8c763e17cbb..b36c8cc9b66 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -569,6 +569,7 @@ type CreateLabelForm struct { ID int64 Title string `binding:"Required;MaxSize(50)" locale:"repo.issues.label_title"` Exclusive bool `form:"exclusive"` + IsArchived bool `form:"is_archived"` Description string `binding:"MaxSize(200)" locale:"repo.issues.label_description"` Color string `binding:"Required;MaxSize(7)" locale:"repo.issues.label_color"` } diff --git a/templates/repo/issue/labels/edit_delete_label.tmpl b/templates/repo/issue/labels/edit_delete_label.tmpl index b4eb6be7fcd..d64782090cc 100644 --- a/templates/repo/issue/labels/edit_delete_label.tmpl +++ b/templates/repo/issue/labels/edit_delete_label.tmpl @@ -33,6 +33,16 @@
{{svg "octicon-alert"}} {{.locale.Tr "repo.issues.label_exclusive_warning" | Safe}}
+
+ +
+
+ + +
+ + {{svg "octicon-info"}} +
diff --git a/templates/repo/issue/labels/label_list.tmpl b/templates/repo/issue/labels/label_list.tmpl index 9eee95be923..c15833d952d 100644 --- a/templates/repo/issue/labels/label_list.tmpl +++ b/templates/repo/issue/labels/label_list.tmpl @@ -44,10 +44,10 @@
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 8cf5332bafc..a5bea8a4cb9 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -17057,6 +17057,11 @@ "x-go-name": "Exclusive", "example": false }, + "is_archived": { + "type": "boolean", + "x-go-name": "IsArchived", + "example": false + }, "name": { "type": "string", "x-go-name": "Name" @@ -18001,6 +18006,11 @@ "x-go-name": "Exclusive", "example": false }, + "is_archived": { + "type": "boolean", + "x-go-name": "IsArchived", + "example": false + }, "name": { "type": "string", "x-go-name": "Name" @@ -19479,6 +19489,11 @@ "format": "int64", "x-go-name": "ID" }, + "is_archived": { + "type": "boolean", + "x-go-name": "IsArchived", + "example": false + }, "name": { "type": "string", "x-go-name": "Name" diff --git a/web_src/js/features/comp/LabelEdit.js b/web_src/js/features/comp/LabelEdit.js index 18676d25e6c..2a22190e10d 100644 --- a/web_src/js/features/comp/LabelEdit.js +++ b/web_src/js/features/comp/LabelEdit.js @@ -36,7 +36,7 @@ export function initCompLabelEdit(selector) { $('.new-label.modal').modal({ onApprove() { $('.new-label.form').trigger('submit'); - } + }, }).modal('show'); return false; }); @@ -49,6 +49,9 @@ export function initCompLabelEdit(selector) { const nameInput = $('.edit-label .label-name-input'); nameInput.val($(this).data('title')); + const isArchivedCheckbox = $('.edit-label .label-is-archived-input'); + isArchivedCheckbox.prop('checked', this.hasAttribute('data-is-archived')); + const exclusiveCheckbox = $('.edit-label .label-exclusive-input'); exclusiveCheckbox.prop('checked', this.hasAttribute('data-exclusive')); // Warn when label was previously not exclusive and used in issues @@ -64,7 +67,7 @@ export function initCompLabelEdit(selector) { $('.edit-label.modal').modal({ onApprove() { $('.edit-label.form').trigger('submit'); - } + }, }).modal('show'); return false; });