loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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 <dev.lh@web.de>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>

authored by

puni9869
delvh
wxiaoguang
and committed by
GitHub
cafce3b4 db7b0a1a

+111 -20
+9
models/fixtures/label.yml
··· 7 7 exclusive: false 8 8 num_issues: 2 9 9 num_closed_issues: 0 10 + archived_unix: 0 10 11 11 12 - 12 13 id: 2 ··· 17 18 exclusive: false 18 19 num_issues: 1 19 20 num_closed_issues: 1 21 + archived_unix: 0 20 22 21 23 - 22 24 id: 3 ··· 27 29 exclusive: false 28 30 num_issues: 0 29 31 num_closed_issues: 0 32 + archived_unix: 0 30 33 31 34 - 32 35 id: 4 ··· 37 40 exclusive: false 38 41 num_issues: 1 39 42 num_closed_issues: 0 43 + archived_unix: 0 40 44 41 45 - 42 46 id: 5 ··· 47 51 exclusive: false 48 52 num_issues: 0 49 53 num_closed_issues: 0 54 + archived_unix: 0 50 55 51 56 - 52 57 id: 6 ··· 57 62 exclusive: false 58 63 num_issues: 0 59 64 num_closed_issues: 0 65 + archived_unix: 0 60 66 61 67 - 62 68 id: 7 ··· 67 73 exclusive: true 68 74 num_issues: 0 69 75 num_closed_issues: 0 76 + archived_unix: 0 70 77 71 78 - 72 79 id: 8 ··· 77 84 exclusive: true 78 85 num_issues: 0 79 86 num_closed_issues: 0 87 + archived_unix: 0 80 88 81 89 - 82 90 id: 9 ··· 87 95 exclusive: true 88 96 num_issues: 0 89 97 num_closed_issues: 0 98 + archived_unix: 0
+17 -1
models/issues/label.go
··· 97 97 QueryString string `xorm:"-"` 98 98 IsSelected bool `xorm:"-"` 99 99 IsExcluded bool `xorm:"-"` 100 + 101 + ArchivedUnix timeutil.TimeStamp `xorm:"DEFAULT NULL"` 100 102 } 101 103 102 104 func init() { ··· 109 111 l.NumOpenIssues = l.NumIssues - l.NumClosedIssues 110 112 } 111 113 114 + // SetArchived set the label as archived 115 + func (l *Label) SetArchived(isArchived bool) { 116 + if isArchived && l.ArchivedUnix.IsZero() { 117 + l.ArchivedUnix = timeutil.TimeStampNow() 118 + } else { 119 + l.ArchivedUnix = timeutil.TimeStamp(0) 120 + } 121 + } 122 + 112 123 // CalOpenOrgIssues calculates the open issues of a label for a specific repo 113 124 func (l *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64) { 114 125 counts, _ := CountIssuesByRepo(ctx, &IssuesOptions{ ··· 151 162 // BelongsToOrg returns true if label is an organization label 152 163 func (l *Label) BelongsToOrg() bool { 153 164 return l.OrgID > 0 165 + } 166 + 167 + // IsArchived returns true if label is an archived 168 + func (l *Label) IsArchived() bool { 169 + return l.ArchivedUnix > 0 154 170 } 155 171 156 172 // BelongsToRepo returns true if label is a repository label ··· 211 227 } 212 228 l.Color = color 213 229 214 - return updateLabelCols(db.DefaultContext, l, "name", "description", "color", "exclusive") 230 + return updateLabelCols(db.DefaultContext, l, "name", "description", "color", "exclusive", "archived_unix") 215 231 } 216 232 217 233 // DeleteLabel delete a label
+8 -5
models/issues/label_test.go
··· 11 11 repo_model "code.gitea.io/gitea/models/repo" 12 12 "code.gitea.io/gitea/models/unittest" 13 13 user_model "code.gitea.io/gitea/models/user" 14 + "code.gitea.io/gitea/modules/timeutil" 14 15 15 16 "github.com/stretchr/testify/assert" 16 17 ) ··· 259 260 label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}) 260 261 // make sure update wont overwrite it 261 262 update := &issues_model.Label{ 262 - ID: label.ID, 263 - Color: "#ffff00", 264 - Name: "newLabelName", 265 - Description: label.Description, 266 - Exclusive: false, 263 + ID: label.ID, 264 + Color: "#ffff00", 265 + Name: "newLabelName", 266 + Description: label.Description, 267 + Exclusive: false, 268 + ArchivedUnix: timeutil.TimeStamp(0), 267 269 } 268 270 label.Color = update.Color 269 271 label.Name = update.Name ··· 273 275 assert.EqualValues(t, label.Color, newLabel.Color) 274 276 assert.EqualValues(t, label.Name, newLabel.Name) 275 277 assert.EqualValues(t, label.Description, newLabel.Description) 278 + assert.EqualValues(t, newLabel.ArchivedUnix, 0) 276 279 unittest.CheckConsistencyFor(t, &issues_model.Label{}, &repo_model.Repository{}) 277 280 } 278 281
+2
models/migrations/migrations.go
··· 522 522 NewMigration("Drop deleted branch table", v1_21.DropDeletedBranchTable), 523 523 // v270 -> v271 524 524 NewMigration("Fix PackageProperty typo", v1_21.FixPackagePropertyTypo), 525 + // v271 -> v272 526 + NewMigration("Allow archiving labels", v1_21.AddArchivedUnixColumInLabelTable), 525 527 } 526 528 527 529 // GetCurrentDBVersion returns the current db version
+16
models/migrations/v1_21/v271.go
··· 1 + // Copyright 2023 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package v1_21 //nolint 5 + import ( 6 + "code.gitea.io/gitea/modules/timeutil" 7 + 8 + "xorm.io/xorm" 9 + ) 10 + 11 + func AddArchivedUnixColumInLabelTable(x *xorm.Engine) error { 12 + type Label struct { 13 + ArchivedUnix timeutil.TimeStamp `xorm:"DEFAULT NULL"` 14 + } 15 + return x.Sync(new(Label)) 16 + }
+6
modules/structs/issue_label.go
··· 11 11 Name string `json:"name"` 12 12 // example: false 13 13 Exclusive bool `json:"exclusive"` 14 + // example: false 15 + IsArchived bool `json:"is_archived"` 14 16 // example: 00aabb 15 17 Color string `json:"color"` 16 18 Description string `json:"description"` ··· 27 29 // example: #00aabb 28 30 Color string `json:"color" binding:"Required"` 29 31 Description string `json:"description"` 32 + // example: false 33 + IsArchived bool `json:"is_archived"` 30 34 } 31 35 32 36 // EditLabelOption options for editing a label ··· 37 41 // example: #00aabb 38 42 Color *string `json:"color"` 39 43 Description *string `json:"description"` 44 + // example: false 45 + IsArchived *bool `json:"is_archived"` 40 46 } 41 47 42 48 // IssueLabelsOption a collection of labels
+2
options/locale/locale_en-US.ini
··· 1491 1491 issues.label_description = Description 1492 1492 issues.label_color = Color 1493 1493 issues.label_exclusive = Exclusive 1494 + issues.label_archive = Archive Label 1495 + 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. 1494 1496 issues.label_exclusive_desc = Name the label <code>scope/item</code> to make it mutually exclusive with other <code>scope/</code> labels. 1495 1497 issues.label_exclusive_warning = Any conflicting scoped labels will be removed when editing the labels of an issue or pull request. 1496 1498 issues.label_count = %d labels
+1
routers/api/v1/org/label.go
··· 209 209 if form.Description != nil { 210 210 l.Description = *form.Description 211 211 } 212 + l.SetArchived(form.IsArchived != nil && *form.IsArchived) 212 213 if err := issues_model.UpdateLabel(l); err != nil { 213 214 ctx.Error(http.StatusInternalServerError, "UpdateLabel", err) 214 215 return
+2 -1
routers/api/v1/repo/label.go
··· 151 151 return 152 152 } 153 153 form.Color = color 154 - 155 154 l := &issues_model.Label{ 156 155 Name: form.Name, 157 156 Exclusive: form.Exclusive, ··· 159 158 RepoID: ctx.Repo.Repository.ID, 160 159 Description: form.Description, 161 160 } 161 + l.SetArchived(form.IsArchived) 162 162 if err := issues_model.NewLabel(ctx, l); err != nil { 163 163 ctx.Error(http.StatusInternalServerError, "NewLabel", err) 164 164 return ··· 231 231 if form.Description != nil { 232 232 l.Description = *form.Description 233 233 } 234 + l.SetArchived(form.IsArchived != nil && *form.IsArchived) 234 235 if err := issues_model.UpdateLabel(l); err != nil { 235 236 ctx.Error(http.StatusInternalServerError, "UpdateLabel", err) 236 237 return
+1
routers/web/org/org_labels.go
··· 75 75 l.Exclusive = form.Exclusive 76 76 l.Description = form.Description 77 77 l.Color = form.Color 78 + l.SetArchived(form.IsArchived) 78 79 if err := issues_model.UpdateLabel(l); err != nil { 79 80 ctx.ServerError("UpdateLabel", err) 80 81 return
+9 -6
routers/web/repo/issue_label.go
··· 14 14 "code.gitea.io/gitea/modules/label" 15 15 "code.gitea.io/gitea/modules/log" 16 16 repo_module "code.gitea.io/gitea/modules/repository" 17 + "code.gitea.io/gitea/modules/timeutil" 17 18 "code.gitea.io/gitea/modules/web" 18 19 "code.gitea.io/gitea/services/forms" 19 20 issue_service "code.gitea.io/gitea/services/issue" ··· 111 112 } 112 113 113 114 l := &issues_model.Label{ 114 - RepoID: ctx.Repo.Repository.ID, 115 - Name: form.Title, 116 - Exclusive: form.Exclusive, 117 - Description: form.Description, 118 - Color: form.Color, 115 + RepoID: ctx.Repo.Repository.ID, 116 + Name: form.Title, 117 + Exclusive: form.Exclusive, 118 + Description: form.Description, 119 + Color: form.Color, 120 + ArchivedUnix: timeutil.TimeStamp(0), 119 121 } 120 122 if err := issues_model.NewLabel(ctx, l); err != nil { 121 123 ctx.ServerError("NewLabel", err) ··· 137 139 } 138 140 return 139 141 } 140 - 141 142 l.Name = form.Title 142 143 l.Exclusive = form.Exclusive 143 144 l.Description = form.Description 144 145 l.Color = form.Color 146 + 147 + l.SetArchived(form.IsArchived) 145 148 if err := issues_model.UpdateLabel(l); err != nil { 146 149 ctx.ServerError("UpdateLabel", err) 147 150 return
+4 -3
routers/web/repo/issue_label_test.go
··· 97 97 test.LoadUser(t, ctx, 2) 98 98 test.LoadRepo(t, ctx, 1) 99 99 web.SetForm(ctx, &forms.CreateLabelForm{ 100 - ID: 2, 101 - Title: "newnameforlabel", 102 - Color: "#abcdef", 100 + ID: 2, 101 + Title: "newnameforlabel", 102 + Color: "#abcdef", 103 + IsArchived: true, 103 104 }) 104 105 UpdateLabel(ctx) 105 106 assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status())
+1
services/convert/issue.go
··· 208 208 Exclusive: label.Exclusive, 209 209 Color: strings.TrimLeft(label.Color, "#"), 210 210 Description: label.Description, 211 + IsArchived: label.IsArchived(), 211 212 } 212 213 213 214 // calculate URL
+1
services/forms/repo_form.go
··· 569 569 ID int64 570 570 Title string `binding:"Required;MaxSize(50)" locale:"repo.issues.label_title"` 571 571 Exclusive bool `form:"exclusive"` 572 + IsArchived bool `form:"is_archived"` 572 573 Description string `binding:"MaxSize(200)" locale:"repo.issues.label_description"` 573 574 Color string `binding:"Required;MaxSize(7)" locale:"repo.issues.label_color"` 574 575 }
+10
templates/repo/issue/labels/edit_delete_label.tmpl
··· 33 33 <div class="desc gt-ml-2 gt-mt-3 gt-hidden label-exclusive-warning"> 34 34 {{svg "octicon-alert"}} {{.locale.Tr "repo.issues.label_exclusive_warning" | Safe}} 35 35 </div> 36 + <br> 37 + </div> 38 + <div class="field label-is-archived-input-field"> 39 + <div class="ui checkbox"> 40 + <input class="label-is-archived-input" name="is_archived" type="checkbox"> 41 + <label>{{.locale.Tr "repo.issues.label_archive"}}</label> 42 + </div> 43 + <i class="gt-ml-2" data-tooltip-content={{.locale.Tr "repo.issues.label_archive_tooltip"}}> 44 + {{svg "octicon-info"}} 45 + </i> 36 46 </div> 37 47 <div class="field"> 38 48 <label for="description">{{.locale.Tr "repo.issues.label_description"}}</label>
+2 -2
templates/repo/issue/labels/label_list.tmpl
··· 44 44 </div> 45 45 <div class="label-operation"> 46 46 {{if and (not $.PageIsOrgSettingsLabels) (not $.Repository.IsArchived) (or $.CanWriteIssues $.CanWritePulls)}} 47 - <a class="edit-label-button" href="#" data-id="{{.ID}}" data-title="{{.Name}}" {{if .Exclusive}}data-exclusive{{end}} data-num-issues="{{.NumIssues}}" data-description="{{.Description}}" data-color={{.Color}}>{{svg "octicon-pencil"}} {{$.locale.Tr "repo.issues.label_edit"}}</a> 47 + <a class="edit-label-button" href="#" data-id="{{.ID}}" data-title="{{.Name}}" {{if .Exclusive}}data-exclusive{{end}} {{if gt .ArchivedUnix 0}}data-is-archived{{end}} data-num-issues="{{.NumIssues}}" data-description="{{.Description}}" data-color={{.Color}}>{{svg "octicon-pencil"}} {{$.locale.Tr "repo.issues.label_edit"}}</a> 48 48 <a class="delete-button" href="#" data-url="{{$.Link}}/delete" data-id="{{.ID}}">{{svg "octicon-trash"}} {{$.locale.Tr "repo.issues.label_delete"}}</a> 49 49 {{else if $.PageIsOrgSettingsLabels}} 50 - <a class="edit-label-button" href="#" data-id="{{.ID}}" data-title="{{.Name}}" {{if .Exclusive}}data-exclusive{{end}} data-num-issues="{{.NumIssues}}" data-description="{{.Description}}" data-color={{.Color}}>{{svg "octicon-pencil"}} {{$.locale.Tr "repo.issues.label_edit"}}</a> 50 + <a class="edit-label-button" href="#" data-id="{{.ID}}" data-title="{{.Name}}" {{if .Exclusive}}data-exclusive{{end}} {{if gt .ArchivedUnix 0}}data-is-archived{{end}} data-num-issues="{{.NumIssues}}" data-description="{{.Description}}" data-color={{.Color}}>{{svg "octicon-pencil"}} {{$.locale.Tr "repo.issues.label_edit"}}</a> 51 51 <a class="delete-button" href="#" data-url="{{$.Link}}/delete" data-id="{{.ID}}">{{svg "octicon-trash"}} {{$.locale.Tr "repo.issues.label_delete"}}</a> 52 52 {{end}} 53 53 </div>
+15
templates/swagger/v1_json.tmpl
··· 17057 17057 "x-go-name": "Exclusive", 17058 17058 "example": false 17059 17059 }, 17060 + "is_archived": { 17061 + "type": "boolean", 17062 + "x-go-name": "IsArchived", 17063 + "example": false 17064 + }, 17060 17065 "name": { 17061 17066 "type": "string", 17062 17067 "x-go-name": "Name" ··· 17999 18004 "exclusive": { 18000 18005 "type": "boolean", 18001 18006 "x-go-name": "Exclusive", 18007 + "example": false 18008 + }, 18009 + "is_archived": { 18010 + "type": "boolean", 18011 + "x-go-name": "IsArchived", 18002 18012 "example": false 18003 18013 }, 18004 18014 "name": { ··· 19478 19488 "type": "integer", 19479 19489 "format": "int64", 19480 19490 "x-go-name": "ID" 19491 + }, 19492 + "is_archived": { 19493 + "type": "boolean", 19494 + "x-go-name": "IsArchived", 19495 + "example": false 19481 19496 }, 19482 19497 "name": { 19483 19498 "type": "string",
+5 -2
web_src/js/features/comp/LabelEdit.js
··· 36 36 $('.new-label.modal').modal({ 37 37 onApprove() { 38 38 $('.new-label.form').trigger('submit'); 39 - } 39 + }, 40 40 }).modal('show'); 41 41 return false; 42 42 }); ··· 48 48 49 49 const nameInput = $('.edit-label .label-name-input'); 50 50 nameInput.val($(this).data('title')); 51 + 52 + const isArchivedCheckbox = $('.edit-label .label-is-archived-input'); 53 + isArchivedCheckbox.prop('checked', this.hasAttribute('data-is-archived')); 51 54 52 55 const exclusiveCheckbox = $('.edit-label .label-exclusive-input'); 53 56 exclusiveCheckbox.prop('checked', this.hasAttribute('data-exclusive')); ··· 64 67 $('.edit-label.modal').modal({ 65 68 onApprove() { 66 69 $('.edit-label.form').trigger('submit'); 67 - } 70 + }, 68 71 }).modal('show'); 69 72 return false; 70 73 });