Batch updates for issues (#926)
This commit is contained in:
parent
021904e4e6
commit
09fe4a2ae9
11 changed files with 365 additions and 133 deletions
11
cmd/web.go
11
cmd/web.go
|
@ -466,17 +466,16 @@ func runWeb(ctx *cli.Context) error {
|
||||||
m.Combo("/new", repo.MustEnableIssues).Get(context.RepoRef(), repo.NewIssue).
|
m.Combo("/new", repo.MustEnableIssues).Get(context.RepoRef(), repo.NewIssue).
|
||||||
Post(bindIgnErr(auth.CreateIssueForm{}), repo.NewIssuePost)
|
Post(bindIgnErr(auth.CreateIssueForm{}), repo.NewIssuePost)
|
||||||
|
|
||||||
m.Group("/:index", func() {
|
|
||||||
m.Post("/label", repo.UpdateIssueLabel)
|
|
||||||
m.Post("/milestone", repo.UpdateIssueMilestone)
|
|
||||||
m.Post("/assignee", repo.UpdateIssueAssignee)
|
|
||||||
}, reqRepoWriter)
|
|
||||||
|
|
||||||
m.Group("/:index", func() {
|
m.Group("/:index", func() {
|
||||||
m.Post("/title", repo.UpdateIssueTitle)
|
m.Post("/title", repo.UpdateIssueTitle)
|
||||||
m.Post("/content", repo.UpdateIssueContent)
|
m.Post("/content", repo.UpdateIssueContent)
|
||||||
m.Combo("/comments").Post(bindIgnErr(auth.CreateCommentForm{}), repo.NewComment)
|
m.Combo("/comments").Post(bindIgnErr(auth.CreateCommentForm{}), repo.NewComment)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
m.Post("/labels", repo.UpdateIssueLabel, reqRepoWriter)
|
||||||
|
m.Post("/milestone", repo.UpdateIssueMilestone, reqRepoWriter)
|
||||||
|
m.Post("/assignee", repo.UpdateIssueAssignee, reqRepoWriter)
|
||||||
|
m.Post("/status", repo.UpdateIssueStatus, reqRepoWriter)
|
||||||
})
|
})
|
||||||
m.Group("/comments/:id", func() {
|
m.Group("/comments/:id", func() {
|
||||||
m.Post("", repo.UpdateCommentContent)
|
m.Post("", repo.UpdateCommentContent)
|
||||||
|
|
|
@ -1002,6 +1002,16 @@ func GetIssueByID(id int64) (*Issue, error) {
|
||||||
return getIssueByID(x, id)
|
return getIssueByID(x, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getIssuesByIDs(e Engine, issueIDs []int64) ([]*Issue, error) {
|
||||||
|
issues := make([]*Issue, 0, 10)
|
||||||
|
return issues, e.In("id", issueIDs).Find(&issues)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIssuesByIDs return issues with the given IDs.
|
||||||
|
func GetIssuesByIDs(issueIDs []int64) ([]*Issue, error) {
|
||||||
|
return getIssuesByIDs(x, issueIDs)
|
||||||
|
}
|
||||||
|
|
||||||
// IssuesOptions represents options of an issue.
|
// IssuesOptions represents options of an issue.
|
||||||
type IssuesOptions struct {
|
type IssuesOptions struct {
|
||||||
RepoID int64
|
RepoID int64
|
||||||
|
|
|
@ -42,3 +42,19 @@ func TestIssueAPIURL(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/issues/1", issue.APIURL())
|
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/issues/1", issue.APIURL())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetIssuesByIDs(t *testing.T) {
|
||||||
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
testSuccess := func(expectedIssueIDs []int64, nonExistentIssueIDs []int64) {
|
||||||
|
issues, err := GetIssuesByIDs(append(expectedIssueIDs, nonExistentIssueIDs...))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
actualIssueIDs := make([]int64, len(issues))
|
||||||
|
for i, issue := range issues {
|
||||||
|
actualIssueIDs[i] = issue.ID
|
||||||
|
}
|
||||||
|
assert.Equal(t, expectedIssueIDs, actualIssueIDs)
|
||||||
|
|
||||||
|
}
|
||||||
|
testSuccess([]int64{1, 2, 3}, []int64{})
|
||||||
|
testSuccess([]int64{1, 2, 3}, []int64{NonexistentID})
|
||||||
|
}
|
||||||
|
|
|
@ -583,6 +583,13 @@ issues.filter_sort.recentupdate = Recently updated
|
||||||
issues.filter_sort.leastupdate = Least recently updated
|
issues.filter_sort.leastupdate = Least recently updated
|
||||||
issues.filter_sort.mostcomment = Most commented
|
issues.filter_sort.mostcomment = Most commented
|
||||||
issues.filter_sort.leastcomment = Least commented
|
issues.filter_sort.leastcomment = Least commented
|
||||||
|
issues.action_open = Open
|
||||||
|
issues.action_close = Close
|
||||||
|
issues.action_label = Label
|
||||||
|
issues.action_milestone = Milestone
|
||||||
|
issues.action_milestone_no_select = No milestone
|
||||||
|
issues.action_assignee = Assignee
|
||||||
|
issues.action_assignee_no_select = No assignee
|
||||||
issues.opened_by = opened %[1]s by <a href="%[2]s">%[3]s</a>
|
issues.opened_by = opened %[1]s by <a href="%[2]s">%[3]s</a>
|
||||||
issues.opened_by_fake = opened %[1]s by %[2]s
|
issues.opened_by_fake = opened %[1]s by %[2]s
|
||||||
issues.previous = Previous
|
issues.previous = Previous
|
||||||
|
|
|
@ -2270,6 +2270,9 @@ footer .ui.language .menu {
|
||||||
#search-user-box .results .item img {
|
#search-user-box .results .item img {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
.issue-actions {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
.issue.list {
|
.issue.list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
|
|
|
@ -87,6 +87,20 @@ function initEditForm() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function updateIssuesMeta(url, action, issueIds, elementId, afterSuccess) {
|
||||||
|
$.ajax({
|
||||||
|
type: "POST",
|
||||||
|
url: url,
|
||||||
|
data: {
|
||||||
|
"_csrf": csrf,
|
||||||
|
"action": action,
|
||||||
|
"issue_ids": issueIds,
|
||||||
|
"id": elementId
|
||||||
|
},
|
||||||
|
success: afterSuccess
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function initCommentForm() {
|
function initCommentForm() {
|
||||||
if ($('.comment.form').length == 0) {
|
if ($('.comment.form').length == 0) {
|
||||||
return
|
return
|
||||||
|
@ -100,14 +114,6 @@ function initCommentForm() {
|
||||||
var $labelMenu = $('.select-label .menu');
|
var $labelMenu = $('.select-label .menu');
|
||||||
var hasLabelUpdateAction = $labelMenu.data('action') == 'update';
|
var hasLabelUpdateAction = $labelMenu.data('action') == 'update';
|
||||||
|
|
||||||
function updateIssueMeta(url, action, id) {
|
|
||||||
$.post(url, {
|
|
||||||
"_csrf": csrf,
|
|
||||||
"action": action,
|
|
||||||
"id": id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$('.select-label').dropdown('setting', 'onHide', function(){
|
$('.select-label').dropdown('setting', 'onHide', function(){
|
||||||
if (hasLabelUpdateAction) {
|
if (hasLabelUpdateAction) {
|
||||||
location.reload();
|
location.reload();
|
||||||
|
@ -119,13 +125,23 @@ function initCommentForm() {
|
||||||
$(this).removeClass('checked');
|
$(this).removeClass('checked');
|
||||||
$(this).find('.octicon').removeClass('octicon-check');
|
$(this).find('.octicon').removeClass('octicon-check');
|
||||||
if (hasLabelUpdateAction) {
|
if (hasLabelUpdateAction) {
|
||||||
updateIssueMeta($labelMenu.data('update-url'), "detach", $(this).data('id'));
|
updateIssuesMeta(
|
||||||
|
$labelMenu.data('update-url'),
|
||||||
|
"detach",
|
||||||
|
$labelMenu.data('issue-id'),
|
||||||
|
$(this).data('id')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$(this).addClass('checked');
|
$(this).addClass('checked');
|
||||||
$(this).find('.octicon').addClass('octicon-check');
|
$(this).find('.octicon').addClass('octicon-check');
|
||||||
if (hasLabelUpdateAction) {
|
if (hasLabelUpdateAction) {
|
||||||
updateIssueMeta($labelMenu.data('update-url'), "attach", $(this).data('id'));
|
updateIssuesMeta(
|
||||||
|
$labelMenu.data('update-url'),
|
||||||
|
"attach",
|
||||||
|
$labelMenu.data('issue-id'),
|
||||||
|
$(this).data('id')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,7 +164,12 @@ function initCommentForm() {
|
||||||
});
|
});
|
||||||
$labelMenu.find('.no-select.item').click(function () {
|
$labelMenu.find('.no-select.item').click(function () {
|
||||||
if (hasLabelUpdateAction) {
|
if (hasLabelUpdateAction) {
|
||||||
updateIssueMeta($labelMenu.data('update-url'), "clear", '');
|
updateIssuesMeta(
|
||||||
|
$labelMenu.data('update-url'),
|
||||||
|
"clear",
|
||||||
|
$labelMenu.data('issue-id'),
|
||||||
|
""
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$(this).parent().find('.item').each(function () {
|
$(this).parent().find('.item').each(function () {
|
||||||
|
@ -181,7 +202,12 @@ function initCommentForm() {
|
||||||
|
|
||||||
$(this).addClass('selected active');
|
$(this).addClass('selected active');
|
||||||
if (hasUpdateAction) {
|
if (hasUpdateAction) {
|
||||||
updateIssueMeta($menu.data('update-url'), '', $(this).data('id'));
|
updateIssuesMeta(
|
||||||
|
$menu.data('update-url'),
|
||||||
|
"",
|
||||||
|
$menu.data('issue-id'),
|
||||||
|
$(this).data('id')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
switch (input_id) {
|
switch (input_id) {
|
||||||
case '#milestone_id':
|
case '#milestone_id':
|
||||||
|
@ -202,7 +228,12 @@ function initCommentForm() {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hasUpdateAction) {
|
if (hasUpdateAction) {
|
||||||
updateIssueMeta($menu.data('update-url'), '', '');
|
updateIssuesMeta(
|
||||||
|
$menu.data('update-url'),
|
||||||
|
"",
|
||||||
|
$menu.data('issue-id'),
|
||||||
|
$(this).data('id')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$list.find('.selected').html('');
|
$list.find('.selected').html('');
|
||||||
|
@ -1431,6 +1462,29 @@ $(document).ready(function () {
|
||||||
});
|
});
|
||||||
$('.markdown').autolink();
|
$('.markdown').autolink();
|
||||||
|
|
||||||
|
$('.issue-checkbox').click(function() {
|
||||||
|
var numChecked = $('.issue-checkbox').children('input:checked').length;
|
||||||
|
if (numChecked > 0) {
|
||||||
|
$('.issue-filters').hide();
|
||||||
|
$('.issue-actions').show();
|
||||||
|
} else {
|
||||||
|
$('.issue-filters').show();
|
||||||
|
$('.issue-actions').hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.issue-action').click(function () {
|
||||||
|
var action = this.dataset.action
|
||||||
|
var elementId = this.dataset.elementId
|
||||||
|
var issueIDs = $('.issue-checkbox').children('input:checked').map(function() {
|
||||||
|
return this.dataset.issueId;
|
||||||
|
}).get().join();
|
||||||
|
var url = this.dataset.url
|
||||||
|
updateIssuesMeta(url, action, issueIDs, elementId, function() {
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
buttonsClickOnEnter();
|
buttonsClickOnEnter();
|
||||||
searchUsers();
|
searchUsers();
|
||||||
searchRepositories();
|
searchRepositories();
|
||||||
|
|
|
@ -1261,6 +1261,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.issue-actions {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.issue.list {
|
.issue.list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -644,6 +645,28 @@ func getActionIssue(ctx *context.Context) *models.Issue {
|
||||||
return issue
|
return issue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getActionIssues(ctx *context.Context) []*models.Issue {
|
||||||
|
commaSeparatedIssueIDs := ctx.Query("issue_ids")
|
||||||
|
if len(commaSeparatedIssueIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
issueIDs := make([]int64, 0, 10)
|
||||||
|
for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") {
|
||||||
|
issueID, err := strconv.ParseInt(stringIssueID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Handle(500, "ParseInt", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
issueIDs = append(issueIDs, issueID)
|
||||||
|
}
|
||||||
|
issues, err := models.GetIssuesByIDs(issueIDs)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Handle(500, "GetIssuesByIDs", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return issues
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateIssueTitle change issue's title
|
// UpdateIssueTitle change issue's title
|
||||||
func UpdateIssueTitle(ctx *context.Context) {
|
func UpdateIssueTitle(ctx *context.Context) {
|
||||||
issue := getActionIssue(ctx)
|
issue := getActionIssue(ctx)
|
||||||
|
@ -697,26 +720,23 @@ func UpdateIssueContent(ctx *context.Context) {
|
||||||
|
|
||||||
// UpdateIssueMilestone change issue's milestone
|
// UpdateIssueMilestone change issue's milestone
|
||||||
func UpdateIssueMilestone(ctx *context.Context) {
|
func UpdateIssueMilestone(ctx *context.Context) {
|
||||||
issue := getActionIssue(ctx)
|
issues := getActionIssues(ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
oldMilestoneID := issue.MilestoneID
|
|
||||||
milestoneID := ctx.QueryInt64("id")
|
milestoneID := ctx.QueryInt64("id")
|
||||||
|
for _, issue := range issues {
|
||||||
|
oldMilestoneID := issue.MilestoneID
|
||||||
if oldMilestoneID == milestoneID {
|
if oldMilestoneID == milestoneID {
|
||||||
ctx.JSON(200, map[string]interface{}{
|
continue
|
||||||
"ok": true,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not check for invalid milestone id and give responsibility to owners.
|
|
||||||
issue.MilestoneID = milestoneID
|
issue.MilestoneID = milestoneID
|
||||||
if err := models.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil {
|
if err := models.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil {
|
||||||
ctx.Handle(500, "ChangeMilestoneAssign", err)
|
ctx.Handle(500, "ChangeMilestoneAssign", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx.JSON(200, map[string]interface{}{
|
ctx.JSON(200, map[string]interface{}{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
|
@ -725,24 +745,53 @@ func UpdateIssueMilestone(ctx *context.Context) {
|
||||||
|
|
||||||
// UpdateIssueAssignee change issue's assignee
|
// UpdateIssueAssignee change issue's assignee
|
||||||
func UpdateIssueAssignee(ctx *context.Context) {
|
func UpdateIssueAssignee(ctx *context.Context) {
|
||||||
issue := getActionIssue(ctx)
|
issues := getActionIssues(ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
assigneeID := ctx.QueryInt64("id")
|
assigneeID := ctx.QueryInt64("id")
|
||||||
|
for _, issue := range issues {
|
||||||
if issue.AssigneeID == assigneeID {
|
if issue.AssigneeID == assigneeID {
|
||||||
ctx.JSON(200, map[string]interface{}{
|
continue
|
||||||
"ok": true,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := issue.ChangeAssignee(ctx.User, assigneeID); err != nil {
|
if err := issue.ChangeAssignee(ctx.User, assigneeID); err != nil {
|
||||||
ctx.Handle(500, "ChangeAssignee", err)
|
ctx.Handle(500, "ChangeAssignee", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
ctx.JSON(200, map[string]interface{}{
|
||||||
|
"ok": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateIssueStatus change issue's status
|
||||||
|
func UpdateIssueStatus(ctx *context.Context) {
|
||||||
|
issues := getActionIssues(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var isClosed bool
|
||||||
|
switch action := ctx.Query("action"); action {
|
||||||
|
case "open":
|
||||||
|
isClosed = false
|
||||||
|
case "close":
|
||||||
|
isClosed = true
|
||||||
|
default:
|
||||||
|
log.Warn("Unrecognized action: %s", action)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := models.IssueList(issues).LoadRepositories(); err != nil {
|
||||||
|
ctx.Handle(500, "LoadRepositories", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, issue := range issues {
|
||||||
|
if err := issue.ChangeStatus(ctx.User, issue.Repo, isClosed); err != nil {
|
||||||
|
ctx.Handle(500, "ChangeStatus", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
ctx.JSON(200, map[string]interface{}{
|
ctx.JSON(200, map[string]interface{}{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/auth"
|
"code.gitea.io/gitea/modules/auth"
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -129,18 +130,20 @@ func DeleteLabel(ctx *context.Context) {
|
||||||
|
|
||||||
// UpdateIssueLabel change issue's labels
|
// UpdateIssueLabel change issue's labels
|
||||||
func UpdateIssueLabel(ctx *context.Context) {
|
func UpdateIssueLabel(ctx *context.Context) {
|
||||||
issue := getActionIssue(ctx)
|
issues := getActionIssues(ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.Query("action") == "clear" {
|
switch action := ctx.Query("action"); action {
|
||||||
|
case "clear":
|
||||||
|
for _, issue := range issues {
|
||||||
if err := issue.ClearLabels(ctx.User); err != nil {
|
if err := issue.ClearLabels(ctx.User); err != nil {
|
||||||
ctx.Handle(500, "ClearLabels", err)
|
ctx.Handle(500, "ClearLabels", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
isAttach := ctx.Query("action") == "attach"
|
case "attach", "detach", "toggle":
|
||||||
label, err := models.GetLabelByID(ctx.QueryInt64("id"))
|
label, err := models.GetLabelByID(ctx.QueryInt64("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if models.IsErrLabelNotExist(err) {
|
if models.IsErrLabelNotExist(err) {
|
||||||
|
@ -151,18 +154,41 @@ func UpdateIssueLabel(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if isAttach && !issue.HasLabel(label.ID) {
|
if action == "toggle" {
|
||||||
|
anyHaveLabel := false
|
||||||
|
for _, issue := range issues {
|
||||||
|
if issue.HasLabel(label.ID) {
|
||||||
|
anyHaveLabel = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if anyHaveLabel {
|
||||||
|
action = "detach"
|
||||||
|
} else {
|
||||||
|
action = "attach"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if action == "attach" {
|
||||||
|
for _, issue := range issues {
|
||||||
if err = issue.AddLabel(ctx.User, label); err != nil {
|
if err = issue.AddLabel(ctx.User, label); err != nil {
|
||||||
ctx.Handle(500, "AddLabel", err)
|
ctx.Handle(500, "AddLabel", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if !isAttach && issue.HasLabel(label.ID) {
|
}
|
||||||
|
} else {
|
||||||
|
for _, issue := range issues {
|
||||||
if err = issue.RemoveLabel(ctx.User, label); err != nil {
|
if err = issue.RemoveLabel(ctx.User, label); err != nil {
|
||||||
ctx.Handle(500, "RemoveLabel", err)
|
ctx.Handle(500, "RemoveLabel", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
log.Warn("Unrecognized action: %s", action)
|
||||||
|
ctx.Error(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ctx.JSON(200, map[string]interface{}{
|
ctx.JSON(200, map[string]interface{}{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
|
<div class="issue-filters">
|
||||||
<div class="ui tiny basic status buttons">
|
<div class="ui tiny basic status buttons">
|
||||||
<a class="ui {{if not .IsShowClosed}}green active{{end}} basic button" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state=open&labels={{.SelectLabels}}&milestone={{.MilestoneID}}&assignee={{.AssigneeID}}">
|
<a class="ui {{if not .IsShowClosed}}green active{{end}} basic button" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&sort={{$.SortType}}&state=open&labels={{.SelectLabels}}&milestone={{.MilestoneID}}&assignee={{.AssigneeID}}">
|
||||||
<i class="octicon octicon-issue-opened"></i>
|
<i class="octicon octicon-issue-opened"></i>
|
||||||
|
@ -97,11 +98,74 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="issue-actions">
|
||||||
|
<div class="ui basic status buttons">
|
||||||
|
<div class="ui green active basic button issue-action" data-action="open" data-url="{{$.Link}}/status">{{.i18n.Tr "repo.issues.action_open"}}</div>
|
||||||
|
<div class="ui red active basic button issue-action" data-action="close" data-url="{{$.Link}}/status">{{.i18n.Tr "repo.issues.action_close"}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ui secondary filter menu floated right">
|
||||||
|
<!-- Labels -->
|
||||||
|
<div class="ui {{if not .Labels}}disabled{{end}} dropdown jump item">
|
||||||
|
<span class="text">
|
||||||
|
{{.i18n.Tr "repo.issues.action_label"}}
|
||||||
|
<i class="dropdown icon"></i>
|
||||||
|
</span>
|
||||||
|
<div class="menu">
|
||||||
|
{{range .Labels}}
|
||||||
|
<div class="item issue-action" data-action="toggle" data-element-id="{{.ID}}" data-url="{{$.Link}}/labels">
|
||||||
|
<span class="octicon {{if eq $.SelectLabels .ID}}octicon-check{{end}}"></span><span class="label color" style="background-color: {{.Color}}"></span> {{.Name | Sanitize}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Milestone -->
|
||||||
|
<div class="ui {{if not .Milestones}}disabled{{end}} dropdown jump item">
|
||||||
|
<span class="text">
|
||||||
|
{{.i18n.Tr "repo.issues.action_milestone"}}
|
||||||
|
<i class="dropdown icon"></i>
|
||||||
|
</span>
|
||||||
|
<div class="menu">
|
||||||
|
<div class="item issue-action" data-element-id="0" data-url="{{$.Link}}/milestone">
|
||||||
|
{{.i18n.Tr "repo.issues.action_milestone_no_select"}}
|
||||||
|
</div>
|
||||||
|
{{range .Milestones}}
|
||||||
|
<div class="item issue-action" data-element-id="{{.ID}}" data-url="{{$.Link}}/milestone">
|
||||||
|
{{.Name | Sanitize}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Assignee -->
|
||||||
|
<div class="ui {{if not .Assignees}}disabled{{end}} dropdown jump item">
|
||||||
|
<span class="text">
|
||||||
|
{{.i18n.Tr "repo.issues.action_assignee"}}
|
||||||
|
<i class="dropdown icon"></i>
|
||||||
|
</span>
|
||||||
|
<div class="menu">
|
||||||
|
<div class="item issue-action" data-element-id="0" data-url="{{$.Link}}/assignee">
|
||||||
|
{{.i18n.Tr "repo.issues.action_assignee_no_select"}}
|
||||||
|
</div>
|
||||||
|
{{range .Assignees}}
|
||||||
|
<div class="item issue-action" data-element-id="{{.ID}}" data-url="{{$.Link}}/assignee">
|
||||||
|
<img src="{{.RelAvatarLink}}"> {{.Name}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="issue list">
|
<div class="issue list">
|
||||||
{{range .Issues}}
|
{{range .Issues}}
|
||||||
{{ $timeStr:= TimeSince .Created $.Lang }}
|
{{ $timeStr:= TimeSince .Created $.Lang }}
|
||||||
<li class="item">
|
<li class="item">
|
||||||
|
<div class="ui checkbox issue-checkbox">
|
||||||
|
<input type="checkbox" data-issue-id={{.ID}}></input>
|
||||||
|
</div>
|
||||||
<div class="ui {{if .IsRead}}black{{else}}green{{end}} label">#{{.Index}}</div>
|
<div class="ui {{if .IsRead}}black{{else}}green{{end}} label">#{{.Index}}</div>
|
||||||
<a class="title has-emoji" href="{{$.Link}}/{{.Index}}">{{.Title}}</a>
|
<a class="title has-emoji" href="{{$.Link}}/{{.Index}}">{{.Title}}</a>
|
||||||
|
|
||||||
|
|
|
@ -311,7 +311,7 @@
|
||||||
<strong>{{.i18n.Tr "repo.issues.new.labels"}}</strong>
|
<strong>{{.i18n.Tr "repo.issues.new.labels"}}</strong>
|
||||||
<span class="octicon octicon-gear"></span>
|
<span class="octicon octicon-gear"></span>
|
||||||
</span>
|
</span>
|
||||||
<div class="filter menu" data-action="update" data-update-url="{{$.RepoLink}}/issues/{{$.Issue.Index}}/label">
|
<div class="filter menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/labels">
|
||||||
<div class="no-select item">{{.i18n.Tr "repo.issues.new.clear_labels"}}</div>
|
<div class="no-select item">{{.i18n.Tr "repo.issues.new.clear_labels"}}</div>
|
||||||
{{range .Labels}}
|
{{range .Labels}}
|
||||||
<a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" data-id-selector="#label_{{.ID}}"><span class="octicon {{if .IsChecked}}octicon-check{{end}}"></span><span class="label color" style="background-color: {{.Color}}"></span> {{.Name}}</a>
|
<a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" data-id-selector="#label_{{.ID}}"><span class="octicon {{if .IsChecked}}octicon-check{{end}}"></span><span class="label color" style="background-color: {{.Color}}"></span> {{.Name}}</a>
|
||||||
|
@ -335,7 +335,7 @@
|
||||||
<strong>{{.i18n.Tr "repo.issues.new.milestone"}}</strong>
|
<strong>{{.i18n.Tr "repo.issues.new.milestone"}}</strong>
|
||||||
<span class="octicon octicon-gear"></span>
|
<span class="octicon octicon-gear"></span>
|
||||||
</span>
|
</span>
|
||||||
<div class="menu" data-action="update" data-update-url="{{$.RepoLink}}/issues/{{$.Issue.Index}}/milestone">
|
<div class="menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/milestone">
|
||||||
<div class="no-select item">{{.i18n.Tr "repo.issues.new.clear_milestone"}}</div>
|
<div class="no-select item">{{.i18n.Tr "repo.issues.new.clear_milestone"}}</div>
|
||||||
{{if .OpenMilestones}}
|
{{if .OpenMilestones}}
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
@ -376,7 +376,7 @@
|
||||||
<strong>{{.i18n.Tr "repo.issues.new.assignee"}}</strong>
|
<strong>{{.i18n.Tr "repo.issues.new.assignee"}}</strong>
|
||||||
<span class="octicon octicon-gear"></span>
|
<span class="octicon octicon-gear"></span>
|
||||||
</span>
|
</span>
|
||||||
<div class="menu" data-action="update" data-update-url="{{$.RepoLink}}/issues/{{$.Issue.Index}}/assignee">
|
<div class="menu" data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/assignee">
|
||||||
<div class="no-select item">{{.i18n.Tr "repo.issues.new.clear_assignee"}}</div>
|
<div class="no-select item">{{.i18n.Tr "repo.issues.new.clear_assignee"}}</div>
|
||||||
{{range .Assignees}}
|
{{range .Assignees}}
|
||||||
<div class="item" data-id="{{.ID}}" data-href="{{$.RepoLink}}/issues?assignee={{.ID}}" data-avatar="{{.RelAvatarLink}}"><img src="{{.RelAvatarLink}}"> {{.Name}}</div>
|
<div class="item" data-id="{{.ID}}" data-href="{{$.RepoLink}}/issues?assignee={{.ID}}" data-avatar="{{.RelAvatarLink}}"><img src="{{.RelAvatarLink}}"> {{.Name}}</div>
|
||||||
|
|
Loading…
Reference in a new issue