mirror of
https://github.com/go-gitea/gitea.git
synced 2025-02-20 11:43:57 +08:00
Merge f54af1e44c6aec49c36d26f2c046775f6c9a15ee into 21af8150b7ba315a9f75264ab77813b0b7c697a8
This commit is contained in:
commit
4db82002a2
@ -70,6 +70,15 @@ func (o *IssuesOptions) Copy(edit ...func(options *IssuesOptions)) *IssuesOption
|
||||
// applySorts sort an issues-related session based on the provided
|
||||
// sortType string
|
||||
func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) {
|
||||
// Since this sortType is dynamically created, it has to be treated specially.
|
||||
if strings.HasPrefix(sortType, "scope-") {
|
||||
scope := strings.TrimPrefix(sortType, "scope-")
|
||||
sess.Join("LEFT", "issue_label", "issue.id = issue_label.issue_id")
|
||||
sess.Join("LEFT", "label", "label.id = issue_label.label_id and label.name LIKE ?", scope+"/%")
|
||||
sess.Asc("label.exclusive_order").Desc("issue.id")
|
||||
return // EARLY RETURN
|
||||
}
|
||||
|
||||
switch sortType {
|
||||
case "oldest":
|
||||
sess.Asc("issue.created_unix").Asc("issue.id")
|
||||
|
@ -87,6 +87,7 @@ type Label struct {
|
||||
OrgID int64 `xorm:"INDEX"`
|
||||
Name string
|
||||
Exclusive bool
|
||||
ExclusiveOrder int `xorm:"DEFAULT 0"`
|
||||
Description string
|
||||
Color string `xorm:"VARCHAR(7)"`
|
||||
NumIssues int
|
||||
@ -236,7 +237,7 @@ func UpdateLabel(ctx context.Context, l *Label) error {
|
||||
}
|
||||
l.Color = color
|
||||
|
||||
return updateLabelCols(ctx, l, "name", "description", "color", "exclusive", "archived_unix")
|
||||
return updateLabelCols(ctx, l, "name", "description", "color", "exclusive", "exclusive_order", "archived_unix")
|
||||
}
|
||||
|
||||
// DeleteLabel delete a label
|
||||
|
@ -374,6 +374,7 @@ func prepareMigrationTasks() []*migration {
|
||||
// Gitea 1.23.0-rc0 ends at migration ID number 311 (database version 312)
|
||||
newMigration(312, "Add DeleteBranchAfterMerge to AutoMerge", v1_24.AddDeleteBranchAfterMergeForAutoMerge),
|
||||
newMigration(313, "Move PinOrder from issue table to a new table issue_pin", v1_24.MovePinOrderToTableIssuePin),
|
||||
newMigration(314, "Add ExclusiveOrder to Label table", v1_24.AddExclusiveOrderColumnToLabelTable),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
16
models/migrations/v1_24/v314.go
Normal file
16
models/migrations/v1_24/v314.go
Normal file
@ -0,0 +1,16 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_24 //nolint
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddExclusiveOrderColumnToLabelTable(x *xorm.Engine) error {
|
||||
type Label struct {
|
||||
ExclusiveOrder int64 `xorm:"DEFAULT 0"`
|
||||
}
|
||||
|
||||
return x.Sync(new(Label))
|
||||
}
|
@ -6,6 +6,7 @@ package db
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issue_model "code.gitea.io/gitea/models/issues"
|
||||
@ -34,7 +35,11 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
|
||||
case internal.SortByDeadlineAsc:
|
||||
sortType = "nearduedate"
|
||||
default:
|
||||
sortType = "newest"
|
||||
if strings.HasPrefix(string(options.SortBy), "scope-") {
|
||||
sortType = string(options.SortBy)
|
||||
} else {
|
||||
sortType = "newest"
|
||||
}
|
||||
}
|
||||
|
||||
// See the comment of issues_model.SearchOptions for the reason why we need to convert
|
||||
|
@ -4,8 +4,11 @@
|
||||
package issues
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/indexer/issues/internal"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
)
|
||||
|
||||
@ -99,7 +102,11 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
|
||||
// Unsupported sort type for search
|
||||
fallthrough
|
||||
default:
|
||||
searchOpt.SortBy = SortByUpdatedDesc
|
||||
if strings.HasPrefix(opts.SortType, "scope-") {
|
||||
searchOpt.SortBy = internal.SortBy(opts.SortType)
|
||||
} else {
|
||||
searchOpt.SortBy = SortByUpdatedDesc
|
||||
}
|
||||
}
|
||||
|
||||
return searchOpt
|
||||
|
@ -14,10 +14,11 @@ var colorPattern = regexp.MustCompile("^#?(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$")
|
||||
|
||||
// Label represents label information loaded from template
|
||||
type Label struct {
|
||||
Name string `yaml:"name"`
|
||||
Color string `yaml:"color"`
|
||||
Description string `yaml:"description,omitempty"`
|
||||
Exclusive bool `yaml:"exclusive,omitempty"`
|
||||
Name string `yaml:"name"`
|
||||
Color string `yaml:"color"`
|
||||
Description string `yaml:"description,omitempty"`
|
||||
Exclusive bool `yaml:"exclusive,omitempty"`
|
||||
ExclusiveOrder int `yaml:"exclusive_order,omitempty"`
|
||||
}
|
||||
|
||||
// NormalizeColor normalizes a color string to a 6-character hex code
|
||||
|
@ -154,10 +154,11 @@ func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg
|
||||
labels := make([]*issues_model.Label, len(list))
|
||||
for i := 0; i < len(list); i++ {
|
||||
labels[i] = &issues_model.Label{
|
||||
Name: list[i].Name,
|
||||
Exclusive: list[i].Exclusive,
|
||||
Description: list[i].Description,
|
||||
Color: list[i].Color,
|
||||
Name: list[i].Name,
|
||||
Exclusive: list[i].Exclusive,
|
||||
ExclusiveOrder: list[i].ExclusiveOrder,
|
||||
Description: list[i].Description,
|
||||
Color: list[i].Color,
|
||||
}
|
||||
if isOrg {
|
||||
labels[i].OrgID = id
|
||||
|
@ -22,49 +22,60 @@ labels:
|
||||
description: Breaking change that won't be backward compatible
|
||||
- name: "Reviewed/Duplicate"
|
||||
exclusive: true
|
||||
exclusive_order: 50
|
||||
color: 616161
|
||||
description: This issue or pull request already exists
|
||||
- name: "Reviewed/Invalid"
|
||||
exclusive: true
|
||||
exclusive_order: 100
|
||||
color: 546e7a
|
||||
description: Invalid issue
|
||||
- name: "Reviewed/Confirmed"
|
||||
exclusive: true
|
||||
exclusive_order: 0
|
||||
color: 795548
|
||||
description: Issue has been confirmed
|
||||
- name: "Reviewed/Won't Fix"
|
||||
exclusive: true
|
||||
exclusive_order: 75
|
||||
color: eeeeee
|
||||
description: This issue won't be fixed
|
||||
- name: "Status/Need More Info"
|
||||
exclusive: true
|
||||
exclusive_order: 25
|
||||
color: 424242
|
||||
description: Feedback is required to reproduce issue or to continue work
|
||||
- name: "Status/Blocked"
|
||||
exclusive: true
|
||||
exclusive_order: 0
|
||||
color: 880e4f
|
||||
description: Something is blocking this issue or pull request
|
||||
- name: "Status/Abandoned"
|
||||
exclusive: true
|
||||
exclusive_order: 100
|
||||
color: "222222"
|
||||
description: Somebody has started to work on this but abandoned work
|
||||
- name: "Priority/Critical"
|
||||
exclusive: true
|
||||
exclusive_order: 0
|
||||
color: b71c1c
|
||||
description: The priority is critical
|
||||
priority: critical
|
||||
- name: "Priority/High"
|
||||
exclusive: true
|
||||
exclusive_order: 1
|
||||
color: d32f2f
|
||||
description: The priority is high
|
||||
priority: high
|
||||
- name: "Priority/Medium"
|
||||
exclusive: true
|
||||
exclusive_order: 2
|
||||
color: e64a19
|
||||
description: The priority is medium
|
||||
priority: medium
|
||||
- name: "Priority/Low"
|
||||
exclusive: true
|
||||
exclusive_order: 3
|
||||
color: 4caf50
|
||||
description: The priority is low
|
||||
priority: low
|
||||
|
@ -1644,6 +1644,8 @@ issues.label_archived_filter = Show archived labels
|
||||
issues.label_archive_tooltip = Archived labels are excluded by default from the suggestions when searching by label.
|
||||
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_exclusive_order = Sort Order
|
||||
issues.label_exclusive_order_tooltip = Exclusive labels in the same scope will be sorted according to this numeric order.
|
||||
issues.label_count = %d labels
|
||||
issues.label_open_issues = %d open issues/pull requests
|
||||
issues.label_edit = Edit
|
||||
|
@ -139,6 +139,7 @@ func UpdateLabel(ctx *context.Context) {
|
||||
}
|
||||
l.Name = form.Title
|
||||
l.Exclusive = form.Exclusive
|
||||
l.ExclusiveOrder = form.ExclusiveOrder
|
||||
l.Description = form.Description
|
||||
l.Color = form.Color
|
||||
|
||||
|
@ -6,7 +6,10 @@ package repo
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/http"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@ -459,6 +462,25 @@ func UpdateIssueStatus(ctx *context.Context) {
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
func renderExclusiveLabelScopes(ctx *context.Context) {
|
||||
labels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, "", db.ListOptions{})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetAllRepoLabels", err)
|
||||
return
|
||||
}
|
||||
scopeSet := make(map[string]bool, 0)
|
||||
for _, label := range labels {
|
||||
scope := label.ExclusiveScope()
|
||||
if len(scope) > 0 {
|
||||
scopeSet[scope] = true
|
||||
}
|
||||
}
|
||||
|
||||
scopes := slices.Collect(maps.Keys(scopeSet))
|
||||
sort.Strings(scopes)
|
||||
ctx.Data["ExclusiveLabelScopes"] = scopes
|
||||
}
|
||||
|
||||
func renderMilestones(ctx *context.Context) {
|
||||
// Get milestones
|
||||
milestones, err := db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
|
||||
@ -778,6 +800,11 @@ func Issues(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
renderExclusiveLabelScopes(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["CanWriteIssuesOrPulls"] = ctx.Repo.CanWriteIssuesOrPulls(isPullList)
|
||||
|
||||
ctx.HTML(http.StatusOK, tplIssues)
|
||||
|
@ -528,12 +528,13 @@ func (f *CreateMilestoneForm) Validate(req *http.Request, errs binding.Errors) b
|
||||
|
||||
// CreateLabelForm form for creating label
|
||||
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"`
|
||||
ID int64
|
||||
Title string `binding:"Required;MaxSize(50)" locale:"repo.issues.label_title"`
|
||||
Exclusive bool `form:"exclusive"`
|
||||
ExclusiveOrder int `form:"exclusive_order"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
|
@ -133,5 +133,12 @@
|
||||
<a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="{{QueryBuild $queryLink "sort" "leastcomment"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a>
|
||||
<a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="{{QueryBuild $queryLink "sort" "nearduedate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a>
|
||||
<a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="{{QueryBuild $queryLink "sort" "farduedate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a>
|
||||
<div class="divider"></div>
|
||||
<div class="header">{{ctx.Locale.Tr "repo.issues.filter_label"}}</div>
|
||||
{{range .ExclusiveLabelScopes}}
|
||||
{{$scope := .}}
|
||||
{{$sortType := (printf "scope-%s" $scope)}}
|
||||
<a class="{{if eq $.SortType $sortType}}active {{end}}item" href="{{QueryBuild $queryLink "sort" $sortType}}">{{$scope}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -25,6 +25,14 @@
|
||||
{{svg "octicon-alert"}} {{ctx.Locale.Tr "repo.issues.label_exclusive_warning"}}
|
||||
</div>
|
||||
<br>
|
||||
<div class="field label-exclusive-order-input-field" style="margin-top: 0.5em">
|
||||
<label for="exclusive_order">{{ctx.Locale.Tr "repo.issues.label_exclusive_order"}}
|
||||
<i class="tw-ml-1" data-tooltip-content={{ctx.Locale.Tr "repo.issues.label_exclusive_order_tooltip"}}>
|
||||
{{svg "octicon-info"}}
|
||||
</i>
|
||||
</label>
|
||||
<input class="label-exclusive-order-input" name="exclusive_order" type="number" maxlength="4">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field label-is-archived-input-field">
|
||||
<div class="ui checkbox">
|
||||
|
@ -50,6 +50,7 @@
|
||||
data-label-id="{{.ID}}" data-label-name="{{.Name}}" data-label-color="{{.Color}}"
|
||||
data-label-exclusive="{{.Exclusive}}" data-label-is-archived="{{gt .ArchivedUnix 0}}"
|
||||
data-label-num-issues="{{.NumIssues}}" data-label-description="{{.Description}}"
|
||||
data-label-exclusive-order="{{.ExclusiveOrder}}"
|
||||
>{{svg "octicon-pencil"}} {{ctx.Locale.Tr "repo.issues.label_edit"}}</a>
|
||||
<a class="link-action" href="#" data-url="{{$.Link}}/delete?id={{.ID}}"
|
||||
data-modal-confirm-header="{{ctx.Locale.Tr "repo.issues.label_deletion"}}"
|
||||
|
@ -18,6 +18,8 @@ export function initCompLabelEdit(pageSelector: string) {
|
||||
const elExclusiveField = elModal.querySelector('.label-exclusive-input-field');
|
||||
const elExclusiveInput = elModal.querySelector<HTMLInputElement>('.label-exclusive-input');
|
||||
const elExclusiveWarning = elModal.querySelector('.label-exclusive-warning');
|
||||
const elExclusiveOrderField = elModal.querySelector<HTMLInputElement>('.label-exclusive-order-input-field');
|
||||
const elExclusiveOrderInput = elModal.querySelector<HTMLInputElement>('.label-exclusive-order-input');
|
||||
const elIsArchivedField = elModal.querySelector('.label-is-archived-input-field');
|
||||
const elIsArchivedInput = elModal.querySelector<HTMLInputElement>('.label-is-archived-input');
|
||||
const elDescInput = elModal.querySelector<HTMLInputElement>('.label-desc-input');
|
||||
@ -29,6 +31,7 @@ export function initCompLabelEdit(pageSelector: string) {
|
||||
const showExclusiveWarning = hasScope && elExclusiveInput.checked && elModal.hasAttribute('data-need-warn-exclusive');
|
||||
toggleElem(elExclusiveWarning, showExclusiveWarning);
|
||||
if (!hasScope) elExclusiveInput.checked = false;
|
||||
toggleElem(elExclusiveOrderField, elExclusiveInput.checked);
|
||||
};
|
||||
|
||||
const showLabelEditModal = (btn:HTMLElement) => {
|
||||
@ -36,6 +39,7 @@ export function initCompLabelEdit(pageSelector: string) {
|
||||
const form = elModal.querySelector<HTMLFormElement>('form');
|
||||
elLabelId.value = btn.getAttribute('data-label-id') || '';
|
||||
elNameInput.value = btn.getAttribute('data-label-name') || '';
|
||||
elExclusiveOrderInput.value = btn.getAttribute('data-label-exclusive-order') || '0';
|
||||
elIsArchivedInput.checked = btn.getAttribute('data-label-is-archived') === 'true';
|
||||
elExclusiveInput.checked = btn.getAttribute('data-label-exclusive') === 'true';
|
||||
elDescInput.value = btn.getAttribute('data-label-description') || '';
|
||||
|
Loading…
x
Reference in New Issue
Block a user