mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-27 00:23:41 +09:00 
			
		
		
		
	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   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>
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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{}) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										16
									
								
								models/migrations/v1_21/v271.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								models/migrations/v1_21/v271.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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)) | ||||
| } | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 <code>scope/item</code> to make it mutually exclusive with other <code>scope/</code> 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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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()) | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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"` | ||||
| } | ||||
|   | ||||
| @@ -33,6 +33,16 @@ | ||||
| 				<div class="desc gt-ml-2 gt-mt-3 gt-hidden label-exclusive-warning"> | ||||
| 					{{svg "octicon-alert"}} {{.locale.Tr "repo.issues.label_exclusive_warning" | Safe}} | ||||
| 				</div> | ||||
| 				<br> | ||||
| 			</div> | ||||
| 			<div class="field label-is-archived-input-field"> | ||||
| 				<div class="ui checkbox"> | ||||
| 					<input class="label-is-archived-input" name="is_archived" type="checkbox"> | ||||
| 					<label>{{.locale.Tr "repo.issues.label_archive"}}</label> | ||||
| 				</div> | ||||
| 				<i class="gt-ml-2" data-tooltip-content={{.locale.Tr "repo.issues.label_archive_tooltip"}}> | ||||
| 					{{svg "octicon-info"}} | ||||
| 				</i> | ||||
| 			</div> | ||||
| 			<div class="field"> | ||||
| 				<label for="description">{{.locale.Tr "repo.issues.label_description"}}</label> | ||||
|   | ||||
| @@ -44,10 +44,10 @@ | ||||
| 			</div> | ||||
| 			<div class="label-operation"> | ||||
| 				{{if and (not $.PageIsOrgSettingsLabels) (not $.Repository.IsArchived) (or $.CanWriteIssues $.CanWritePulls)}} | ||||
| 					<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> | ||||
| 					<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> | ||||
| 					<a class="delete-button" href="#" data-url="{{$.Link}}/delete" data-id="{{.ID}}">{{svg "octicon-trash"}} {{$.locale.Tr "repo.issues.label_delete"}}</a> | ||||
| 				{{else if $.PageIsOrgSettingsLabels}} | ||||
| 					<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> | ||||
| 					<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> | ||||
| 					<a class="delete-button" href="#" data-url="{{$.Link}}/delete" data-id="{{.ID}}">{{svg "octicon-trash"}} {{$.locale.Tr "repo.issues.label_delete"}}</a> | ||||
| 				{{end}} | ||||
| 			</div> | ||||
|   | ||||
							
								
								
									
										15
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										15
									
								
								templates/swagger/v1_json.tmpl
									
									
									
										generated
									
									
									
								
							| @@ -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" | ||||
|   | ||||
| @@ -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; | ||||
|   }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user