mirror of
https://github.com/go-gitea/gitea
synced 2024-12-22 09:37:53 +01:00
Merge branch 'main' into feat-32257-add-comments-unchanged-lines-and-show
This commit is contained in:
commit
51a70e3104
@ -26,8 +26,10 @@ const (
|
||||
SearchOrderByForksReverse SearchOrderBy = "num_forks DESC"
|
||||
)
|
||||
|
||||
const (
|
||||
// Which means a condition to filter the records which don't match any id.
|
||||
// It's different from zero which means the condition could be ignored.
|
||||
NoConditionID = -1
|
||||
)
|
||||
// NoConditionID means a condition to filter the records which don't match any id.
|
||||
// eg: "milestone_id=-1" means "find the items without any milestone.
|
||||
const NoConditionID int64 = -1
|
||||
|
||||
// NonExistingID means a condition to match no result (eg: a non-existing user)
|
||||
// It doesn't use -1 or -2 because they are used as builtin users.
|
||||
const NonExistingID int64 = -1000000
|
||||
|
@ -4,6 +4,7 @@
|
||||
reviewer_id: 1
|
||||
issue_id: 2
|
||||
content: "Demo Review"
|
||||
original_author_id: 0
|
||||
updated_unix: 946684810
|
||||
created_unix: 946684810
|
||||
-
|
||||
@ -12,6 +13,7 @@
|
||||
reviewer_id: 534543
|
||||
issue_id: 534543
|
||||
content: "Invalid Review #1"
|
||||
original_author_id: 0
|
||||
updated_unix: 946684810
|
||||
created_unix: 946684810
|
||||
-
|
||||
@ -20,6 +22,7 @@
|
||||
reviewer_id: 1
|
||||
issue_id: 343545
|
||||
content: "Invalid Review #2"
|
||||
original_author_id: 0
|
||||
updated_unix: 946684810
|
||||
created_unix: 946684810
|
||||
-
|
||||
@ -28,6 +31,7 @@
|
||||
reviewer_id: 1
|
||||
issue_id: 2
|
||||
content: "Pending Review"
|
||||
original_author_id: 0
|
||||
updated_unix: 946684810
|
||||
created_unix: 946684810
|
||||
-
|
||||
@ -36,6 +40,7 @@
|
||||
reviewer_id: 1
|
||||
issue_id: 3
|
||||
content: "New review 1"
|
||||
original_author_id: 0
|
||||
updated_unix: 946684810
|
||||
created_unix: 946684810
|
||||
-
|
||||
@ -61,8 +66,8 @@
|
||||
type: 1
|
||||
reviewer_id: 4
|
||||
issue_id: 3
|
||||
original_author_id: 0
|
||||
content: "New review 5"
|
||||
original_author_id: 0
|
||||
commit_id: 8091a55037cd59e47293aca02981b5a67076b364
|
||||
stale: true
|
||||
updated_unix: 946684813
|
||||
@ -73,9 +78,9 @@
|
||||
reviewer_id: 2
|
||||
issue_id: 3
|
||||
content: "New review 3 rejected"
|
||||
original_author_id: 0
|
||||
updated_unix: 946684814
|
||||
created_unix: 946684814
|
||||
original_author_id: 0
|
||||
|
||||
-
|
||||
id: 10
|
||||
@ -83,6 +88,7 @@
|
||||
reviewer_id: 100
|
||||
issue_id: 3
|
||||
content: "a deleted user's review"
|
||||
original_author_id: 0
|
||||
official: true
|
||||
updated_unix: 946684815
|
||||
created_unix: 946684815
|
||||
@ -112,6 +118,7 @@
|
||||
reviewer_id: 5
|
||||
issue_id: 11
|
||||
content: "old review from user5"
|
||||
original_author_id: 0
|
||||
updated_unix: 946684820
|
||||
created_unix: 946684820
|
||||
|
||||
@ -121,6 +128,7 @@
|
||||
reviewer_id: 5
|
||||
issue_id: 11
|
||||
content: "duplicate review from user5 (latest)"
|
||||
original_author_id: 0
|
||||
updated_unix: 946684830
|
||||
created_unix: 946684830
|
||||
|
||||
@ -130,6 +138,7 @@
|
||||
reviewer_id: 6
|
||||
issue_id: 11
|
||||
content: "singular review from org6 and final review for this pr"
|
||||
original_author_id: 0
|
||||
updated_unix: 946684831
|
||||
created_unix: 946684831
|
||||
|
||||
@ -139,6 +148,7 @@
|
||||
reviewer_id: 20
|
||||
issue_id: 20
|
||||
content: "review request for user20"
|
||||
original_author_id: 0
|
||||
updated_unix: 946684832
|
||||
created_unix: 946684832
|
||||
|
||||
@ -148,6 +158,7 @@
|
||||
reviewer_id: 20
|
||||
issue_id: 20
|
||||
content: "review approved by user20"
|
||||
original_author_id: 0
|
||||
updated_unix: 946684833
|
||||
created_unix: 946684833
|
||||
|
||||
@ -158,6 +169,7 @@
|
||||
reviewer_team_id: 5
|
||||
issue_id: 20
|
||||
content: "review request for team5"
|
||||
original_author_id: 0
|
||||
updated_unix: 946684834
|
||||
created_unix: 946684834
|
||||
|
||||
@ -168,6 +180,7 @@
|
||||
reviewer_team_id: 0
|
||||
issue_id: 20
|
||||
content: "review request for user15"
|
||||
original_author_id: 0
|
||||
updated_unix: 946684835
|
||||
created_unix: 946684835
|
||||
|
||||
@ -177,6 +190,7 @@
|
||||
reviewer_id: 1
|
||||
issue_id: 2
|
||||
content: "Review Comment"
|
||||
original_author_id: 0
|
||||
updated_unix: 946684810
|
||||
created_unix: 946684810
|
||||
|
||||
@ -186,6 +200,7 @@
|
||||
reviewer_id: 5
|
||||
issue_id: 3
|
||||
content: "reviewed by user5"
|
||||
original_author_id: 0
|
||||
commit_id: 4a357436d925b5c974181ff12a994538ddc5a269
|
||||
updated_unix: 946684816
|
||||
created_unix: 946684816
|
||||
@ -196,5 +211,6 @@
|
||||
reviewer_id: 5
|
||||
issue_id: 3
|
||||
content: "review request for user5"
|
||||
original_author_id: 0
|
||||
updated_unix: 946684817
|
||||
created_unix: 946684817
|
||||
|
@ -27,8 +27,8 @@ type IssuesOptions struct { //nolint
|
||||
RepoIDs []int64 // overwrites RepoCond if the length is not 0
|
||||
AllPublic bool // include also all public repositories
|
||||
RepoCond builder.Cond
|
||||
AssigneeID int64
|
||||
PosterID int64
|
||||
AssigneeID optional.Option[int64]
|
||||
PosterID optional.Option[int64]
|
||||
MentionedID int64
|
||||
ReviewRequestedID int64
|
||||
ReviewedID int64
|
||||
@ -231,15 +231,8 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) {
|
||||
sess.And("issue.is_closed=?", opts.IsClosed.Value())
|
||||
}
|
||||
|
||||
if opts.AssigneeID > 0 {
|
||||
applyAssigneeCondition(sess, opts.AssigneeID)
|
||||
} else if opts.AssigneeID == db.NoConditionID {
|
||||
sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)")
|
||||
}
|
||||
|
||||
if opts.PosterID > 0 {
|
||||
applyPosterCondition(sess, opts.PosterID)
|
||||
}
|
||||
applyAssigneeCondition(sess, opts.AssigneeID)
|
||||
applyPosterCondition(sess, opts.PosterID)
|
||||
|
||||
if opts.MentionedID > 0 {
|
||||
applyMentionedCondition(sess, opts.MentionedID)
|
||||
@ -359,13 +352,27 @@ func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *organizati
|
||||
return cond
|
||||
}
|
||||
|
||||
func applyAssigneeCondition(sess *xorm.Session, assigneeID int64) {
|
||||
sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
|
||||
And("issue_assignees.assignee_id = ?", assigneeID)
|
||||
func applyAssigneeCondition(sess *xorm.Session, assigneeID optional.Option[int64]) {
|
||||
// old logic: 0 is also treated as "not filtering assignee", because the "assignee" was read as FormInt64
|
||||
if !assigneeID.Has() || assigneeID.Value() == 0 {
|
||||
return
|
||||
}
|
||||
if assigneeID.Value() == db.NoConditionID {
|
||||
sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)")
|
||||
} else {
|
||||
sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
|
||||
And("issue_assignees.assignee_id = ?", assigneeID.Value())
|
||||
}
|
||||
}
|
||||
|
||||
func applyPosterCondition(sess *xorm.Session, posterID int64) {
|
||||
sess.And("issue.poster_id=?", posterID)
|
||||
func applyPosterCondition(sess *xorm.Session, posterID optional.Option[int64]) {
|
||||
if !posterID.Has() {
|
||||
return
|
||||
}
|
||||
// poster doesn't need to support db.NoConditionID(-1), so just use the value as-is
|
||||
if posterID.Has() {
|
||||
sess.And("issue.poster_id=?", posterID.Value())
|
||||
}
|
||||
}
|
||||
|
||||
func applyMentionedCondition(sess *xorm.Session, mentionedID int64) {
|
||||
|
@ -151,15 +151,9 @@ func applyIssuesOptions(sess *xorm.Session, opts *IssuesOptions, issueIDs []int6
|
||||
|
||||
applyProjectCondition(sess, opts)
|
||||
|
||||
if opts.AssigneeID > 0 {
|
||||
applyAssigneeCondition(sess, opts.AssigneeID)
|
||||
} else if opts.AssigneeID == db.NoConditionID {
|
||||
sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)")
|
||||
}
|
||||
applyAssigneeCondition(sess, opts.AssigneeID)
|
||||
|
||||
if opts.PosterID > 0 {
|
||||
applyPosterCondition(sess, opts.PosterID)
|
||||
}
|
||||
applyPosterCondition(sess, opts.PosterID)
|
||||
|
||||
if opts.MentionedID > 0 {
|
||||
applyMentionedCondition(sess, opts.MentionedID)
|
||||
|
@ -16,6 +16,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/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -155,7 +156,7 @@ func TestIssues(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
issues_model.IssuesOptions{
|
||||
AssigneeID: 1,
|
||||
AssigneeID: optional.Some(int64(1)),
|
||||
SortType: "oldest",
|
||||
},
|
||||
[]int64{1, 6},
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
@ -240,6 +241,64 @@ func (prs PullRequestList) GetIssueIDs() []int64 {
|
||||
})
|
||||
}
|
||||
|
||||
func (prs PullRequestList) LoadReviewCommentsCounts(ctx context.Context) (map[int64]int, error) {
|
||||
issueIDs := prs.GetIssueIDs()
|
||||
countsMap := make(map[int64]int, len(issueIDs))
|
||||
counts := make([]struct {
|
||||
IssueID int64
|
||||
Count int
|
||||
}, 0, len(issueIDs))
|
||||
if err := db.GetEngine(ctx).Select("issue_id, count(*) as count").
|
||||
Table("comment").In("issue_id", issueIDs).And("type = ?", CommentTypeReview).
|
||||
GroupBy("issue_id").Find(&counts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, c := range counts {
|
||||
countsMap[c.IssueID] = c.Count
|
||||
}
|
||||
return countsMap, nil
|
||||
}
|
||||
|
||||
func (prs PullRequestList) LoadReviews(ctx context.Context) (ReviewList, error) {
|
||||
issueIDs := prs.GetIssueIDs()
|
||||
reviews := make([]*Review, 0, len(issueIDs))
|
||||
|
||||
subQuery := builder.Select("max(id) as id").
|
||||
From("review").
|
||||
Where(builder.In("issue_id", issueIDs)).
|
||||
And(builder.In("`type`", ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest)).
|
||||
And(builder.Eq{
|
||||
"dismissed": false,
|
||||
"original_author_id": 0,
|
||||
"reviewer_team_id": 0,
|
||||
}).
|
||||
GroupBy("issue_id, reviewer_id")
|
||||
// Get latest review of each reviewer, sorted in order they were made
|
||||
if err := db.GetEngine(ctx).In("id", subQuery).OrderBy("review.updated_unix ASC").Find(&reviews); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
teamReviewRequests := make([]*Review, 0, 5)
|
||||
subQueryTeam := builder.Select("max(id) as id").
|
||||
From("review").
|
||||
Where(builder.In("issue_id", issueIDs)).
|
||||
And(builder.Eq{
|
||||
"original_author_id": 0,
|
||||
}).And(builder.Neq{
|
||||
"reviewer_team_id": 0,
|
||||
}).
|
||||
GroupBy("issue_id, reviewer_team_id")
|
||||
if err := db.GetEngine(ctx).In("id", subQueryTeam).OrderBy("review.updated_unix ASC").Find(&teamReviewRequests); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(teamReviewRequests) > 0 {
|
||||
reviews = append(reviews, teamReviewRequests...)
|
||||
}
|
||||
|
||||
return reviews, nil
|
||||
}
|
||||
|
||||
// HasMergedPullRequestInRepo returns whether the user(poster) has merged pull-request in the repo
|
||||
func HasMergedPullRequestInRepo(ctx context.Context, repoID, posterID int64) (bool, error) {
|
||||
return db.GetEngine(ctx).
|
||||
|
64
models/issues/pull_list_test.go
Normal file
64
models/issues/pull_list_test.go
Normal file
@ -0,0 +1,64 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package issues_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPullRequestList_LoadAttributes(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
prs := []*issues_model.PullRequest{
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}),
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}),
|
||||
}
|
||||
assert.NoError(t, issues_model.PullRequestList(prs).LoadAttributes(db.DefaultContext))
|
||||
for _, pr := range prs {
|
||||
assert.NotNil(t, pr.Issue)
|
||||
assert.Equal(t, pr.IssueID, pr.Issue.ID)
|
||||
}
|
||||
|
||||
assert.NoError(t, issues_model.PullRequestList([]*issues_model.PullRequest{}).LoadAttributes(db.DefaultContext))
|
||||
}
|
||||
|
||||
func TestPullRequestList_LoadReviewCommentsCounts(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
prs := []*issues_model.PullRequest{
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}),
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}),
|
||||
}
|
||||
reviewComments, err := issues_model.PullRequestList(prs).LoadReviewCommentsCounts(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviewComments, 2)
|
||||
for _, pr := range prs {
|
||||
assert.EqualValues(t, reviewComments[pr.IssueID], 1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullRequestList_LoadReviews(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
prs := []*issues_model.PullRequest{
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}),
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}),
|
||||
}
|
||||
reviewList, err := issues_model.PullRequestList(prs).LoadReviews(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
// 1, 7, 8, 9, 10, 22
|
||||
assert.Len(t, reviewList, 6)
|
||||
assert.EqualValues(t, 1, reviewList[0].ID)
|
||||
assert.EqualValues(t, 7, reviewList[1].ID)
|
||||
assert.EqualValues(t, 8, reviewList[2].ID)
|
||||
assert.EqualValues(t, 9, reviewList[3].ID)
|
||||
assert.EqualValues(t, 10, reviewList[4].ID)
|
||||
assert.EqualValues(t, 22, reviewList[5].ID)
|
||||
}
|
@ -79,7 +79,7 @@ func TestPullRequestsNewest(t *testing.T) {
|
||||
func TestLoadRequestedReviewers(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
|
||||
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||
assert.NoError(t, pull.LoadIssue(db.DefaultContext))
|
||||
issue := pull.Issue
|
||||
assert.NoError(t, issue.LoadRepo(db.DefaultContext))
|
||||
@ -93,7 +93,7 @@ func TestLoadRequestedReviewers(t *testing.T) {
|
||||
assert.NotNil(t, comment)
|
||||
|
||||
assert.NoError(t, pull.LoadRequestedReviewers(db.DefaultContext))
|
||||
assert.Len(t, pull.RequestedReviewers, 1)
|
||||
assert.Len(t, pull.RequestedReviewers, 6)
|
||||
|
||||
comment, err = issues_model.RemoveReviewRequest(db.DefaultContext, issue, user1, &user_model.User{})
|
||||
assert.NoError(t, err)
|
||||
@ -234,22 +234,6 @@ func TestPullRequest_UpdateCols(t *testing.T) {
|
||||
unittest.CheckConsistencyFor(t, pr)
|
||||
}
|
||||
|
||||
func TestPullRequestList_LoadAttributes(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
prs := []*issues_model.PullRequest{
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}),
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}),
|
||||
}
|
||||
assert.NoError(t, issues_model.PullRequestList(prs).LoadAttributes(db.DefaultContext))
|
||||
for _, pr := range prs {
|
||||
assert.NotNil(t, pr.Issue)
|
||||
assert.Equal(t, pr.IssueID, pr.Issue.ID)
|
||||
}
|
||||
|
||||
assert.NoError(t, issues_model.PullRequestList([]*issues_model.PullRequest{}).LoadAttributes(db.DefaultContext))
|
||||
}
|
||||
|
||||
// TODO TestAddTestPullRequestTask
|
||||
|
||||
func TestPullRequest_IsWorkInProgress(t *testing.T) {
|
||||
|
@ -47,14 +47,9 @@ func (reviews ReviewList) LoadReviewersTeams(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
teamsMap := make(map[int64]*organization_model.Team, 0)
|
||||
for _, teamID := range reviewersTeamsIDs {
|
||||
team, err := organization_model.GetTeamByID(ctx, teamID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
teamsMap[teamID] = team
|
||||
teamsMap, err := organization_model.GetTeamsByIDs(ctx, reviewersTeamsIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, review := range reviews {
|
||||
|
@ -126,42 +126,48 @@ func TestGetReviewersByIssueID(t *testing.T) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
|
||||
expectedReviews := []*issues_model.Review{}
|
||||
expectedReviews = append(expectedReviews,
|
||||
&issues_model.Review{
|
||||
ID: 7,
|
||||
Reviewer: org3,
|
||||
Type: issues_model.ReviewTypeReject,
|
||||
UpdatedUnix: 946684812,
|
||||
},
|
||||
&issues_model.Review{
|
||||
ID: 8,
|
||||
Reviewer: user4,
|
||||
Type: issues_model.ReviewTypeApprove,
|
||||
UpdatedUnix: 946684813,
|
||||
},
|
||||
&issues_model.Review{
|
||||
ID: 9,
|
||||
Reviewer: user2,
|
||||
Type: issues_model.ReviewTypeReject,
|
||||
UpdatedUnix: 946684814,
|
||||
})
|
||||
},
|
||||
&issues_model.Review{
|
||||
ID: 10,
|
||||
Reviewer: user_model.NewGhostUser(),
|
||||
Type: issues_model.ReviewTypeReject,
|
||||
UpdatedUnix: 946684815,
|
||||
},
|
||||
&issues_model.Review{
|
||||
ID: 22,
|
||||
Reviewer: user5,
|
||||
Type: issues_model.ReviewTypeRequest,
|
||||
UpdatedUnix: 946684817,
|
||||
},
|
||||
)
|
||||
|
||||
allReviews, err := issues_model.GetReviewsByIssueID(db.DefaultContext, issue.ID)
|
||||
assert.NoError(t, err)
|
||||
for _, review := range allReviews {
|
||||
assert.NoError(t, review.LoadReviewer(db.DefaultContext))
|
||||
}
|
||||
if assert.Len(t, allReviews, 3) {
|
||||
for i, review := range allReviews {
|
||||
assert.Equal(t, expectedReviews[i].Reviewer, review.Reviewer)
|
||||
assert.Equal(t, expectedReviews[i].Type, review.Type)
|
||||
assert.Equal(t, expectedReviews[i].UpdatedUnix, review.UpdatedUnix)
|
||||
}
|
||||
}
|
||||
|
||||
allReviews, err = issues_model.GetReviewsByIssueID(db.DefaultContext, issue.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, allReviews.LoadReviewers(db.DefaultContext))
|
||||
if assert.Len(t, allReviews, 3) {
|
||||
if assert.Len(t, allReviews, 5) {
|
||||
for i, review := range allReviews {
|
||||
assert.Equal(t, expectedReviews[i].Reviewer, review.Reviewer)
|
||||
assert.Equal(t, expectedReviews[i].Type, review.Type)
|
||||
|
@ -131,3 +131,8 @@ func GetTeamsByOrgIDs(ctx context.Context, orgIDs []int64) (TeamList, error) {
|
||||
teams := make([]*Team, 0, 10)
|
||||
return teams, db.GetEngine(ctx).Where(builder.In("org_id", orgIDs)).Find(&teams)
|
||||
}
|
||||
|
||||
func GetTeamsByIDs(ctx context.Context, teamIDs []int64) (map[int64]*Team, error) {
|
||||
teams := make(map[int64]*Team, len(teamIDs))
|
||||
return teams, db.GetEngine(ctx).Where(builder.In("`id`", teamIDs)).Find(&teams)
|
||||
}
|
||||
|
25
models/organization/team_list_test.go
Normal file
25
models/organization/team_list_test.go
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
org_model "code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_GetTeamsByIDs(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
// 1 owner team, 2 normal team
|
||||
teams, err := org_model.GetTeamsByIDs(db.DefaultContext, []int64{1, 2})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, teams, 2)
|
||||
assert.Equal(t, "Owners", teams[1].Name)
|
||||
assert.Equal(t, "team1", teams[2].Name)
|
||||
}
|
@ -246,18 +246,40 @@ func (repo *Repository) GetDiffOrPatch(base, head string, w io.Writer, patch, bi
|
||||
|
||||
// GetDiff generates and returns patch data between given revisions, optimized for human readability
|
||||
func (repo *Repository) GetDiff(base, head string, w io.Writer) error {
|
||||
return NewCommand(repo.Ctx, "diff", "-p").AddDynamicArguments(base, head).Run(&RunOpts{
|
||||
Dir: repo.Path,
|
||||
Stdout: w,
|
||||
})
|
||||
stderr := new(bytes.Buffer)
|
||||
err := NewCommand(repo.Ctx, "diff", "-p").AddDynamicArguments(base + "..." + head).
|
||||
Run(&RunOpts{
|
||||
Dir: repo.Path,
|
||||
Stdout: w,
|
||||
Stderr: stderr,
|
||||
})
|
||||
if err != nil && bytes.Contains(stderr.Bytes(), []byte("no merge base")) {
|
||||
return NewCommand(repo.Ctx, "diff", "-p").AddDynamicArguments(base, head).
|
||||
Run(&RunOpts{
|
||||
Dir: repo.Path,
|
||||
Stdout: w,
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// GetDiffBinary generates and returns patch data between given revisions, including binary diffs.
|
||||
func (repo *Repository) GetDiffBinary(base, head string, w io.Writer) error {
|
||||
return NewCommand(repo.Ctx, "diff", "-p", "--binary", "--histogram").AddDynamicArguments(base, head).Run(&RunOpts{
|
||||
Dir: repo.Path,
|
||||
Stdout: w,
|
||||
})
|
||||
stderr := new(bytes.Buffer)
|
||||
err := NewCommand(repo.Ctx, "diff", "-p", "--binary", "--histogram").AddDynamicArguments(base + "..." + head).
|
||||
Run(&RunOpts{
|
||||
Dir: repo.Path,
|
||||
Stdout: w,
|
||||
Stderr: stderr,
|
||||
})
|
||||
if err != nil && bytes.Contains(stderr.Bytes(), []byte("no merge base")) {
|
||||
return NewCommand(repo.Ctx, "diff", "-p", "--binary", "--histogram").AddDynamicArguments(base, head).
|
||||
Run(&RunOpts{
|
||||
Dir: repo.Path,
|
||||
Stdout: w,
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// GetPatch generates and returns format-patch data between given revisions, able to be used with `git apply`
|
||||
|
@ -54,8 +54,8 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m
|
||||
RepoIDs: options.RepoIDs,
|
||||
AllPublic: options.AllPublic,
|
||||
RepoCond: nil,
|
||||
AssigneeID: convertID(options.AssigneeID),
|
||||
PosterID: convertID(options.PosterID),
|
||||
AssigneeID: optional.Some(convertID(options.AssigneeID)),
|
||||
PosterID: options.PosterID,
|
||||
MentionedID: convertID(options.MentionID),
|
||||
ReviewRequestedID: convertID(options.ReviewRequestedID),
|
||||
ReviewedID: convertID(options.ReviewedID),
|
||||
|
@ -40,14 +40,14 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
|
||||
|
||||
if opts.ProjectID > 0 {
|
||||
searchOpt.ProjectID = optional.Some(opts.ProjectID)
|
||||
} else if opts.ProjectID == -1 { // FIXME: this is inconsistent from other places
|
||||
} else if opts.ProjectID == db.NoConditionID { // FIXME: this is inconsistent from other places
|
||||
searchOpt.ProjectID = optional.Some[int64](0) // Those issues with no project(projectid==0)
|
||||
}
|
||||
|
||||
if opts.AssigneeID > 0 {
|
||||
searchOpt.AssigneeID = optional.Some(opts.AssigneeID)
|
||||
} else if opts.AssigneeID == -1 { // FIXME: this is inconsistent from other places
|
||||
searchOpt.AssigneeID = optional.Some[int64](0)
|
||||
if opts.AssigneeID.Value() == db.NoConditionID {
|
||||
searchOpt.AssigneeID = optional.Some[int64](0) // FIXME: this is inconsistent from other places, 0 means "no assignee"
|
||||
} else if opts.AssigneeID.Value() != 0 {
|
||||
searchOpt.AssigneeID = opts.AssigneeID
|
||||
}
|
||||
|
||||
// See the comment of issues_model.SearchOptions for the reason why we need to convert
|
||||
@ -62,7 +62,7 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
|
||||
}
|
||||
|
||||
searchOpt.ProjectColumnID = convertID(opts.ProjectColumnID)
|
||||
searchOpt.PosterID = convertID(opts.PosterID)
|
||||
searchOpt.PosterID = opts.PosterID
|
||||
searchOpt.MentionID = convertID(opts.MentionedID)
|
||||
searchOpt.ReviewedID = convertID(opts.ReviewedID)
|
||||
searchOpt.ReviewRequestedID = convertID(opts.ReviewRequestedID)
|
||||
|
@ -191,7 +191,7 @@ func searchIssueByID(t *testing.T) {
|
||||
},
|
||||
{
|
||||
// NOTE: This tests no assignees filtering and also ToSearchOptions() to ensure it will set AssigneeID to 0 when it is passed as -1.
|
||||
opts: *ToSearchOptions("", &issues.IssuesOptions{AssigneeID: -1}),
|
||||
opts: *ToSearchOptions("", &issues.IssuesOptions{AssigneeID: optional.Some(db.NoConditionID)}),
|
||||
expectedIDs: []int64{22, 21, 16, 15, 14, 13, 12, 11, 20, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2},
|
||||
},
|
||||
{
|
||||
|
@ -1102,7 +1102,6 @@ delete_preexisting_success=Smazány nepřijaté soubory v %s
|
||||
blame_prior=Zobrazit blame před touto změnou
|
||||
blame.ignore_revs=Ignorování revizí v <a href="%s">.git-blame-ignorerevs</a>. Klikněte zde <a href="%s">pro obejití</a> a zobrazení normálního pohledu blame.
|
||||
blame.ignore_revs.failed=Nepodařilo se ignorovat revize v <a href="%s">.git-blame-ignore-revs</a>.
|
||||
author_search_tooltip=Zobrazí maximálně 30 uživatelů
|
||||
|
||||
tree_path_not_found_commit=Cesta %[1]s v commitu %[2]s neexistuje
|
||||
tree_path_not_found_branch=Cesta %[1]s ve větvi %[2]s neexistuje
|
||||
@ -1521,7 +1520,6 @@ issues.filter_assignee=Zpracovatel
|
||||
issues.filter_assginee_no_select=Všichni zpracovatelé
|
||||
issues.filter_assginee_no_assignee=Bez zpracovatele
|
||||
issues.filter_poster=Autor
|
||||
issues.filter_poster_no_select=Všichni autoři
|
||||
issues.filter_type=Typ
|
||||
issues.filter_type.all_issues=Všechny úkoly
|
||||
issues.filter_type.assigned_to_you=Přiřazené vám
|
||||
|
@ -1058,7 +1058,6 @@ delete_preexisting_success=Nicht übernommene Dateien in %s gelöscht
|
||||
blame_prior=Blame vor dieser Änderung anzeigen
|
||||
blame.ignore_revs=Revisionen in <a href="%s">.git-blame-ignore-revs</a> werden ignoriert. Klicke <a href="%s">hier, um das zu umgehen</a> und die normale Blame-Ansicht zu sehen.
|
||||
blame.ignore_revs.failed=Fehler beim Ignorieren der Revisionen in <a href="%s">.git-blame-ignore-revs</a>.
|
||||
author_search_tooltip=Zeigt maximal 30 Benutzer
|
||||
|
||||
tree_path_not_found_commit=Pfad %[1]s existiert nicht in Commit%[2]s
|
||||
tree_path_not_found_branch=Pfad %[1]s existiert nicht in Branch %[2]s
|
||||
@ -1460,7 +1459,6 @@ issues.filter_assignee=Zuständig
|
||||
issues.filter_assginee_no_select=Alle Zuständigen
|
||||
issues.filter_assginee_no_assignee=Niemand zuständig
|
||||
issues.filter_poster=Autor
|
||||
issues.filter_poster_no_select=Alle Autoren
|
||||
issues.filter_type=Typ
|
||||
issues.filter_type.all_issues=Alle Issues
|
||||
issues.filter_type.assigned_to_you=Dir zugewiesen
|
||||
|
@ -991,7 +991,6 @@ delete_preexisting_success=Διαγράφηκαν τα μη υιοθετημέν
|
||||
blame_prior=Προβολή ευθύνης πριν από αυτή την αλλαγή
|
||||
blame.ignore_revs=Αγνόηση των αναθεωρήσεων στο <a href="%s">.git-blame-ignore-revs</a>. Πατήστε <a href="%s">εδώ</a> για να το παρακάμψετε και να δείτε την κανονική προβολή ευθυνών.
|
||||
blame.ignore_revs.failed=Αποτυχία αγνόησης των αναθεωρήσεων στο <a href="%s">.git-blame-ignore-revs</a>.
|
||||
author_search_tooltip=Εμφάνιση το πολύ 30 χρηστών
|
||||
|
||||
tree_path_not_found_commit=Η διαδρομή %[1]s δεν υπάρχει στην υποβολή %[2]s
|
||||
tree_path_not_found_branch=Η διαδρομή %[1]s δεν υπάρχει στον κλάδο %[2]s
|
||||
@ -1383,7 +1382,6 @@ issues.filter_assignee=Αποδέκτης
|
||||
issues.filter_assginee_no_select=Όλοι οι αποδέκτες
|
||||
issues.filter_assginee_no_assignee=Κανένας Αποδέκτης
|
||||
issues.filter_poster=Συγγραφέας
|
||||
issues.filter_poster_no_select=Όλοι οι συγγραφείς
|
||||
issues.filter_type=Τύπος
|
||||
issues.filter_type.all_issues=Όλα τα ζητήματα
|
||||
issues.filter_type.assigned_to_you=Ανατέθηκαν σε εσάς
|
||||
|
@ -981,7 +981,6 @@ delete_preexisting_success=Eliminó archivos no adoptados en %s
|
||||
blame_prior=Ver la culpa antes de este cambio
|
||||
blame.ignore_revs=Ignorando revisiones en <a href="%s">.git-blame-ignore-revs</a>. Haga clic <a href="%s">aquí para saltar</a> y para a la vista normal.
|
||||
blame.ignore_revs.failed=No se pudieron ignorar las revisiones en <a href="%s">.git-blame-ignore-revs</a>.
|
||||
author_search_tooltip=Muestra un máximo de 30 usuarios
|
||||
|
||||
tree_path_not_found_commit=La ruta %[1]s no existe en el commit %[2]s
|
||||
tree_path_not_found_branch=La ruta %[1]s no existe en la rama %[2]s
|
||||
@ -1373,7 +1372,6 @@ issues.filter_assignee=Asignada a
|
||||
issues.filter_assginee_no_select=Todos los asignados
|
||||
issues.filter_assginee_no_assignee=Sin asignado
|
||||
issues.filter_poster=Autor
|
||||
issues.filter_poster_no_select=Todos los autores
|
||||
issues.filter_type=Tipo
|
||||
issues.filter_type.all_issues=Todas las incidencias
|
||||
issues.filter_type.assigned_to_you=Asignadas a ti
|
||||
|
@ -1109,7 +1109,6 @@ delete_preexisting_success=Fichiers dépossédés supprimés dans %s.
|
||||
blame_prior=Voir le blame avant cette modification
|
||||
blame.ignore_revs=Les révisions dans <a href="%s">.git-blame-ignore-revs</a> sont ignorées. Vous pouvez quand même <a href="%s">voir ces blâmes</a>.
|
||||
blame.ignore_revs.failed=Impossible d'ignorer les révisions dans <a href="%s">.git-blame-ignore-revs</a>.
|
||||
author_search_tooltip=Affiche un maximum de 30 utilisateurs
|
||||
|
||||
tree_path_not_found_commit=Le chemin %[1]s n’existe pas dans la révision %[2]s.
|
||||
tree_path_not_found_branch=Le chemin %[1]s n’existe pas dans la branche %[2]s.
|
||||
@ -1528,7 +1527,6 @@ issues.filter_assignee=Assigné
|
||||
issues.filter_assginee_no_select=Tous les assignés
|
||||
issues.filter_assginee_no_assignee=Aucun assigné
|
||||
issues.filter_poster=Auteur
|
||||
issues.filter_poster_no_select=Tous les auteurs
|
||||
issues.filter_type=Type
|
||||
issues.filter_type.all_issues=Tous les tickets
|
||||
issues.filter_type.assigned_to_you=Qui vous sont assignés
|
||||
|
@ -1109,7 +1109,6 @@ delete_preexisting_success=Scriosta comhaid neamhghlactha i %s
|
||||
blame_prior=Féach ar an milleán roimh an athrú seo
|
||||
blame.ignore_revs=Ag déanamh neamhairde de leasuithe i <a href="%s">.git-blame-ignore-revs</a>. Cliceáil <a href="%s">anseo chun seachaint</a> agus an gnáth-amharc milleán a fheiceáil.
|
||||
blame.ignore_revs.failed=Theip ar neamhaird a dhéanamh ar leasuithe i <a href="%s">.git-blame-ignore-revs</a>.
|
||||
author_search_tooltip=Taispeánann 30 úsáideoir ar a mhéad
|
||||
|
||||
tree_path_not_found_commit=Níl cosán %[1]s ann i dtiomantas %[2]s
|
||||
tree_path_not_found_branch=Níl cosán %[1]s ann i mbrainse %[2]s
|
||||
@ -1528,7 +1527,6 @@ issues.filter_assignee=Sannaitheoir
|
||||
issues.filter_assginee_no_select=Gach sannaithe
|
||||
issues.filter_assginee_no_assignee=Gan sannaitheoir
|
||||
issues.filter_poster=Údar
|
||||
issues.filter_poster_no_select=Gach údair
|
||||
issues.filter_type=Cineál
|
||||
issues.filter_type.all_issues=Gach saincheist
|
||||
issues.filter_type.assigned_to_you=Sannta duit
|
||||
|
@ -1144,7 +1144,6 @@ issues.filter_assignee=Assegnatario
|
||||
issues.filter_assginee_no_select=Tutte le assegnazioni
|
||||
issues.filter_assginee_no_assignee=Nessun assegnatario
|
||||
issues.filter_poster=Autore
|
||||
issues.filter_poster_no_select=Tutti gli autori
|
||||
issues.filter_type=Tipo
|
||||
issues.filter_type.all_issues=Tutti i problemi
|
||||
issues.filter_type.assigned_to_you=Assegnati a te
|
||||
|
@ -1104,7 +1104,6 @@ delete_preexisting_success=%s の未登録ファイルを削除しました
|
||||
blame_prior=この変更より前のBlameを表示
|
||||
blame.ignore_revs=<a href="%s">.git-blame-ignore-revs</a> で指定されたリビジョンは除外しています。 これを迂回して通常のBlame表示を見るには <a href="%s">ここ</a>をクリック。
|
||||
blame.ignore_revs.failed=<a href="%s">.git-blame-ignore-revs</a> によるリビジョンの無視は失敗しました。
|
||||
author_search_tooltip=最大30人までのユーザーを表示
|
||||
|
||||
tree_path_not_found_commit=パス %[1]s はコミット %[2]s に存在しません
|
||||
tree_path_not_found_branch=パス %[1]s はブランチ %[2]s に存在しません
|
||||
@ -1523,7 +1522,6 @@ issues.filter_assignee=担当者
|
||||
issues.filter_assginee_no_select=すべての担当者
|
||||
issues.filter_assginee_no_assignee=担当者なし
|
||||
issues.filter_poster=作成者
|
||||
issues.filter_poster_no_select=すべての作成者
|
||||
issues.filter_type=タイプ
|
||||
issues.filter_type.all_issues=すべてのイシュー
|
||||
issues.filter_type.assigned_to_you=自分が担当
|
||||
|
@ -996,7 +996,6 @@ delete_preexisting_success=Dzēst nepārņemtos failus direktorijā %s
|
||||
blame_prior=Aplūkot vainīgo par izmaiņām pirms šīs revīzijas
|
||||
blame.ignore_revs=Neņem vērā izmaiņas no <a href="%s">.git-blame-ignore-revs</a>. Nospiediet <a href="%s">šeit, lai to apietu</a> un redzētu visu izmaiņu skatu.
|
||||
blame.ignore_revs.failed=Neizdevās neņemt vērā izmaiņas no <a href="%s">.git-blam-ignore-revs</a>.
|
||||
author_search_tooltip=Tiks attēloti ne vairāk kā 30 lietotāji
|
||||
|
||||
tree_path_not_found_commit=Revīzijā %[2]s neeksistē ceļš %[1]s
|
||||
tree_path_not_found_branch=Atzarā %[2]s nepastāv ceļš %[1]s
|
||||
@ -1389,7 +1388,6 @@ issues.filter_assignee=Atbildīgais
|
||||
issues.filter_assginee_no_select=Visi atbildīgie
|
||||
issues.filter_assginee_no_assignee=Nav atbildīgā
|
||||
issues.filter_poster=Autors
|
||||
issues.filter_poster_no_select=Visi autori
|
||||
issues.filter_type=Veids
|
||||
issues.filter_type.all_issues=Visas problēmas
|
||||
issues.filter_type.assigned_to_you=Piešķirtās Jums
|
||||
|
@ -1142,7 +1142,6 @@ issues.filter_assignee=Aangewezene
|
||||
issues.filter_assginee_no_select=Alle toegewezen personen
|
||||
issues.filter_assginee_no_assignee=Geen verantwoordelijke
|
||||
issues.filter_poster=Auteur
|
||||
issues.filter_poster_no_select=Alle auteurs
|
||||
issues.filter_type=Type
|
||||
issues.filter_type.all_issues=Alle kwesties
|
||||
issues.filter_type.assigned_to_you=Aan jou toegewezen
|
||||
|
@ -990,7 +990,6 @@ delete_preexisting=Excluir arquivos pré-existentes
|
||||
delete_preexisting_content=Excluir arquivos em %s
|
||||
delete_preexisting_success=Arquivos órfãos excluídos em %s
|
||||
blame_prior=Ver a responsabilização anterior a esta modificação
|
||||
author_search_tooltip=Mostra um máximo de 30 usuários
|
||||
|
||||
|
||||
transfer.accept=Aceitar transferência
|
||||
@ -1381,7 +1380,6 @@ issues.filter_assignee=Atribuído
|
||||
issues.filter_assginee_no_select=Todos os responsáveis
|
||||
issues.filter_assginee_no_assignee=Sem responsável
|
||||
issues.filter_poster=Autor
|
||||
issues.filter_poster_no_select=Todos os autores
|
||||
issues.filter_type=Tipo
|
||||
issues.filter_type.all_issues=Todas as issues
|
||||
issues.filter_type.assigned_to_you=Atribuídos a você
|
||||
|
@ -1109,7 +1109,6 @@ delete_preexisting_success=Eliminados os ficheiros não adoptados em %s
|
||||
blame_prior=Ver a responsabilização anterior a esta modificação
|
||||
blame.ignore_revs=Ignorando as revisões em <a href="%s">.git-blame-ignore-revs</a>. Clique <a href="%s">aqui para contornar</a> e ver a vista normal de responsabilização.
|
||||
blame.ignore_revs.failed=Falhou ao ignorar as revisões em <a href="%s">.git-blame-ignore-revs</a>.
|
||||
author_search_tooltip=Mostra um máximo de 30 utilizadores
|
||||
|
||||
tree_path_not_found_commit=A localização %[1]s não existe no cometimento %[2]s
|
||||
tree_path_not_found_branch=A localização %[1]s não existe no ramo %[2]s
|
||||
@ -1528,7 +1527,6 @@ issues.filter_assignee=Encarregado
|
||||
issues.filter_assginee_no_select=Todos os encarregados
|
||||
issues.filter_assginee_no_assignee=Sem encarregado
|
||||
issues.filter_poster=Autor(a)
|
||||
issues.filter_poster_no_select=Todos os autores
|
||||
issues.filter_type=Tipo
|
||||
issues.filter_type.all_issues=Todas as questões
|
||||
issues.filter_type.assigned_to_you=Atribuídas a si
|
||||
|
@ -977,7 +977,6 @@ delete_preexisting=Удалить уже существующие файлы
|
||||
delete_preexisting_content=Удалить файлы из %s
|
||||
delete_preexisting_success=Удалены непринятые файлы в %s
|
||||
blame_prior=Показать авторство предшествующих изменений
|
||||
author_search_tooltip=Показывает максимум 30 пользователей
|
||||
|
||||
tree_path_not_found_commit=Путь %[1]s не существует в коммите %[2]s
|
||||
tree_path_not_found_branch=Путь %[1]s не существует в ветке %[2]s
|
||||
@ -1360,7 +1359,6 @@ issues.filter_assignee=Назначено
|
||||
issues.filter_assginee_no_select=Все назначения
|
||||
issues.filter_assginee_no_assignee=Нет ответственного
|
||||
issues.filter_poster=Автор
|
||||
issues.filter_poster_no_select=Все авторы
|
||||
issues.filter_type=Тип
|
||||
issues.filter_type.all_issues=Все задачи
|
||||
issues.filter_type.assigned_to_you=Назначено вам
|
||||
|
@ -1073,7 +1073,6 @@ delete_preexisting_success=%s içindeki kabul edilmeyen dosyalar silindi
|
||||
blame_prior=Bu değişiklikten önceki suçu görüntüle
|
||||
blame.ignore_revs=<a href="%s">.git-blame-ignore-revs</a> dosyasındaki sürümler yok sayılıyor. Bunun yerine normal sorumlu görüntüsü için <a href="%s">buraya tıklayın</a>.
|
||||
blame.ignore_revs.failed=<a href="%s">.git-blame-ignore-revs</a> dosyasındaki sürümler yok sayılamadı.
|
||||
author_search_tooltip=En fazla 30 kullanıcı görüntüler
|
||||
|
||||
tree_path_not_found_commit=%[1] yolu, %[2]s işlemesinde mevcut değil
|
||||
tree_path_not_found_branch=%[1] yolu, %[2]s dalında mevcut değil
|
||||
@ -1481,7 +1480,6 @@ issues.filter_assignee=Atanan
|
||||
issues.filter_assginee_no_select=Tüm atananlar
|
||||
issues.filter_assginee_no_assignee=Atanan yok
|
||||
issues.filter_poster=Yazar
|
||||
issues.filter_poster_no_select=Tüm yazarlar
|
||||
issues.filter_type=Tür
|
||||
issues.filter_type.all_issues=Tüm konular
|
||||
issues.filter_type.assigned_to_you=Size atanan
|
||||
|
@ -1074,7 +1074,6 @@ delete_preexisting_success=删除 %s 中未收录的文件
|
||||
blame_prior=查看此更改前的 blame
|
||||
blame.ignore_revs=忽略 <a href="%s">.git-blame-ignore-revs</a> 的修订。点击 <a href="%s">绕过</a> 并查看正常的 Blame 视图。
|
||||
blame.ignore_revs.failed=忽略 <a href="%s">.git-blame-ignore-revs</a> 版本失败。
|
||||
author_search_tooltip=最多显示30个用户
|
||||
|
||||
tree_path_not_found_commit=路径%[1]s 在提交 %[2]s 中不存在
|
||||
tree_path_not_found_branch=路径 %[1]s 不存在于分支 %[2]s 中。
|
||||
@ -1489,7 +1488,6 @@ issues.filter_assignee=指派人筛选
|
||||
issues.filter_assginee_no_select=所有指派成员
|
||||
issues.filter_assginee_no_assignee=未指派
|
||||
issues.filter_poster=作者
|
||||
issues.filter_poster_no_select=所有作者
|
||||
issues.filter_type=类型筛选
|
||||
issues.filter_type.all_issues=所有工单
|
||||
issues.filter_type.assigned_to_you=指派给您的
|
||||
|
@ -904,7 +904,6 @@ delete_preexisting=刪除既有的檔案
|
||||
delete_preexisting_content=刪除 %s 中的檔案
|
||||
delete_preexisting_success=刪除 %s 中未接管的檔案
|
||||
blame_prior=檢視此變更前的 Blame
|
||||
author_search_tooltip=最多顯示 30 位使用者
|
||||
|
||||
|
||||
transfer.accept=同意轉移
|
||||
@ -1266,7 +1265,6 @@ issues.filter_assignee=負責人
|
||||
issues.filter_assginee_no_select=所有負責人
|
||||
issues.filter_assginee_no_assignee=沒有負責人
|
||||
issues.filter_poster=作者
|
||||
issues.filter_poster_no_select=所有作者
|
||||
issues.filter_type=類型
|
||||
issues.filter_type.all_issues=所有問題
|
||||
issues.filter_type.assigned_to_you=指派給您的
|
||||
|
@ -319,6 +319,11 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||
ctx.Error(http.StatusForbidden, "CanWriteIssuesOrPulls", "write permission is required")
|
||||
return nil, nil, fmt.Errorf("permission denied")
|
||||
}
|
||||
|
||||
var (
|
||||
labelIDs []int64
|
||||
labelNames []string
|
||||
@ -350,10 +355,5 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
return issue, labels, err
|
||||
}
|
||||
|
@ -139,43 +139,12 @@ func ListPullRequests(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
apiPrs := make([]*api.PullRequest, len(prs))
|
||||
// NOTE: load repository first, so that issue.Repo will be filled with pr.BaseRepo
|
||||
if err := prs.LoadRepositories(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadRepositories", err)
|
||||
return
|
||||
}
|
||||
issueList, err := prs.LoadIssues(ctx)
|
||||
apiPrs, err := convert.ToAPIPullRequests(ctx, ctx.Repo.Repository, prs, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadIssues", err)
|
||||
ctx.Error(http.StatusInternalServerError, "ToAPIPullRequests", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := issueList.LoadLabels(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadLabels", err)
|
||||
return
|
||||
}
|
||||
if err := issueList.LoadPosters(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadPoster", err)
|
||||
return
|
||||
}
|
||||
if err := issueList.LoadAttachments(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
|
||||
return
|
||||
}
|
||||
if err := issueList.LoadMilestones(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadMilestones", err)
|
||||
return
|
||||
}
|
||||
if err := issueList.LoadAssignees(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadAssignees", err)
|
||||
return
|
||||
}
|
||||
|
||||
for i := range prs {
|
||||
apiPrs[i] = convert.ToAPIPullRequest(ctx, prs[i], ctx.Doer)
|
||||
}
|
||||
|
||||
ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
|
||||
ctx.SetTotalCountHeader(maxResults)
|
||||
ctx.JSON(http.StatusOK, &apiPrs)
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/web/shared/issue"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
@ -334,23 +335,15 @@ func ViewProject(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var labelIDs []int64
|
||||
// 1,-2 means including label 1 and excluding label 2
|
||||
// 0 means issues with no label
|
||||
// blank means labels will not be filtered for issues
|
||||
selectLabels := ctx.FormString("labels")
|
||||
if selectLabels != "" {
|
||||
labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
|
||||
if err != nil {
|
||||
ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true)
|
||||
}
|
||||
labelIDs := issue.PrepareFilterIssueLabels(ctx, project.RepoID, project.Owner)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
assigneeID := ctx.FormInt64("assignee")
|
||||
assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
|
||||
|
||||
issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{
|
||||
LabelIDs: labelIDs,
|
||||
AssigneeID: assigneeID,
|
||||
AssigneeID: optional.Some(assigneeID),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadIssuesOfColumns", err)
|
||||
@ -426,8 +419,6 @@ func ViewProject(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
||||
|
||||
ctx.Data["SelectLabels"] = selectLabels
|
||||
ctx.Data["AssigneeID"] = assigneeID
|
||||
|
||||
project.RenderedContent = templates.NewRenderUtils(ctx).MarkdownToHtml(project.Description)
|
||||
|
@ -17,12 +17,12 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/web/shared/issue"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
@ -263,8 +263,10 @@ func getUserIDForFilter(ctx *context.Context, queryName string) int64 {
|
||||
return user.ID
|
||||
}
|
||||
|
||||
// ListIssues list the issues of a repository
|
||||
func ListIssues(ctx *context.Context) {
|
||||
// SearchRepoIssuesJSON lists the issues of a repository
|
||||
// This function was copied from API (decouple the web and API routes),
|
||||
// it is only used by frontend to search some dependency or related issues
|
||||
func SearchRepoIssuesJSON(ctx *context.Context) {
|
||||
before, since, err := context.GetQueryBeforeSince(ctx.Base)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusUnprocessableEntity, err.Error())
|
||||
@ -286,20 +288,11 @@ func ListIssues(ctx *context.Context) {
|
||||
keyword = ""
|
||||
}
|
||||
|
||||
var labelIDs []int64
|
||||
if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
|
||||
labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, splitted)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var mileIDs []int64
|
||||
if part := strings.Split(ctx.FormString("milestones"), ","); len(part) > 0 {
|
||||
for i := range part {
|
||||
// uses names and fall back to ids
|
||||
// non existent milestones are discarded
|
||||
// non-existent milestones are discarded
|
||||
mile, err := issues_model.GetMilestoneByRepoIDANDName(ctx, ctx.Repo.Repository.ID, part[i])
|
||||
if err == nil {
|
||||
mileIDs = append(mileIDs, mile.ID)
|
||||
@ -370,17 +363,8 @@ func ListIssues(ctx *context.Context) {
|
||||
if before != 0 {
|
||||
searchOpt.UpdatedBeforeUnix = optional.Some(before)
|
||||
}
|
||||
if len(labelIDs) == 1 && labelIDs[0] == 0 {
|
||||
searchOpt.NoLabelOnly = true
|
||||
} else {
|
||||
for _, labelID := range labelIDs {
|
||||
if labelID > 0 {
|
||||
searchOpt.IncludedLabelIDs = append(searchOpt.IncludedLabelIDs, labelID)
|
||||
} else {
|
||||
searchOpt.ExcludedLabelIDs = append(searchOpt.ExcludedLabelIDs, -labelID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: the "labels" query parameter is never used, so no need to handle it
|
||||
|
||||
if len(mileIDs) == 1 && mileIDs[0] == db.NoConditionID {
|
||||
searchOpt.MilestoneIDs = []int64{0}
|
||||
@ -503,8 +487,8 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
||||
if !util.SliceContainsString(types, viewType, true) {
|
||||
viewType = "all"
|
||||
}
|
||||
// TODO: "assignee" should also use GetFilterUserIDByName in the future to support usernames directly
|
||||
assigneeID := ctx.FormInt64("assignee")
|
||||
|
||||
assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
|
||||
posterUsername := ctx.FormString("poster")
|
||||
posterUserID := shared_user.GetFilterUserIDByName(ctx, posterUsername)
|
||||
var mentionedID, reviewRequestedID, reviewedID int64
|
||||
@ -512,7 +496,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
||||
if ctx.IsSigned {
|
||||
switch viewType {
|
||||
case "created_by":
|
||||
posterUserID = ctx.Doer.ID
|
||||
posterUserID = optional.Some(ctx.Doer.ID)
|
||||
case "mentioned":
|
||||
mentionedID = ctx.Doer.ID
|
||||
case "assigned":
|
||||
@ -525,18 +509,6 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
||||
}
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
var labelIDs []int64
|
||||
// 1,-2 means including label 1 and excluding label 2
|
||||
// 0 means issues with no label
|
||||
// blank means labels will not be filtered for issues
|
||||
selectLabels := ctx.FormString("labels")
|
||||
if selectLabels != "" {
|
||||
labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
|
||||
if err != nil {
|
||||
ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true)
|
||||
}
|
||||
}
|
||||
|
||||
keyword := strings.Trim(ctx.FormString("q"), " ")
|
||||
if bytes.Contains([]byte(keyword), []byte{0x00}) {
|
||||
keyword = ""
|
||||
@ -547,13 +519,18 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
||||
mileIDs = []int64{milestoneID}
|
||||
}
|
||||
|
||||
labelIDs := issue.PrepareFilterIssueLabels(ctx, repo.ID, ctx.Repo.Owner)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
var issueStats *issues_model.IssueStats
|
||||
statsOpts := &issues_model.IssuesOptions{
|
||||
RepoIDs: []int64{repo.ID},
|
||||
LabelIDs: labelIDs,
|
||||
MilestoneIDs: mileIDs,
|
||||
ProjectID: projectID,
|
||||
AssigneeID: assigneeID,
|
||||
AssigneeID: optional.Some(assigneeID),
|
||||
MentionedID: mentionedID,
|
||||
PosterID: posterUserID,
|
||||
ReviewRequestedID: reviewRequestedID,
|
||||
@ -634,7 +611,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
||||
PageSize: setting.UI.IssuePagingNum,
|
||||
},
|
||||
RepoIDs: []int64{repo.ID},
|
||||
AssigneeID: assigneeID,
|
||||
AssigneeID: optional.Some(assigneeID),
|
||||
PosterID: posterUserID,
|
||||
MentionedID: mentionedID,
|
||||
ReviewRequestedID: reviewRequestedID,
|
||||
@ -709,49 +686,6 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
||||
return
|
||||
}
|
||||
|
||||
labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLabelsByRepoID", err)
|
||||
return
|
||||
}
|
||||
|
||||
if repo.Owner.IsOrganization() {
|
||||
orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLabelsByOrgID", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["OrgLabels"] = orgLabels
|
||||
labels = append(labels, orgLabels...)
|
||||
}
|
||||
|
||||
// Get the exclusive scope for every label ID
|
||||
labelExclusiveScopes := make([]string, 0, len(labelIDs))
|
||||
for _, labelID := range labelIDs {
|
||||
foundExclusiveScope := false
|
||||
for _, label := range labels {
|
||||
if label.ID == labelID || label.ID == -labelID {
|
||||
labelExclusiveScopes = append(labelExclusiveScopes, label.ExclusiveScope())
|
||||
foundExclusiveScope = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundExclusiveScope {
|
||||
labelExclusiveScopes = append(labelExclusiveScopes, "")
|
||||
}
|
||||
}
|
||||
|
||||
for _, l := range labels {
|
||||
l.LoadSelectedLabelsAfterClick(labelIDs, labelExclusiveScopes)
|
||||
}
|
||||
ctx.Data["Labels"] = labels
|
||||
ctx.Data["NumLabels"] = len(labels)
|
||||
|
||||
if ctx.FormInt64("assignee") == 0 {
|
||||
assigneeID = 0 // Reset ID to prevent unexpected selection of assignee.
|
||||
}
|
||||
|
||||
ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, ctx.Repo.RepoLink)
|
||||
|
||||
ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 {
|
||||
@ -792,13 +726,11 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
||||
ctx.Data["OpenCount"] = issueStats.OpenCount
|
||||
ctx.Data["ClosedCount"] = issueStats.ClosedCount
|
||||
ctx.Data["SelLabelIDs"] = labelIDs
|
||||
ctx.Data["SelectLabels"] = selectLabels
|
||||
ctx.Data["ViewType"] = viewType
|
||||
ctx.Data["SortType"] = sortType
|
||||
ctx.Data["MilestoneID"] = milestoneID
|
||||
ctx.Data["ProjectID"] = projectID
|
||||
ctx.Data["AssigneeID"] = assigneeID
|
||||
ctx.Data["PosterUserID"] = posterUserID
|
||||
ctx.Data["PosterUsername"] = posterUsername
|
||||
ctx.Data["Keyword"] = keyword
|
||||
ctx.Data["IsShowClosed"] = isShowClosed
|
||||
@ -810,19 +742,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt
|
||||
default:
|
||||
ctx.Data["State"] = "open"
|
||||
}
|
||||
|
||||
pager.AddParamString("q", keyword)
|
||||
pager.AddParamString("type", viewType)
|
||||
pager.AddParamString("sort", sortType)
|
||||
pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
|
||||
pager.AddParamString("labels", fmt.Sprint(selectLabels))
|
||||
pager.AddParamString("milestone", fmt.Sprint(milestoneID))
|
||||
pager.AddParamString("project", fmt.Sprint(projectID))
|
||||
pager.AddParamString("assignee", fmt.Sprint(assigneeID))
|
||||
pager.AddParamString("poster", posterUsername)
|
||||
if showArchivedLabels {
|
||||
pager.AddParamString("archived_labels", "true")
|
||||
}
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/web/shared/issue"
|
||||
shared_user "code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
@ -307,23 +308,13 @@ func ViewProject(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var labelIDs []int64
|
||||
// 1,-2 means including label 1 and excluding label 2
|
||||
// 0 means issues with no label
|
||||
// blank means labels will not be filtered for issues
|
||||
selectLabels := ctx.FormString("labels")
|
||||
if selectLabels != "" {
|
||||
labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
|
||||
if err != nil {
|
||||
ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true)
|
||||
}
|
||||
}
|
||||
labelIDs := issue.PrepareFilterIssueLabels(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner)
|
||||
|
||||
assigneeID := ctx.FormInt64("assignee")
|
||||
assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future
|
||||
|
||||
issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{
|
||||
LabelIDs: labelIDs,
|
||||
AssigneeID: assigneeID,
|
||||
AssigneeID: optional.Some(assigneeID),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("LoadIssuesOfColumns", err)
|
||||
@ -409,8 +400,6 @@ func ViewProject(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
||||
|
||||
ctx.Data["SelectLabels"] = selectLabels
|
||||
ctx.Data["AssigneeID"] = assigneeID
|
||||
|
||||
rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository)
|
||||
|
71
routers/web/shared/issue/issue_label.go
Normal file
71
routers/web/shared/issue/issue_label.go
Normal file
@ -0,0 +1,71 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package issue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// PrepareFilterIssueLabels reads the "labels" query parameter, sets `ctx.Data["Labels"]` and `ctx.Data["SelectLabels"]`
|
||||
func PrepareFilterIssueLabels(ctx *context.Context, repoID int64, owner *user_model.User) (labelIDs []int64) {
|
||||
// 1,-2 means including label 1 and excluding label 2
|
||||
// 0 means issues with no label
|
||||
// blank means labels will not be filtered for issues
|
||||
selectLabels := ctx.FormString("labels")
|
||||
if selectLabels != "" {
|
||||
var err error
|
||||
labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
|
||||
if err != nil {
|
||||
ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true)
|
||||
}
|
||||
}
|
||||
|
||||
var allLabels []*issues_model.Label
|
||||
if repoID != 0 {
|
||||
repoLabels, err := issues_model.GetLabelsByRepoID(ctx, repoID, "", db.ListOptions{})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLabelsByRepoID", err)
|
||||
return nil
|
||||
}
|
||||
allLabels = append(allLabels, repoLabels...)
|
||||
}
|
||||
|
||||
if owner != nil && owner.IsOrganization() {
|
||||
orgLabels, err := issues_model.GetLabelsByOrgID(ctx, owner.ID, "", db.ListOptions{})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLabelsByOrgID", err)
|
||||
return nil
|
||||
}
|
||||
allLabels = append(allLabels, orgLabels...)
|
||||
}
|
||||
|
||||
// Get the exclusive scope for every label ID
|
||||
labelExclusiveScopes := make([]string, 0, len(labelIDs))
|
||||
for _, labelID := range labelIDs {
|
||||
foundExclusiveScope := false
|
||||
for _, label := range allLabels {
|
||||
if label.ID == labelID || label.ID == -labelID {
|
||||
labelExclusiveScopes = append(labelExclusiveScopes, label.ExclusiveScope())
|
||||
foundExclusiveScope = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundExclusiveScope {
|
||||
labelExclusiveScopes = append(labelExclusiveScopes, "")
|
||||
}
|
||||
}
|
||||
|
||||
for _, l := range allLabels {
|
||||
l.LoadSelectedLabelsAfterClick(labelIDs, labelExclusiveScopes)
|
||||
}
|
||||
ctx.Data["Labels"] = allLabels
|
||||
ctx.Data["SelectLabels"] = selectLabels
|
||||
return labelIDs
|
||||
}
|
@ -8,7 +8,9 @@ import (
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
)
|
||||
|
||||
func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User {
|
||||
@ -31,17 +33,20 @@ func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User {
|
||||
// Before, the "issue filter" passes user ID to query the list, but in many cases, it's impossible to pre-fetch the full user list.
|
||||
// So it's better to make it work like GitHub: users could input username directly.
|
||||
// Since it only converts the username to ID directly and is only used internally (to search issues), so no permission check is needed.
|
||||
// Old usage: poster=123, new usage: poster=the-username (at the moment, non-existing username is treated as poster=0, not ideal but acceptable)
|
||||
func GetFilterUserIDByName(ctx context.Context, name string) int64 {
|
||||
// Return values:
|
||||
// * nil: no filter
|
||||
// * some(id): match the id, the id could be -1 to match the issues without assignee
|
||||
// * some(NonExistingID): match no issue (due to the user doesn't exist)
|
||||
func GetFilterUserIDByName(ctx context.Context, name string) optional.Option[int64] {
|
||||
if name == "" {
|
||||
return 0
|
||||
return optional.None[int64]()
|
||||
}
|
||||
u, err := user.GetUserByName(ctx, name)
|
||||
if err != nil {
|
||||
if id, err := strconv.ParseInt(name, 10, 64); err == nil {
|
||||
return id
|
||||
return optional.Some(id)
|
||||
}
|
||||
return 0
|
||||
return optional.Some(db.NonExistingID)
|
||||
}
|
||||
return u.ID
|
||||
return optional.Some(u.ID)
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/web/feed"
|
||||
"code.gitea.io/gitea/routers/web/shared/issue"
|
||||
"code.gitea.io/gitea/routers/web/shared/user"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
feed_service "code.gitea.io/gitea/services/feed"
|
||||
@ -413,6 +414,13 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
||||
viewType = "your_repositories"
|
||||
}
|
||||
|
||||
isPullList := unitType == unit.TypePullRequests
|
||||
opts := &issues_model.IssuesOptions{
|
||||
IsPull: optional.Some(isPullList),
|
||||
SortType: sortType,
|
||||
IsArchived: optional.Some(false),
|
||||
User: ctx.Doer,
|
||||
}
|
||||
// --------------------------------------------------------------------------
|
||||
// Build opts (IssuesOptions), which contains filter information.
|
||||
// Will eventually be used to retrieve issues relevant for the overview page.
|
||||
@ -422,30 +430,24 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
// Get repository IDs where User/Org/Team has access.
|
||||
var team *organization.Team
|
||||
var org *organization.Organization
|
||||
if ctx.Org != nil {
|
||||
org = ctx.Org.Organization
|
||||
team = ctx.Org.Team
|
||||
}
|
||||
if ctx.Org != nil && ctx.Org.Organization != nil {
|
||||
opts.Org = ctx.Org.Organization
|
||||
opts.Team = ctx.Org.Team
|
||||
|
||||
isPullList := unitType == unit.TypePullRequests
|
||||
opts := &issues_model.IssuesOptions{
|
||||
IsPull: optional.Some(isPullList),
|
||||
SortType: sortType,
|
||||
IsArchived: optional.Some(false),
|
||||
Org: org,
|
||||
Team: team,
|
||||
User: ctx.Doer,
|
||||
issue.PrepareFilterIssueLabels(ctx, 0, ctx.Org.Organization.AsUser())
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
}
|
||||
// Get filter by author id & assignee id
|
||||
// FIXME: this feature doesn't work at the moment, because frontend can't use a "user-remote-search" dropdown directly
|
||||
// the existing "/posters" handlers doesn't work for this case, it is unable to list the related users correctly.
|
||||
// In the future, we need something like github: "author:user1" to accept usernames directly.
|
||||
posterUsername := ctx.FormString("poster")
|
||||
ctx.Data["FilterPosterUsername"] = posterUsername
|
||||
opts.PosterID = user.GetFilterUserIDByName(ctx, posterUsername)
|
||||
// TODO: "assignee" should also use GetFilterUserIDByName in the future to support usernames directly
|
||||
opts.AssigneeID, _ = strconv.ParseInt(ctx.FormString("assignee"), 10, 64)
|
||||
assigneeUsername := ctx.FormString("assignee")
|
||||
ctx.Data["FilterAssigneeUsername"] = assigneeUsername
|
||||
opts.AssigneeID = user.GetFilterUserIDByName(ctx, assigneeUsername)
|
||||
|
||||
isFuzzy := ctx.FormBool("fuzzy")
|
||||
|
||||
@ -471,8 +473,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
||||
UnitType: unitType,
|
||||
Archived: optional.Some(false),
|
||||
}
|
||||
if team != nil {
|
||||
repoOpts.TeamID = team.ID
|
||||
if opts.Team != nil {
|
||||
repoOpts.TeamID = opts.Team.ID
|
||||
}
|
||||
accessibleRepos := container.Set[int64]{}
|
||||
{
|
||||
@ -500,9 +502,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
||||
case issues_model.FilterModeAll:
|
||||
case issues_model.FilterModeYourRepositories:
|
||||
case issues_model.FilterModeAssign:
|
||||
opts.AssigneeID = ctx.Doer.ID
|
||||
opts.AssigneeID = optional.Some(ctx.Doer.ID)
|
||||
case issues_model.FilterModeCreate:
|
||||
opts.PosterID = ctx.Doer.ID
|
||||
opts.PosterID = optional.Some(ctx.Doer.ID)
|
||||
case issues_model.FilterModeMention:
|
||||
opts.MentionedID = ctx.Doer.ID
|
||||
case issues_model.FilterModeReviewRequested:
|
||||
@ -584,10 +586,6 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
||||
// because the doer may create issues or be mentioned in any public repo.
|
||||
// So we need search issues in all public repos.
|
||||
o.AllPublic = ctx.Doer.ID == ctxUser.ID
|
||||
// TODO: to make it work with poster/assignee filter, then these IDs should be kept
|
||||
o.AssigneeID = nil
|
||||
o.PosterID = nil
|
||||
|
||||
o.MentionID = nil
|
||||
o.ReviewRequestedID = nil
|
||||
o.ReviewedID = nil
|
||||
@ -645,10 +643,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
||||
ctx.Data["ViewType"] = viewType
|
||||
ctx.Data["SortType"] = sortType
|
||||
ctx.Data["IsShowClosed"] = isShowClosed
|
||||
ctx.Data["SelectLabels"] = selectedLabels
|
||||
ctx.Data["IsFuzzy"] = isFuzzy
|
||||
ctx.Data["SearchFilterPosterID"] = util.Iif[any](opts.PosterID != 0, opts.PosterID, nil)
|
||||
ctx.Data["SearchFilterAssigneeID"] = util.Iif[any](opts.AssigneeID != 0, opts.AssigneeID, nil)
|
||||
|
||||
if isShowClosed {
|
||||
ctx.Data["State"] = "closed"
|
||||
@ -657,16 +652,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
||||
}
|
||||
|
||||
pager := context.NewPagination(shownIssues, setting.UI.IssuePagingNum, page, 5)
|
||||
pager.AddParamString("q", keyword)
|
||||
pager.AddParamString("type", viewType)
|
||||
pager.AddParamString("sort", sortType)
|
||||
pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
|
||||
pager.AddParamString("labels", selectedLabels)
|
||||
pager.AddParamString("fuzzy", fmt.Sprint(isFuzzy))
|
||||
pager.AddParamString("poster", posterUsername)
|
||||
if opts.AssigneeID != 0 {
|
||||
pager.AddParamString("assignee", fmt.Sprint(opts.AssigneeID))
|
||||
}
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
ctx.HTML(http.StatusOK, tplIssues)
|
||||
|
@ -1208,7 +1208,7 @@ func registerRoutes(m *web.Router) {
|
||||
Post(web.Bind(forms.CreateIssueForm{}), repo.NewIssuePost)
|
||||
m.Get("/choose", context.RepoRef(), repo.NewIssueChooseTemplate)
|
||||
})
|
||||
m.Get("/search", repo.ListIssues)
|
||||
m.Get("/search", repo.SearchRepoIssuesJSON)
|
||||
}, context.RepoMustNotBeArchived(), reqRepoIssueReader)
|
||||
|
||||
// FIXME: should use different URLs but mostly same logic for comments of issue and pull request.
|
||||
|
@ -6,6 +6,7 @@ package context
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
@ -32,6 +33,18 @@ func (p *Pagination) AddParamString(key, value string) {
|
||||
p.urlParams = append(p.urlParams, urlParam)
|
||||
}
|
||||
|
||||
func (p *Pagination) AddParamFromRequest(req *http.Request) {
|
||||
for key, values := range req.URL.Query() {
|
||||
if key == "page" || len(values) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, value := range values {
|
||||
urlParam := fmt.Sprintf("%s=%v", key, url.QueryEscape(value))
|
||||
p.urlParams = append(p.urlParams, urlParam)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetParams returns the configured URL params
|
||||
func (p *Pagination) GetParams() template.URL {
|
||||
return template.URL(strings.Join(p.urlParams, "&"))
|
||||
|
@ -7,9 +7,11 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/cache"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
@ -259,3 +261,252 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u
|
||||
|
||||
return apiPullRequest
|
||||
}
|
||||
|
||||
func ToAPIPullRequests(ctx context.Context, baseRepo *repo_model.Repository, prs issues_model.PullRequestList, doer *user_model.User) ([]*api.PullRequest, error) {
|
||||
for _, pr := range prs {
|
||||
pr.BaseRepo = baseRepo
|
||||
if pr.BaseRepoID == pr.HeadRepoID {
|
||||
pr.HeadRepo = baseRepo
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: load head repositories
|
||||
if err := prs.LoadRepositories(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
issueList, err := prs.LoadIssues(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := issueList.LoadLabels(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := issueList.LoadPosters(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := issueList.LoadAttachments(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := issueList.LoadMilestones(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := issueList.LoadAssignees(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reviews, err := prs.LoadReviews(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = reviews.LoadReviewers(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reviewersMap := make(map[int64][]*user_model.User)
|
||||
for _, review := range reviews {
|
||||
if review.Reviewer != nil {
|
||||
reviewersMap[review.IssueID] = append(reviewersMap[review.IssueID], review.Reviewer)
|
||||
}
|
||||
}
|
||||
|
||||
reviewCounts, err := prs.LoadReviewCommentsCounts(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, baseRepo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, baseRepo, doer)
|
||||
if err != nil {
|
||||
log.Error("GetUserRepoPermission[%d]: %v", baseRepo.ID, err)
|
||||
baseRepoPerm.AccessMode = perm.AccessModeNone
|
||||
}
|
||||
|
||||
apiRepo := ToRepo(ctx, baseRepo, baseRepoPerm)
|
||||
baseBranchCache := make(map[string]*git_model.Branch)
|
||||
apiPullRequests := make([]*api.PullRequest, 0, len(prs))
|
||||
for _, pr := range prs {
|
||||
apiIssue := ToAPIIssue(ctx, doer, pr.Issue)
|
||||
|
||||
apiPullRequest := &api.PullRequest{
|
||||
ID: pr.ID,
|
||||
URL: pr.Issue.HTMLURL(),
|
||||
Index: pr.Index,
|
||||
Poster: apiIssue.Poster,
|
||||
Title: apiIssue.Title,
|
||||
Body: apiIssue.Body,
|
||||
Labels: apiIssue.Labels,
|
||||
Milestone: apiIssue.Milestone,
|
||||
Assignee: apiIssue.Assignee,
|
||||
Assignees: apiIssue.Assignees,
|
||||
State: apiIssue.State,
|
||||
Draft: pr.IsWorkInProgress(ctx),
|
||||
IsLocked: apiIssue.IsLocked,
|
||||
Comments: apiIssue.Comments,
|
||||
ReviewComments: reviewCounts[pr.IssueID],
|
||||
HTMLURL: pr.Issue.HTMLURL(),
|
||||
DiffURL: pr.Issue.DiffURL(),
|
||||
PatchURL: pr.Issue.PatchURL(),
|
||||
HasMerged: pr.HasMerged,
|
||||
MergeBase: pr.MergeBase,
|
||||
Mergeable: pr.Mergeable(ctx),
|
||||
Deadline: apiIssue.Deadline,
|
||||
Created: pr.Issue.CreatedUnix.AsTimePtr(),
|
||||
Updated: pr.Issue.UpdatedUnix.AsTimePtr(),
|
||||
PinOrder: apiIssue.PinOrder,
|
||||
|
||||
AllowMaintainerEdit: pr.AllowMaintainerEdit,
|
||||
|
||||
Base: &api.PRBranchInfo{
|
||||
Name: pr.BaseBranch,
|
||||
Ref: pr.BaseBranch,
|
||||
RepoID: pr.BaseRepoID,
|
||||
Repository: apiRepo,
|
||||
},
|
||||
Head: &api.PRBranchInfo{
|
||||
Name: pr.HeadBranch,
|
||||
Ref: fmt.Sprintf("%s%d/head", git.PullPrefix, pr.Index),
|
||||
RepoID: -1,
|
||||
},
|
||||
}
|
||||
|
||||
pr.RequestedReviewers = reviewersMap[pr.IssueID]
|
||||
for _, reviewer := range pr.RequestedReviewers {
|
||||
apiPullRequest.RequestedReviewers = append(apiPullRequest.RequestedReviewers, ToUser(ctx, reviewer, nil))
|
||||
}
|
||||
|
||||
for _, reviewerTeam := range pr.RequestedReviewersTeams {
|
||||
convertedTeam, err := ToTeam(ctx, reviewerTeam, true)
|
||||
if err != nil {
|
||||
log.Error("LoadRequestedReviewersTeams[%d]: %v", pr.ID, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apiPullRequest.RequestedReviewersTeams = append(apiPullRequest.RequestedReviewersTeams, convertedTeam)
|
||||
}
|
||||
|
||||
if pr.Issue.ClosedUnix != 0 {
|
||||
apiPullRequest.Closed = pr.Issue.ClosedUnix.AsTimePtr()
|
||||
}
|
||||
|
||||
baseBranch, ok := baseBranchCache[pr.BaseBranch]
|
||||
if !ok {
|
||||
baseBranch, err = git_model.GetBranch(ctx, baseRepo.ID, pr.BaseBranch)
|
||||
if err == nil {
|
||||
baseBranchCache[pr.BaseBranch] = baseBranch
|
||||
} else if !git_model.IsErrBranchNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if baseBranch != nil {
|
||||
apiPullRequest.Base.Sha = baseBranch.CommitID
|
||||
}
|
||||
|
||||
if pr.Flow == issues_model.PullRequestFlowAGit {
|
||||
apiPullRequest.Head.Sha, err = gitRepo.GetRefCommitID(pr.GetGitRefName())
|
||||
if err != nil {
|
||||
log.Error("GetRefCommitID[%s]: %v", pr.GetGitRefName(), err)
|
||||
return nil, err
|
||||
}
|
||||
apiPullRequest.Head.RepoID = pr.BaseRepoID
|
||||
apiPullRequest.Head.Repository = apiPullRequest.Base.Repository
|
||||
apiPullRequest.Head.Name = ""
|
||||
}
|
||||
|
||||
var headGitRepo *git.Repository
|
||||
if pr.HeadRepo != nil && pr.Flow == issues_model.PullRequestFlowGithub {
|
||||
if pr.HeadRepoID == pr.BaseRepoID {
|
||||
apiPullRequest.Head.RepoID = pr.HeadRepo.ID
|
||||
apiPullRequest.Head.Repository = apiRepo
|
||||
headGitRepo = gitRepo
|
||||
} else {
|
||||
p, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, doer)
|
||||
if err != nil {
|
||||
log.Error("GetUserRepoPermission[%d]: %v", pr.HeadRepoID, err)
|
||||
p.AccessMode = perm.AccessModeNone
|
||||
}
|
||||
|
||||
apiPullRequest.Head.RepoID = pr.HeadRepo.ID
|
||||
apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p)
|
||||
|
||||
headGitRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo)
|
||||
if err != nil {
|
||||
log.Error("OpenRepository[%s]: %v", pr.HeadRepo.RepoPath(), err)
|
||||
return nil, err
|
||||
}
|
||||
defer headGitRepo.Close()
|
||||
}
|
||||
|
||||
headBranch, err := headGitRepo.GetBranch(pr.HeadBranch)
|
||||
if err != nil && !git.IsErrBranchNotExist(err) {
|
||||
log.Error("GetBranch[%s]: %v", pr.HeadBranch, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Outer scope variables to be used in diff calculation
|
||||
var (
|
||||
startCommitID string
|
||||
endCommitID string
|
||||
)
|
||||
|
||||
if git.IsErrBranchNotExist(err) {
|
||||
headCommitID, err := headGitRepo.GetRefCommitID(apiPullRequest.Head.Ref)
|
||||
if err != nil && !git.IsErrNotExist(err) {
|
||||
log.Error("GetCommit[%s]: %v", pr.HeadBranch, err)
|
||||
return nil, err
|
||||
}
|
||||
if err == nil {
|
||||
apiPullRequest.Head.Sha = headCommitID
|
||||
endCommitID = headCommitID
|
||||
}
|
||||
} else {
|
||||
commit, err := headBranch.GetCommit()
|
||||
if err != nil && !git.IsErrNotExist(err) {
|
||||
log.Error("GetCommit[%s]: %v", headBranch.Name, err)
|
||||
return nil, err
|
||||
}
|
||||
if err == nil {
|
||||
apiPullRequest.Head.Ref = pr.HeadBranch
|
||||
apiPullRequest.Head.Sha = commit.ID.String()
|
||||
endCommitID = commit.ID.String()
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate diff
|
||||
startCommitID = pr.MergeBase
|
||||
|
||||
apiPullRequest.ChangedFiles, apiPullRequest.Additions, apiPullRequest.Deletions, err = gitRepo.GetDiffShortStat(startCommitID, endCommitID)
|
||||
if err != nil {
|
||||
log.Error("GetDiffShortStat: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(apiPullRequest.Head.Sha) == 0 && len(apiPullRequest.Head.Ref) != 0 {
|
||||
refs, err := gitRepo.GetRefsFiltered(apiPullRequest.Head.Ref)
|
||||
if err != nil {
|
||||
log.Error("GetRefsFiltered[%s]: %v", apiPullRequest.Head.Ref, err)
|
||||
return nil, err
|
||||
} else if len(refs) == 0 {
|
||||
log.Error("unable to resolve PR head ref")
|
||||
} else {
|
||||
apiPullRequest.Head.Sha = refs[0].Object.String()
|
||||
}
|
||||
}
|
||||
|
||||
if pr.HasMerged {
|
||||
apiPullRequest.Merged = pr.MergedUnix.AsTimePtr()
|
||||
apiPullRequest.MergedCommitID = &pr.MergedCommitID
|
||||
apiPullRequest.MergedBy = ToUser(ctx, pr.Merger, nil)
|
||||
}
|
||||
|
||||
apiPullRequests = append(apiPullRequests, apiPullRequest)
|
||||
}
|
||||
|
||||
return apiPullRequests, nil
|
||||
}
|
||||
|
@ -24,16 +24,19 @@
|
||||
<input type="hidden" name="state" value="{{$.State}}">
|
||||
{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.project_kind")}}
|
||||
</form>
|
||||
<!-- Sort -->
|
||||
<div class="list-header-sort ui small dropdown type jump item">
|
||||
<span class="text">
|
||||
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
|
||||
</span>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="menu">
|
||||
<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="?q={{$.Keyword}}&sort=oldest&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
|
||||
<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="?q={{$.Keyword}}&sort=recentupdate&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
|
||||
<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="?q={{$.Keyword}}&sort=leastupdate&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
|
||||
|
||||
<div class="list-header-filters">
|
||||
<!-- Sort -->
|
||||
<div class="item ui small dropdown jump">
|
||||
<span class="text">
|
||||
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
|
||||
</span>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="menu">
|
||||
<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="?q={{$.Keyword}}&sort=oldest&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
|
||||
<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="?q={{$.Keyword}}&sort=recentupdate&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
|
||||
<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="?q={{$.Keyword}}&sort=leastupdate&state={{$.State}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -19,7 +19,7 @@
|
||||
<span data-tooltip-content={{ctx.Locale.Tr "repo.issues.label_archive_tooltip"}}>{{svg "octicon-info"}}</span>
|
||||
</label>
|
||||
{{end}}
|
||||
<span class="info">{{ctx.Locale.Tr "repo.issues.filter_label_exclude"}}</span>
|
||||
<span class="label-filter-exclude-info">{{ctx.Locale.Tr "repo.issues.filter_label_exclude"}}</span>
|
||||
<div class="divider"></div>
|
||||
<a class="item label-filter-query-default" href="{{QueryBuild $queryLink "labels" NIL}}">{{ctx.Locale.Tr "repo.issues.filter_label_no_select"}}</a>
|
||||
<a class="item label-filter-query-not-set" href="{{QueryBuild $queryLink "labels" 0}}">{{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}}</a>
|
||||
|
@ -2,13 +2,13 @@
|
||||
* QueryParamKey: eg: "poster", "assignee"
|
||||
* QueryLink
|
||||
* UserSearchUrl
|
||||
* SelectedUserId
|
||||
* SelectedUsername
|
||||
* TextFilterTitle
|
||||
*/}}
|
||||
{{$queryLink := .QueryLink}}
|
||||
<div class="item ui dropdown custom user-remote-search" data-tooltip-content="{{ctx.Locale.Tr "repo.user_search_tooltip"}}"
|
||||
data-search-url="{{$.UserSearchUrl}}"
|
||||
data-selected-user-id="{{$.SelectedUserId}}"
|
||||
data-selected-username="{{$.SelectedUsername}}"
|
||||
data-action-jump-url="{{QueryBuild $queryLink $.QueryParamKey NIL}}&{{$.QueryParamKey}}={username}"
|
||||
>
|
||||
{{$.TextFilterTitle}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
{{if not .Milestone}}
|
||||
<!-- Milestone -->
|
||||
<div class="ui {{if not (or .OpenMilestones .ClosedMilestones)}}disabled{{end}} dropdown jump item">
|
||||
<div class="item ui dropdown jump {{if not (or .OpenMilestones .ClosedMilestones)}}disabled{{end}}">
|
||||
<span class="text">
|
||||
{{ctx.Locale.Tr "repo.issues.filter_milestone"}}
|
||||
</span>
|
||||
@ -42,7 +42,7 @@
|
||||
{{end}}
|
||||
|
||||
<!-- Project -->
|
||||
<div class="ui{{if not (or .OpenProjects .ClosedProjects)}} disabled{{end}} dropdown jump item">
|
||||
<div class="item ui dropdown jump {{if not (or .OpenProjects .ClosedProjects)}}disabled{{end}}">
|
||||
<span class="text">
|
||||
{{ctx.Locale.Tr "repo.issues.filter_project"}}
|
||||
</span>
|
||||
@ -84,7 +84,7 @@
|
||||
"QueryParamKey" "poster"
|
||||
"QueryLink" $queryLink
|
||||
"UserSearchUrl" (Iif .Milestone (print $.RepoLink "/issues/posters") (print $.Link "/posters"))
|
||||
"SelectedUserId" $.PosterUserID
|
||||
"SelectedUsername" $.PosterUsername
|
||||
"TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_poster")
|
||||
}}
|
||||
|
||||
@ -100,7 +100,7 @@
|
||||
|
||||
{{if .IsSigned}}
|
||||
<!-- Type -->
|
||||
<div class="ui dropdown type jump item">
|
||||
<div class="item ui dropdown jump">
|
||||
<span class="text">
|
||||
{{ctx.Locale.Tr "repo.issues.filter_type"}}
|
||||
</span>
|
||||
@ -119,7 +119,7 @@
|
||||
{{end}}
|
||||
|
||||
<!-- Sort -->
|
||||
<div class="list-header-sort ui small dropdown downward type jump item">
|
||||
<div class="item ui dropdown jump">
|
||||
<span class="text">
|
||||
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
|
||||
</span>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!-- Sort -->
|
||||
<div class="list-header-sort ui small dropdown type jump item">
|
||||
<div class="item ui small dropdown jump">
|
||||
<span class="text">
|
||||
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
|
||||
</span>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<input type="hidden" name="state" value="{{$.State}}">
|
||||
{{if not .PageIsMilestones}}
|
||||
<input type="hidden" name="type" value="{{$.ViewType}}">
|
||||
<input type="hidden" name="labels" value="{{.SelectLabels}}">
|
||||
<input type="hidden" name="labels" value="{{$.SelectLabels}}">
|
||||
<input type="hidden" name="milestone" value="{{$.MilestoneID}}">
|
||||
<input type="hidden" name="project" value="{{$.ProjectID}}">
|
||||
<input type="hidden" name="assignee" value="{{$.AssigneeID}}">
|
||||
|
@ -34,26 +34,26 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui dividing header">
|
||||
<div class="ui stackable grid">
|
||||
<div class="eight wide column">
|
||||
<div class="flex-text-block tw-flex-wrap tw-justify-end">
|
||||
<div class="flex-text-block tw-flex-1 tw-min-w-[300px]">
|
||||
<a class="file-revisions-btn ui basic button" title="{{ctx.Locale.Tr "repo.wiki.file_revision"}}" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_revision" ><span>{{.CommitCount}}</span> {{svg "octicon-history"}}</a>
|
||||
{{$title}}
|
||||
<div class="ui sub header">
|
||||
{{$timeSince := DateUtils.TimeSince .Author.When}}
|
||||
{{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince}}
|
||||
<div class="tw-flex-1 gt-ellipsis">
|
||||
{{$title}}
|
||||
<div class="ui sub header gt-ellipsis">
|
||||
{{$timeSince := DateUtils.TimeSince .Author.When}}
|
||||
{{ctx.Locale.Tr "repo.wiki.last_commit_info" .Author.Name $timeSince}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="eight wide right aligned column">
|
||||
<div class="flex-text-block tw-flex-wrap tw-justify-end">
|
||||
{{if .EscapeStatus.Escaped}}
|
||||
<a class="ui small button unescape-button tw-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</a>
|
||||
<a class="ui small button escape-button">{{ctx.Locale.Tr "repo.escape_control_characters"}}</a>
|
||||
<a class="ui small button unescape-button tw-m-0 tw-hidden">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</a>
|
||||
<a class="ui small button escape-button tw-m-0">{{ctx.Locale.Tr "repo.escape_control_characters"}}</a>
|
||||
{{end}}
|
||||
{{if and .CanWriteWiki (not .Repository.IsMirror)}}
|
||||
<div class="ui right">
|
||||
<a class="ui small button" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_edit">{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}</a>
|
||||
<a class="ui small primary button" href="{{.RepoLink}}/wiki?action=_new">{{ctx.Locale.Tr "repo.wiki.new_page_button"}}</a>
|
||||
<a class="ui small red button delete-button" href="" data-url="{{.RepoLink}}/wiki/{{.PageURL}}?action=_delete" data-id="{{.PageURL}}">{{ctx.Locale.Tr "repo.wiki.delete_page_button"}}</a>
|
||||
</div>
|
||||
<a class="ui small button" href="{{.RepoLink}}/wiki/{{.PageURL}}?action=_edit">{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}</a>
|
||||
<a class="ui small primary button" href="{{.RepoLink}}/wiki?action=_new">{{ctx.Locale.Tr "repo.wiki.new_page_button"}}</a>
|
||||
<a class="ui small red button tw-m-0 delete-button" href="" data-url="{{.RepoLink}}/wiki/{{.PageURL}}?action=_delete" data-id="{{.PageURL}}">{{ctx.Locale.Tr "repo.wiki.delete_page_button"}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
<div class="flex-container">
|
||||
{{$queryLink := QueryBuild "?" "type" $.ViewType "sort" $.SortType "state" $.State "q" $.Keyword "fuzzy" $.IsFuzzy}}
|
||||
{{$queryLink := QueryBuild "?" "type" $.ViewType "sort" $.SortType "state" $.State "q" $.Keyword "labels" .SelectLabels "fuzzy" $.IsFuzzy}}
|
||||
<div class="flex-container-nav">
|
||||
<div class="ui secondary vertical filter menu tw-bg-transparent">
|
||||
<a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "your_repositories"}}">
|
||||
@ -36,7 +36,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{$queryLinkWithFilter := QueryBuild $queryLink "poster" $.SearchFilterPosterUsername "assignee" $.SearchFilterAssigneeID}}
|
||||
{{$queryLinkWithFilter := QueryBuild $queryLink "poster" $.FilterPosterUsername "assignee" $.FilterAssigneeUsername}}
|
||||
<div class="flex-container-main content">
|
||||
<div class="list-header">
|
||||
<div class="small-menu-items ui compact tiny menu list-header-toggle flex-items-block">
|
||||
@ -50,28 +50,51 @@
|
||||
</a>
|
||||
</div>
|
||||
<form class="list-header-search ui form ignore-dirty">
|
||||
<div class="ui small search fluid action input">
|
||||
<input type="hidden" name="type" value="{{$.ViewType}}">
|
||||
<input type="hidden" name="sort" value="{{$.SortType}}">
|
||||
<input type="hidden" name="state" value="{{$.State}}">
|
||||
{{template "shared/search/combo_fuzzy" dict "Value" $.Keyword "IsFuzzy" $.IsFuzzy "Placeholder" (ctx.Locale.Tr (Iif .PageIsPulls "search.pull_kind" "search.issue_kind")) "Tooltip" (ctx.Locale.Tr "explore.go_to")}}
|
||||
</div>
|
||||
<input type="hidden" name="type" value="{{$.ViewType}}">
|
||||
<input type="hidden" name="sort" value="{{$.SortType}}">
|
||||
<input type="hidden" name="state" value="{{$.State}}">
|
||||
{{template "shared/search/combo_fuzzy" dict "Value" $.Keyword "IsFuzzy" $.IsFuzzy "Placeholder" (ctx.Locale.Tr (Iif .PageIsPulls "search.pull_kind" "search.issue_kind")) "Tooltip" (ctx.Locale.Tr "explore.go_to")}}
|
||||
</form>
|
||||
<!-- Sort -->
|
||||
<div class="list-header-sort ui small dropdown type jump item">
|
||||
<span class="text tw-whitespace-nowrap">
|
||||
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
</span>
|
||||
<div class="menu">
|
||||
<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "recentupdate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
|
||||
<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "leastupdate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
|
||||
<a class="{{if eq .SortType "latest"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "latest"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
|
||||
<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "oldest"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
|
||||
<a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "mostcomment"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a>
|
||||
<a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "leastcomment"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a>
|
||||
<a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "nearduedate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a>
|
||||
<a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "farduedate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a>
|
||||
|
||||
<div class="list-header-filters">
|
||||
{{if $.Labels}}
|
||||
{{template "repo/issue/filter_item_label" dict "Labels" .Labels "QueryLink" $queryLinkWithFilter "SupportArchivedLabel" true}}
|
||||
{{end}}
|
||||
|
||||
{{if ne $.ViewType "created_by"}}
|
||||
{{template "repo/issue/filter_item_user_fetch" dict
|
||||
"QueryParamKey" "poster"
|
||||
"QueryLink" $queryLinkWithFilter
|
||||
"SelectedUsername" $.FilterPosterUsername
|
||||
"TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_poster")
|
||||
}}
|
||||
{{end}}
|
||||
|
||||
{{if ne $.ViewType "assigned"}}
|
||||
{{template "repo/issue/filter_item_user_fetch" dict
|
||||
"QueryParamKey" "assignee"
|
||||
"QueryLink" $queryLinkWithFilter
|
||||
"SelectedUsername" $.FilterAssigneeUsername
|
||||
"TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_assignee")
|
||||
}}
|
||||
{{end}}
|
||||
|
||||
<!-- Sort -->
|
||||
<div class="item ui small dropdown jump">
|
||||
<span class="text tw-whitespace-nowrap">
|
||||
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
</span>
|
||||
<div class="menu">
|
||||
<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "recentupdate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
|
||||
<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "leastupdate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
|
||||
<a class="{{if eq .SortType "latest"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "latest"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</a>
|
||||
<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "oldest"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</a>
|
||||
<a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "mostcomment"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.mostcomment"}}</a>
|
||||
<a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "leastcomment"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.leastcomment"}}</a>
|
||||
<a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "nearduedate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.nearduedate"}}</a>
|
||||
<a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="{{QueryBuild $queryLinkWithFilter "sort" "farduedate"}}">{{ctx.Locale.Tr "repo.issues.filter_sort.farduedate"}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -52,20 +52,22 @@
|
||||
<input type="hidden" name="state" value="{{$.State}}">
|
||||
{{template "shared/search/combo" dict "Value" $.Keyword}}
|
||||
</form>
|
||||
<!-- Sort -->
|
||||
<div class="list-header-sort ui dropdown type jump item">
|
||||
<span class="text">
|
||||
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
|
||||
</span>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="menu">
|
||||
<a class="{{if or (eq .SortType "closestduedate") (not .SortType)}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=closestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.earliest_due_data"}}</a>
|
||||
<a class="{{if eq .SortType "furthestduedate"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=furthestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.latest_due_date"}}</a>
|
||||
<a class="{{if eq .SortType "leastcomplete"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_complete"}}</a>
|
||||
<a class="{{if eq .SortType "mostcomplete"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_complete"}}</a>
|
||||
<a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a>
|
||||
<a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a>
|
||||
<a class="{{if eq .SortType "name"}}active {{end}}item" href="{{$.Link}}?sort=name&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.name"}}</a>
|
||||
<div class="list-header-filters">
|
||||
<!-- Sort -->
|
||||
<div class="item ui dropdown jump">
|
||||
<span class="text">
|
||||
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
|
||||
</span>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="menu">
|
||||
<a class="{{if or (eq .SortType "closestduedate") (not .SortType)}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=closestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.earliest_due_data"}}</a>
|
||||
<a class="{{if eq .SortType "furthestduedate"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=furthestduedate&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.latest_due_date"}}</a>
|
||||
<a class="{{if eq .SortType "leastcomplete"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_complete"}}</a>
|
||||
<a class="{{if eq .SortType "mostcomplete"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostcomplete&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_complete"}}</a>
|
||||
<a class="{{if eq .SortType "mostissues"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=mostissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.most_issues"}}</a>
|
||||
<a class="{{if eq .SortType "leastissues"}}active {{end}}item" href="?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort=leastissues&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.least_issues"}}</a>
|
||||
<a class="{{if eq .SortType "name"}}active {{end}}item" href="{{$.Link}}?sort=name&state={{$.State}}&q={{$.Keyword}}">{{ctx.Locale.Tr "repo.milestones.filter_sort.name"}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1 +1 @@
|
||||
5f22f7d0d95d614d25a5b68592adb345a4b5c7fd
|
||||
985f0301dba5e7b34be866819cd15ad3d8f508ee
|
||||
|
@ -33,8 +33,8 @@ func TestAPIPullCommits(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, "5f22f7d0d95d614d25a5b68592adb345a4b5c7fd", commits[0].SHA)
|
||||
assert.Equal(t, "4a357436d925b5c974181ff12a994538ddc5a269", commits[1].SHA)
|
||||
assert.Equal(t, "985f0301dba5e7b34be866819cd15ad3d8f508ee", commits[0].SHA)
|
||||
assert.Equal(t, "5c050d3b6d2db231ab1f64e324f1b6b9a0b181c2", commits[1].SHA)
|
||||
|
||||
assert.NotEmpty(t, commits[0].Files)
|
||||
assert.NotEmpty(t, commits[1].Files)
|
||||
|
@ -4,6 +4,8 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@ -19,6 +21,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"code.gitea.io/gitea/services/gitdiff"
|
||||
issue_service "code.gitea.io/gitea/services/issue"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
@ -41,25 +44,99 @@ func TestAPIViewPulls(t *testing.T) {
|
||||
expectedLen := unittest.GetCount(t, &issues_model.Issue{RepoID: repo.ID}, unittest.Cond("is_pull = ?", true))
|
||||
assert.Len(t, pulls, expectedLen)
|
||||
|
||||
assert.Len(t, pulls, 3)
|
||||
pull := pulls[0]
|
||||
assert.EqualValues(t, 1, pull.Poster.ID)
|
||||
assert.Len(t, pull.RequestedReviewers, 2)
|
||||
assert.Len(t, pull.RequestedReviewersTeams, 0)
|
||||
assert.EqualValues(t, 5, pull.RequestedReviewers[0].ID)
|
||||
assert.EqualValues(t, 6, pull.RequestedReviewers[1].ID)
|
||||
assert.EqualValues(t, 1, pull.ChangedFiles)
|
||||
|
||||
if assert.EqualValues(t, 5, pull.ID) {
|
||||
resp = ctx.Session.MakeRequest(t, NewRequest(t, "GET", pull.DiffURL), http.StatusOK)
|
||||
_, err := io.ReadAll(resp.Body)
|
||||
bs, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
// TODO: use diff to generate stats to test against
|
||||
patch, err := gitdiff.ParsePatch(context.Background(), 1000, 5000, 10, bytes.NewReader(bs), "")
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, patch.Files, pull.ChangedFiles) {
|
||||
assert.Equal(t, "File-WoW", patch.Files[0].Name)
|
||||
// FIXME: The old name should be empty if it's a file add type
|
||||
assert.Equal(t, "File-WoW", patch.Files[0].OldName)
|
||||
assert.EqualValues(t, pull.Additions, patch.Files[0].Addition)
|
||||
assert.EqualValues(t, pull.Deletions, patch.Files[0].Deletion)
|
||||
assert.Equal(t, gitdiff.DiffFileAdd, patch.Files[0].Type)
|
||||
}
|
||||
|
||||
t.Run(fmt.Sprintf("APIGetPullFiles_%d", pull.ID),
|
||||
doAPIGetPullFiles(ctx, pull, func(t *testing.T, files []*api.ChangedFile) {
|
||||
if assert.Len(t, files, 1) {
|
||||
if assert.Len(t, files, pull.ChangedFiles) {
|
||||
assert.Equal(t, "File-WoW", files[0].Filename)
|
||||
assert.Empty(t, files[0].PreviousFilename)
|
||||
assert.EqualValues(t, 1, files[0].Additions)
|
||||
assert.EqualValues(t, 1, files[0].Changes)
|
||||
assert.EqualValues(t, 0, files[0].Deletions)
|
||||
assert.EqualValues(t, pull.Additions, files[0].Additions)
|
||||
assert.EqualValues(t, pull.Deletions, files[0].Deletions)
|
||||
assert.Equal(t, "added", files[0].Status)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pull = pulls[1]
|
||||
assert.EqualValues(t, 1, pull.Poster.ID)
|
||||
assert.Len(t, pull.RequestedReviewers, 4)
|
||||
assert.Len(t, pull.RequestedReviewersTeams, 0)
|
||||
assert.EqualValues(t, 3, pull.RequestedReviewers[0].ID)
|
||||
assert.EqualValues(t, 4, pull.RequestedReviewers[1].ID)
|
||||
assert.EqualValues(t, 2, pull.RequestedReviewers[2].ID)
|
||||
assert.EqualValues(t, 5, pull.RequestedReviewers[3].ID)
|
||||
assert.EqualValues(t, 1, pull.ChangedFiles)
|
||||
|
||||
if assert.EqualValues(t, 2, pull.ID) {
|
||||
resp = ctx.Session.MakeRequest(t, NewRequest(t, "GET", pull.DiffURL), http.StatusOK)
|
||||
bs, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
patch, err := gitdiff.ParsePatch(context.Background(), 1000, 5000, 10, bytes.NewReader(bs), "")
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, patch.Files, pull.ChangedFiles) {
|
||||
assert.Equal(t, "README.md", patch.Files[0].Name)
|
||||
assert.Equal(t, "README.md", patch.Files[0].OldName)
|
||||
assert.EqualValues(t, pull.Additions, patch.Files[0].Addition)
|
||||
assert.EqualValues(t, pull.Deletions, patch.Files[0].Deletion)
|
||||
assert.Equal(t, gitdiff.DiffFileChange, patch.Files[0].Type)
|
||||
}
|
||||
|
||||
t.Run(fmt.Sprintf("APIGetPullFiles_%d", pull.ID),
|
||||
doAPIGetPullFiles(ctx, pull, func(t *testing.T, files []*api.ChangedFile) {
|
||||
if assert.Len(t, files, pull.ChangedFiles) {
|
||||
assert.Equal(t, "README.md", files[0].Filename)
|
||||
// FIXME: The PreviousFilename name should be the same as Filename if it's a file change
|
||||
assert.Equal(t, "", files[0].PreviousFilename)
|
||||
assert.EqualValues(t, pull.Additions, files[0].Additions)
|
||||
assert.EqualValues(t, pull.Deletions, files[0].Deletions)
|
||||
assert.Equal(t, "changed", files[0].Status)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pull = pulls[2]
|
||||
assert.EqualValues(t, 1, pull.Poster.ID)
|
||||
assert.Len(t, pull.RequestedReviewers, 1)
|
||||
assert.Len(t, pull.RequestedReviewersTeams, 0)
|
||||
assert.EqualValues(t, 1, pull.RequestedReviewers[0].ID)
|
||||
assert.EqualValues(t, 0, pull.ChangedFiles)
|
||||
|
||||
if assert.EqualValues(t, 1, pull.ID) {
|
||||
resp = ctx.Session.MakeRequest(t, NewRequest(t, "GET", pull.DiffURL), http.StatusOK)
|
||||
bs, err := io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
patch, err := gitdiff.ParsePatch(context.Background(), 1000, 5000, 10, bytes.NewReader(bs), "")
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, pull.ChangedFiles, patch.NumFiles)
|
||||
|
||||
t.Run(fmt.Sprintf("APIGetPullFiles_%d", pull.ID),
|
||||
doAPIGetPullFiles(ctx, pull, func(t *testing.T, files []*api.ChangedFile) {
|
||||
assert.Len(t, files, pull.ChangedFiles)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIViewPullsByBaseHead(t *testing.T) {
|
||||
|
@ -26,8 +26,8 @@ func TestListPullCommits(t *testing.T) {
|
||||
DecodeJSON(t, resp, &pullCommitList)
|
||||
|
||||
if assert.Len(t, pullCommitList.Commits, 2) {
|
||||
assert.Equal(t, "5f22f7d0d95d614d25a5b68592adb345a4b5c7fd", pullCommitList.Commits[0].ID)
|
||||
assert.Equal(t, "4a357436d925b5c974181ff12a994538ddc5a269", pullCommitList.Commits[1].ID)
|
||||
assert.Equal(t, "985f0301dba5e7b34be866819cd15ad3d8f508ee", pullCommitList.Commits[0].ID)
|
||||
assert.Equal(t, "5c050d3b6d2db231ab1f64e324f1b6b9a0b181c2", pullCommitList.Commits[1].ID)
|
||||
}
|
||||
assert.Equal(t, "4a357436d925b5c974181ff12a994538ddc5a269", pullCommitList.LastReviewCommitSha)
|
||||
})
|
||||
|
@ -90,24 +90,6 @@
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.repository .filter.menu .ui.dropdown.label-filter .menu .info {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 0;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
margin-left: 10px;
|
||||
margin-right: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.repository .filter.menu .ui.dropdown.label-filter .menu .info code {
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1px 2px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* For the secondary pointing menu, respect its own border-bottom */
|
||||
/* style reference: https://semantic-ui.com/collections/menu.html#pointing */
|
||||
.repository .ui.tabs.container .ui.menu:not(.secondary.pointing) {
|
||||
|
@ -73,3 +73,21 @@
|
||||
font-size: 12px;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.label-filter-exclude-info {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 0;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
margin-left: 10px;
|
||||
margin-right: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.label-filter-exclude-info code {
|
||||
border: 1px solid var(--color-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1px 2px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
@ -5,13 +5,6 @@
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.list-header-sort {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.list-header-search {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
@ -21,8 +14,22 @@
|
||||
min-width: 200px; /* to enable flexbox wrapping on mobile */
|
||||
}
|
||||
|
||||
.list-header-search .input {
|
||||
.list-header-search > .ui.input {
|
||||
flex: 1;
|
||||
min-width: 100px !important;
|
||||
}
|
||||
|
||||
.list-header-search > .ui.input .ui.dropdown {
|
||||
min-width: auto !important;
|
||||
}
|
||||
|
||||
.list-header-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.list-header-filters > .item {
|
||||
padding: 5px 0 5px 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
@ -32,8 +39,7 @@
|
||||
.list-header-toggle {
|
||||
order: 1;
|
||||
}
|
||||
.list-header-sort {
|
||||
.list-header-filters {
|
||||
order: 2;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ function initRepoCreateBranchButton() {
|
||||
for (const el of document.querySelectorAll('.show-create-branch-modal')) {
|
||||
el.addEventListener('click', () => {
|
||||
const modalFormName = el.getAttribute('data-modal-form') || '#create-branch-form';
|
||||
const modalForm = document.querySelector(modalFormName);
|
||||
const modalForm = document.querySelector<HTMLFormElement>(modalFormName);
|
||||
if (!modalForm) return;
|
||||
modalForm.action = `${modalForm.getAttribute('data-base-action')}${el.getAttribute('data-branch-from-urlcomponent')}`;
|
||||
|
||||
@ -29,7 +29,7 @@ function initRepoRenameBranchButton() {
|
||||
const target = el.getAttribute('data-modal');
|
||||
const modal = document.querySelector(target);
|
||||
const oldBranchName = el.getAttribute('data-old-branch-name');
|
||||
modal.querySelector('input[name=from]').value = oldBranchName;
|
||||
modal.querySelector<HTMLInputElement>('input[name=from]').value = oldBranchName;
|
||||
|
||||
// display the warning that the branch which is chosen is the default branch
|
||||
const warn = modal.querySelector('.default-branch-warning');
|
||||
|
@ -8,7 +8,7 @@ import {toAbsoluteUrl} from '../utils.ts';
|
||||
export const singleAnchorRegex = /^#(L|n)([1-9][0-9]*)$/;
|
||||
export const rangeAnchorRegex = /^#(L[1-9][0-9]*)-(L[1-9][0-9]*)$/;
|
||||
|
||||
function changeHash(hash) {
|
||||
function changeHash(hash: string) {
|
||||
if (window.history.pushState) {
|
||||
window.history.pushState(null, null, hash);
|
||||
} else {
|
||||
@ -24,7 +24,7 @@ function getLineEls() {
|
||||
return document.querySelectorAll(`.code-view td.lines-code${isBlame() ? '.blame-code' : ''}`);
|
||||
}
|
||||
|
||||
function selectRange($linesEls, $selectionEndEl, $selectionStartEls) {
|
||||
function selectRange($linesEls, $selectionEndEl, $selectionStartEls?) {
|
||||
for (const el of $linesEls) {
|
||||
el.closest('tr').classList.remove('active');
|
||||
}
|
||||
@ -34,7 +34,7 @@ function selectRange($linesEls, $selectionEndEl, $selectionStartEls) {
|
||||
const copyPermalink = document.querySelector('a.copy-line-permalink');
|
||||
const viewGitBlame = document.querySelector('a.view_git_blame');
|
||||
|
||||
const updateIssueHref = function (anchor) {
|
||||
const updateIssueHref = function (anchor: string) {
|
||||
if (!refInNewIssue) return;
|
||||
const urlIssueNew = refInNewIssue.getAttribute('data-url-issue-new');
|
||||
const urlParamBodyLink = refInNewIssue.getAttribute('data-url-param-body-link');
|
||||
@ -42,7 +42,7 @@ function selectRange($linesEls, $selectionEndEl, $selectionStartEls) {
|
||||
refInNewIssue.setAttribute('href', `${urlIssueNew}?body=${encodeURIComponent(issueContent)}`);
|
||||
};
|
||||
|
||||
const updateViewGitBlameFragment = function (anchor) {
|
||||
const updateViewGitBlameFragment = function (anchor: string) {
|
||||
if (!viewGitBlame) return;
|
||||
let href = viewGitBlame.getAttribute('href');
|
||||
href = `${href.replace(/#L\d+$|#L\d+-L\d+$/, '')}`;
|
||||
@ -52,7 +52,7 @@ function selectRange($linesEls, $selectionEndEl, $selectionStartEls) {
|
||||
viewGitBlame.setAttribute('href', href);
|
||||
};
|
||||
|
||||
const updateCopyPermalinkUrl = function (anchor) {
|
||||
const updateCopyPermalinkUrl = function (anchor: string) {
|
||||
if (!copyPermalink) return;
|
||||
let link = copyPermalink.getAttribute('data-url');
|
||||
link = `${link.replace(/#L\d+$|#L\d+-L\d+$/, '')}#${anchor}`;
|
||||
@ -142,13 +142,7 @@ export function initRepoCodeView() {
|
||||
});
|
||||
}
|
||||
selectRange($(linesEls), $(selectedEls), from ? $(from) : null);
|
||||
|
||||
if (window.getSelection) {
|
||||
window.getSelection().removeAllRanges();
|
||||
} else {
|
||||
document.selection.empty();
|
||||
}
|
||||
|
||||
window.getSelection().removeAllRanges();
|
||||
showLineButton();
|
||||
});
|
||||
|
||||
|
@ -64,7 +64,7 @@ export function initRepoCloneLink() {
|
||||
});
|
||||
}
|
||||
|
||||
export function initRepoCommonBranchOrTagDropdown(selector) {
|
||||
export function initRepoCommonBranchOrTagDropdown(selector: string) {
|
||||
$(selector).each(function () {
|
||||
const $dropdown = $(this);
|
||||
$dropdown.find('.reference.column').on('click', function () {
|
||||
@ -75,7 +75,7 @@ export function initRepoCommonBranchOrTagDropdown(selector) {
|
||||
});
|
||||
}
|
||||
|
||||
export function initRepoCommonFilterSearchDropdown(selector) {
|
||||
export function initRepoCommonFilterSearchDropdown(selector: string) {
|
||||
const $dropdown = $(selector);
|
||||
if (!$dropdown.length) return;
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {hideElem, showElem, toggleElem} from '../utils/dom.ts';
|
||||
import {GET} from '../modules/fetch.ts';
|
||||
|
||||
async function loadBranchesAndTags(area, loadingButton) {
|
||||
async function loadBranchesAndTags(area: Element, loadingButton: Element) {
|
||||
loadingButton.classList.add('disabled');
|
||||
try {
|
||||
const res = await GET(loadingButton.getAttribute('data-fetch-url'));
|
||||
@ -15,7 +15,7 @@ async function loadBranchesAndTags(area, loadingButton) {
|
||||
}
|
||||
}
|
||||
|
||||
function addTags(area, tags) {
|
||||
function addTags(area: Element, tags: Array<Record<string, any>>) {
|
||||
const tagArea = area.querySelector('.tag-area');
|
||||
toggleElem(tagArea.parentElement, tags.length > 0);
|
||||
for (const tag of tags) {
|
||||
@ -23,7 +23,7 @@ function addTags(area, tags) {
|
||||
}
|
||||
}
|
||||
|
||||
function addBranches(area, branches, defaultBranch) {
|
||||
function addBranches(area: Element, branches: Array<Record<string, any>>, defaultBranch: string) {
|
||||
const defaultBranchTooltip = area.getAttribute('data-text-default-branch-tooltip');
|
||||
const branchArea = area.querySelector('.branch-area');
|
||||
toggleElem(branchArea.parentElement, branches.length > 0);
|
||||
@ -33,7 +33,7 @@ function addBranches(area, branches, defaultBranch) {
|
||||
}
|
||||
}
|
||||
|
||||
function addLink(parent, href, text, tooltip) {
|
||||
function addLink(parent: Element, href: string, text: string, tooltip?: string) {
|
||||
const link = document.createElement('a');
|
||||
link.classList.add('muted', 'tw-px-1');
|
||||
link.href = href;
|
||||
|
@ -22,7 +22,7 @@ export function initRepoGraphGit() {
|
||||
for (const link of document.querySelectorAll('.pagination a')) {
|
||||
const href = link.getAttribute('href');
|
||||
if (!href) continue;
|
||||
const url = new URL(href, window.location);
|
||||
const url = new URL(href, window.location.href);
|
||||
const params = url.searchParams;
|
||||
params.set('mode', 'monochrome');
|
||||
url.search = `?${params.toString()}`;
|
||||
@ -38,7 +38,7 @@ export function initRepoGraphGit() {
|
||||
for (const link of document.querySelectorAll('.pagination a')) {
|
||||
const href = link.getAttribute('href');
|
||||
if (!href) continue;
|
||||
const url = new URL(href, window.location);
|
||||
const url = new URL(href, window.location.href);
|
||||
const params = url.searchParams;
|
||||
params.delete('mode');
|
||||
url.search = `?${params.toString()}`;
|
||||
@ -53,7 +53,7 @@ export function initRepoGraphGit() {
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
}
|
||||
});
|
||||
const url = new URL(window.location);
|
||||
const url = new URL(window.location.href);
|
||||
const params = url.searchParams;
|
||||
const updateGraph = () => {
|
||||
const queryString = params.toString();
|
||||
@ -103,7 +103,7 @@ export function initRepoGraphGit() {
|
||||
},
|
||||
onAdd(toAdd) {
|
||||
if (toAdd === '...flow-hide-pr-refs') {
|
||||
params.set('hide-pr-refs', true);
|
||||
params.set('hide-pr-refs', 'true');
|
||||
} else {
|
||||
params.append('branch', toAdd);
|
||||
}
|
||||
@ -111,7 +111,7 @@ export function initRepoGraphGit() {
|
||||
},
|
||||
});
|
||||
|
||||
graphContainer.addEventListener('mouseenter', (e) => {
|
||||
graphContainer.addEventListener('mouseenter', (e: MouseEvent & {target: HTMLElement}) => {
|
||||
if (e.target.matches('#rev-list li')) {
|
||||
const flow = e.target.getAttribute('data-flow');
|
||||
if (flow === '0') return;
|
||||
@ -132,7 +132,7 @@ export function initRepoGraphGit() {
|
||||
}
|
||||
});
|
||||
|
||||
graphContainer.addEventListener('mouseleave', (e) => {
|
||||
graphContainer.addEventListener('mouseleave', (e: MouseEvent & {target: HTMLElement}) => {
|
||||
if (e.target.matches('#rev-list li')) {
|
||||
const flow = e.target.getAttribute('data-flow');
|
||||
if (flow === '0') return;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {stripTags} from '../utils.ts';
|
||||
import {hideElem, queryElemChildren, showElem} from '../utils/dom.ts';
|
||||
import {POST} from '../modules/fetch.ts';
|
||||
import {showErrorToast} from '../modules/toast.ts';
|
||||
import {showErrorToast, type Toast} from '../modules/toast.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
|
||||
const {appSubUrl} = window.config;
|
||||
@ -13,7 +13,7 @@ export function initRepoTopicBar() {
|
||||
const editDiv = document.querySelector('#topic_edit');
|
||||
const viewDiv = document.querySelector('#repo-topics');
|
||||
const topicDropdown = editDiv.querySelector('.ui.dropdown');
|
||||
let lastErrorToast;
|
||||
let lastErrorToast: Toast;
|
||||
|
||||
mgrBtn.addEventListener('click', () => {
|
||||
hideElem(viewDiv);
|
||||
|
@ -95,10 +95,9 @@ function initRepoIssueListCheckboxes() {
|
||||
function initDropdownUserRemoteSearch(el: Element) {
|
||||
let searchUrl = el.getAttribute('data-search-url');
|
||||
const actionJumpUrl = el.getAttribute('data-action-jump-url');
|
||||
const selectedUserId = parseInt(el.getAttribute('data-selected-user-id'));
|
||||
let selectedUsername = '';
|
||||
if (!searchUrl.includes('?')) searchUrl += '?';
|
||||
let selectedUsername = el.getAttribute('data-selected-username') || '';
|
||||
const $searchDropdown = fomanticQuery(el);
|
||||
const elMenu = el.querySelector('.menu');
|
||||
const elSearchInput = el.querySelector<HTMLInputElement>('.ui.search input');
|
||||
const elItemFromInput = el.querySelector('.menu > .item-from-input');
|
||||
|
||||
@ -110,17 +109,27 @@ function initDropdownUserRemoteSearch(el: Element) {
|
||||
},
|
||||
});
|
||||
|
||||
const selectUsername = (username: string) => {
|
||||
queryElems(elMenu, '.item.active, .item.selected', (el) => el.classList.remove('active', 'selected'));
|
||||
elMenu.querySelector(`.item[data-value="${CSS.escape(username)}"]`)?.classList.add('selected');
|
||||
};
|
||||
|
||||
type ProcessedResult = {value: string, name: string};
|
||||
const processedResults: ProcessedResult[] = []; // to be used by dropdown to generate menu items
|
||||
const syncItemFromInput = () => {
|
||||
elItemFromInput.setAttribute('data-value', elSearchInput.value);
|
||||
elItemFromInput.textContent = elSearchInput.value;
|
||||
toggleElem(elItemFromInput, !processedResults.length);
|
||||
const inputVal = elSearchInput.value.trim();
|
||||
elItemFromInput.setAttribute('data-value', inputVal);
|
||||
elItemFromInput.textContent = inputVal;
|
||||
const showItemFromInput = !processedResults.length && inputVal !== '';
|
||||
toggleElem(elItemFromInput, showItemFromInput);
|
||||
selectUsername(showItemFromInput ? inputVal : selectedUsername);
|
||||
};
|
||||
|
||||
elSearchInput.value = selectedUsername;
|
||||
if (!searchUrl) {
|
||||
elSearchInput.addEventListener('input', syncItemFromInput);
|
||||
} else {
|
||||
if (!searchUrl.includes('?')) searchUrl += '?';
|
||||
$searchDropdown.dropdown('setting', 'apiSettings', {
|
||||
cache: false,
|
||||
url: `${searchUrl}&q={query}`,
|
||||
@ -130,11 +139,10 @@ function initDropdownUserRemoteSearch(el: Element) {
|
||||
for (const item of resp.results) {
|
||||
let html = `<img class="ui avatar tw-align-middle" src="${htmlEscape(item.avatar_link)}" aria-hidden="true" alt="" width="20" height="20"><span class="gt-ellipsis">${htmlEscape(item.username)}</span>`;
|
||||
if (item.full_name) html += `<span class="search-fullname tw-ml-2">${htmlEscape(item.full_name)}</span>`;
|
||||
if (selectedUserId === item.user_id) selectedUsername = item.username;
|
||||
if (selectedUsername.toLowerCase() === item.username.toLowerCase()) selectedUsername = item.username;
|
||||
processedResults.push({value: item.username, name: html});
|
||||
}
|
||||
resp.results = processedResults;
|
||||
syncItemFromInput();
|
||||
return resp;
|
||||
},
|
||||
});
|
||||
@ -146,9 +154,8 @@ function initDropdownUserRemoteSearch(el: Element) {
|
||||
const dropdownTemplates = $searchDropdown.dropdown('setting', 'templates');
|
||||
$searchDropdown.dropdown('internal', 'setup', dropdownSetup);
|
||||
dropdownSetup.menu = function (values) {
|
||||
const menu = $searchDropdown.find('> .menu')[0];
|
||||
// remove old dynamic items
|
||||
for (const el of menu.querySelectorAll(':scope > .dynamic-item')) {
|
||||
for (const el of elMenu.querySelectorAll(':scope > .dynamic-item')) {
|
||||
el.remove();
|
||||
}
|
||||
|
||||
@ -160,16 +167,11 @@ function initDropdownUserRemoteSearch(el: Element) {
|
||||
}
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('divider', 'dynamic-item');
|
||||
menu.append(div, ...newMenuItems);
|
||||
elMenu.append(div, ...newMenuItems);
|
||||
}
|
||||
$searchDropdown.dropdown('refresh');
|
||||
// defer our selection to the next tick, because dropdown will set the selection item after this `menu` function
|
||||
setTimeout(() => {
|
||||
for (const el of menu.querySelectorAll('.item.active, .item.selected')) {
|
||||
el.classList.remove('active', 'selected');
|
||||
}
|
||||
menu.querySelector(`.item[data-value="${CSS.escape(selectedUsername)}"]`)?.classList.add('selected');
|
||||
}, 0);
|
||||
setTimeout(() => syncItemFromInput(), 0);
|
||||
};
|
||||
}
|
||||
|
||||
@ -221,8 +223,12 @@ async function initIssuePinSort() {
|
||||
}
|
||||
|
||||
export function initRepoIssueList() {
|
||||
if (!document.querySelector('.page-content.repository.issue-list, .page-content.repository.milestone-issue-list')) return;
|
||||
initRepoIssueListCheckboxes();
|
||||
queryElems(document, '.ui.dropdown.user-remote-search', (el) => initDropdownUserRemoteSearch(el));
|
||||
initIssuePinSort();
|
||||
if (document.querySelector('.page-content.repository.issue-list, .page-content.repository.milestone-issue-list')) {
|
||||
initRepoIssueListCheckboxes();
|
||||
queryElems(document, '.ui.dropdown.user-remote-search', (el) => initDropdownUserRemoteSearch(el));
|
||||
initIssuePinSort();
|
||||
} else if (document.querySelector('.page-content.dashboard.issues')) {
|
||||
// user or org home: issue list, pull request list
|
||||
queryElems(document, '.ui.dropdown.user-remote-search', (el) => initDropdownUserRemoteSearch(el));
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
export function initRepoPullRequestCommitStatus() {
|
||||
for (const btn of document.querySelectorAll('.commit-status-hide-checks')) {
|
||||
const panel = btn.closest('.commit-status-panel');
|
||||
const list = panel.querySelector('.commit-status-list');
|
||||
const list = panel.querySelector<HTMLElement>('.commit-status-list');
|
||||
btn.addEventListener('click', () => {
|
||||
list.style.maxHeight = list.style.maxHeight ? '' : '0px'; // toggle
|
||||
btn.textContent = btn.getAttribute(list.style.maxHeight ? 'data-show-all' : 'data-hide-all');
|
||||
|
@ -124,7 +124,7 @@ export function initRepoIssueFilterItemLabel() {
|
||||
|
||||
export function initRepoIssueCommentDelete() {
|
||||
// Delete comment
|
||||
document.addEventListener('click', async (e) => {
|
||||
document.addEventListener('click', async (e: MouseEvent & {target: HTMLElement}) => {
|
||||
if (!e.target.matches('.delete-comment')) return;
|
||||
e.preventDefault();
|
||||
|
||||
@ -143,7 +143,7 @@ export function initRepoIssueCommentDelete() {
|
||||
const counter = document.querySelector('#review-box .review-comments-counter');
|
||||
let num = parseInt(counter?.getAttribute('data-pending-comment-number')) - 1 || 0;
|
||||
num = Math.max(num, 0);
|
||||
counter.setAttribute('data-pending-comment-number', num);
|
||||
counter.setAttribute('data-pending-comment-number', String(num));
|
||||
counter.textContent = String(num);
|
||||
}
|
||||
|
||||
@ -199,7 +199,7 @@ export function initRepoIssueDependencyDelete() {
|
||||
|
||||
export function initRepoIssueCodeCommentCancel() {
|
||||
// Cancel inline code comment
|
||||
document.addEventListener('click', (e) => {
|
||||
document.addEventListener('click', (e: MouseEvent & {target: HTMLElement}) => {
|
||||
if (!e.target.matches('.cancel-code-comment')) return;
|
||||
|
||||
const form = e.target.closest('form');
|
||||
@ -268,12 +268,14 @@ export function initRepoPullRequestMergeInstruction() {
|
||||
export function initRepoPullRequestAllowMaintainerEdit() {
|
||||
const wrapper = document.querySelector('#allow-edits-from-maintainers');
|
||||
if (!wrapper) return;
|
||||
const checkbox = wrapper.querySelector('input[type="checkbox"]');
|
||||
const checkbox = wrapper.querySelector<HTMLInputElement>('input[type="checkbox"]');
|
||||
checkbox.addEventListener('input', async () => {
|
||||
const url = `${wrapper.getAttribute('data-url')}/set_allow_maintainer_edit`;
|
||||
wrapper.classList.add('is-loading');
|
||||
try {
|
||||
const resp = await POST(url, {data: new URLSearchParams({allow_maintainer_edit: checkbox.checked})});
|
||||
const resp = await POST(url, {data: new URLSearchParams({
|
||||
allow_maintainer_edit: String(checkbox.checked),
|
||||
})});
|
||||
if (!resp.ok) {
|
||||
throw new Error('Failed to update maintainer edit permission');
|
||||
}
|
||||
@ -322,7 +324,7 @@ export function initRepoIssueWipTitle() {
|
||||
|
||||
const $issueTitle = $('#issue_title');
|
||||
$issueTitle.trigger('focus');
|
||||
const value = $issueTitle.val().trim().toUpperCase();
|
||||
const value = ($issueTitle.val() as string).trim().toUpperCase();
|
||||
|
||||
const wipPrefixes = $('.title_wip_desc').data('wip-prefixes');
|
||||
for (const prefix of wipPrefixes) {
|
||||
@ -338,7 +340,7 @@ export function initRepoIssueWipTitle() {
|
||||
export function initRepoIssueComments() {
|
||||
if (!$('.repository.view.issue .timeline').length) return;
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
document.addEventListener('click', (e: MouseEvent & {target: HTMLElement}) => {
|
||||
const urlTarget = document.querySelector(':target');
|
||||
if (!urlTarget) return;
|
||||
|
||||
@ -490,7 +492,7 @@ export function initRepoPullRequestReview() {
|
||||
|
||||
export function initRepoIssueReferenceIssue() {
|
||||
// Reference issue
|
||||
$(document).on('click', '.reference-issue', function (event) {
|
||||
$(document).on('click', '.reference-issue', function (e) {
|
||||
const target = this.getAttribute('data-target');
|
||||
const content = document.querySelector(`#${target}`)?.textContent ?? '';
|
||||
const poster = this.getAttribute('data-poster-username');
|
||||
@ -500,7 +502,7 @@ export function initRepoIssueReferenceIssue() {
|
||||
const textarea = modal.querySelector('textarea[name="content"]');
|
||||
textarea.value = `${content}\n\n_Originally posted by @${poster} in ${reference}_`;
|
||||
$(modal).modal('show');
|
||||
event.preventDefault();
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
@ -584,7 +586,7 @@ export function initRepoIssueTitleEdit() {
|
||||
}
|
||||
|
||||
export function initRepoIssueBranchSelect() {
|
||||
document.querySelector('#branch-select')?.addEventListener('click', (e) => {
|
||||
document.querySelector('#branch-select')?.addEventListener('click', (e: MouseEvent & {target: HTMLElement}) => {
|
||||
const el = e.target.closest('.item[data-branch]');
|
||||
if (!el) return;
|
||||
const pullTargetBranch = document.querySelector('#pull-target-branch');
|
||||
|
@ -1,14 +1,14 @@
|
||||
import {hideElem, showElem, toggleElem} from '../utils/dom.ts';
|
||||
|
||||
const service = document.querySelector('#service_type');
|
||||
const user = document.querySelector('#auth_username');
|
||||
const pass = document.querySelector('#auth_password');
|
||||
const token = document.querySelector('#auth_token');
|
||||
const mirror = document.querySelector('#mirror');
|
||||
const lfs = document.querySelector('#lfs');
|
||||
const lfsSettings = document.querySelector('#lfs_settings');
|
||||
const lfsEndpoint = document.querySelector('#lfs_endpoint');
|
||||
const items = document.querySelectorAll('#migrate_items input[type=checkbox]');
|
||||
const service = document.querySelector<HTMLInputElement>('#service_type');
|
||||
const user = document.querySelector<HTMLInputElement>('#auth_username');
|
||||
const pass = document.querySelector<HTMLInputElement>('#auth_password');
|
||||
const token = document.querySelector<HTMLInputElement>('#auth_token');
|
||||
const mirror = document.querySelector<HTMLInputElement>('#mirror');
|
||||
const lfs = document.querySelector<HTMLInputElement>('#lfs');
|
||||
const lfsSettings = document.querySelector<HTMLElement>('#lfs_settings');
|
||||
const lfsEndpoint = document.querySelector<HTMLElement>('#lfs_endpoint');
|
||||
const items = document.querySelectorAll<HTMLInputElement>('#migrate_items input[type=checkbox]');
|
||||
|
||||
export function initRepoMigration() {
|
||||
checkAuth();
|
||||
@ -25,11 +25,11 @@ export function initRepoMigration() {
|
||||
});
|
||||
lfs?.addEventListener('change', setLFSSettingsVisibility);
|
||||
|
||||
const cloneAddr = document.querySelector('#clone_addr');
|
||||
const cloneAddr = document.querySelector<HTMLInputElement>('#clone_addr');
|
||||
cloneAddr?.addEventListener('change', () => {
|
||||
const repoName = document.querySelector('#repo_name');
|
||||
const repoName = document.querySelector<HTMLInputElement>('#repo_name');
|
||||
if (cloneAddr.value && !repoName?.value) { // Only modify if repo_name input is blank
|
||||
repoName.value = cloneAddr.value.match(/^(.*\/)?((.+?)(\.git)?)$/)[3];
|
||||
repoName.value = /^(.*\/)?((.+?)(\.git)?)$/.exec(cloneAddr.value)[3];
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -41,8 +41,8 @@ function checkAuth() {
|
||||
checkItems(serviceType !== 1);
|
||||
}
|
||||
|
||||
function checkItems(tokenAuth) {
|
||||
let enableItems;
|
||||
function checkItems(tokenAuth: boolean) {
|
||||
let enableItems = false;
|
||||
if (tokenAuth) {
|
||||
enableItems = token?.value !== '';
|
||||
} else {
|
||||
|
@ -7,7 +7,7 @@ export function initRepoNew() {
|
||||
const gitignores = $('input[name="gitignores"]').val();
|
||||
const license = $('input[name="license"]').val();
|
||||
if (gitignores || license) {
|
||||
document.querySelector('input[name="auto_init"]').checked = true;
|
||||
document.querySelector<HTMLInputElement>('input[name="auto_init"]').checked = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ async function createNewColumn(url, columnTitle, projectColorInput) {
|
||||
}
|
||||
}
|
||||
|
||||
async function moveIssue({item, from, to, oldIndex}) {
|
||||
async function moveIssue({item, from, to, oldIndex}: {item: HTMLElement, from: HTMLElement, to: HTMLElement, oldIndex: number}) {
|
||||
const columnCards = to.querySelectorAll('.issue-card');
|
||||
updateIssueCount(from);
|
||||
updateIssueCount(to);
|
||||
@ -97,14 +97,14 @@ export function initRepoProject() {
|
||||
return;
|
||||
}
|
||||
|
||||
const _promise = initRepoProjectSortable();
|
||||
initRepoProjectSortable(); // no await
|
||||
|
||||
for (const modal of document.querySelectorAll('.edit-project-column-modal')) {
|
||||
const projectHeader = modal.closest('.project-column-header');
|
||||
const projectTitleLabel = projectHeader?.querySelector('.project-column-title-label');
|
||||
const projectTitleInput = modal.querySelector('.project-column-title-input');
|
||||
const projectColorInput = modal.querySelector('#new_project_column_color');
|
||||
const boardColumn = modal.closest('.project-column');
|
||||
const projectHeader = modal.closest<HTMLElement>('.project-column-header');
|
||||
const projectTitleLabel = projectHeader?.querySelector<HTMLElement>('.project-column-title-label');
|
||||
const projectTitleInput = modal.querySelector<HTMLInputElement>('.project-column-title-input');
|
||||
const projectColorInput = modal.querySelector<HTMLInputElement>('#new_project_column_color');
|
||||
const boardColumn = modal.closest<HTMLElement>('.project-column');
|
||||
modal.querySelector('.edit-project-column-button')?.addEventListener('click', async function (e) {
|
||||
e.preventDefault();
|
||||
try {
|
||||
@ -119,7 +119,7 @@ export function initRepoProject() {
|
||||
} finally {
|
||||
projectTitleLabel.textContent = projectTitleInput?.value;
|
||||
projectTitleInput.closest('form')?.classList.remove('dirty');
|
||||
const dividers = boardColumn.querySelectorAll(':scope > .divider');
|
||||
const dividers = boardColumn.querySelectorAll<HTMLElement>(':scope > .divider');
|
||||
if (projectColorInput.value) {
|
||||
const color = contrastColor(projectColorInput.value);
|
||||
boardColumn.style.setProperty('background', projectColorInput.value, 'important');
|
||||
|
@ -1,11 +1,11 @@
|
||||
import {hideElem, showElem} from '../utils/dom.ts';
|
||||
|
||||
export function initRepoRelease() {
|
||||
document.addEventListener('click', (e) => {
|
||||
document.addEventListener('click', (e: MouseEvent & {target: HTMLElement}) => {
|
||||
if (e.target.matches('.remove-rel-attach')) {
|
||||
const uuid = e.target.getAttribute('data-uuid');
|
||||
const id = e.target.getAttribute('data-id');
|
||||
document.querySelector(`input[name='attachment-del-${uuid}']`).value = 'true';
|
||||
document.querySelector<HTMLInputElement>(`input[name='attachment-del-${uuid}']`).value = 'true';
|
||||
hideElem(`#attachment-${id}`);
|
||||
}
|
||||
});
|
||||
@ -28,8 +28,8 @@ function initTagNameEditor() {
|
||||
const newTagHelperText = el.getAttribute('data-tag-helper-new');
|
||||
const existingTagHelperText = el.getAttribute('data-tag-helper-existing');
|
||||
|
||||
const tagNameInput = document.querySelector('#tag-name');
|
||||
const hideTargetInput = function(tagNameInput) {
|
||||
const tagNameInput = document.querySelector<HTMLInputElement>('#tag-name');
|
||||
const hideTargetInput = function(tagNameInput: HTMLInputElement) {
|
||||
const value = tagNameInput.value;
|
||||
const tagHelper = document.querySelector('#tag-helper');
|
||||
if (existingTags.includes(value)) {
|
||||
@ -42,7 +42,7 @@ function initTagNameEditor() {
|
||||
}
|
||||
};
|
||||
hideTargetInput(tagNameInput); // update on page load because the input may have a value
|
||||
tagNameInput.addEventListener('input', (e) => {
|
||||
tagNameInput.addEventListener('input', (e: InputEvent & {target: HTMLInputElement}) => {
|
||||
hideTargetInput(e.target);
|
||||
});
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
export function initRepositorySearch() {
|
||||
const repositorySearchForm = document.querySelector('#repo-search-form');
|
||||
const repositorySearchForm = document.querySelector<HTMLFormElement>('#repo-search-form');
|
||||
if (!repositorySearchForm) return;
|
||||
|
||||
repositorySearchForm.addEventListener('change', (e) => {
|
||||
repositorySearchForm.addEventListener('change', (e: Event & {target: HTMLFormElement}) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(repositorySearchForm);
|
||||
|
@ -73,7 +73,7 @@ function initRepoSettingsSearchTeamBox() {
|
||||
function initRepoSettingsGitHook() {
|
||||
if (!$('.edit.githook').length) return;
|
||||
const filename = document.querySelector('.hook-filename').textContent;
|
||||
createMonaco($('#content')[0], filename, {language: 'shell'});
|
||||
createMonaco($('#content')[0] as HTMLTextAreaElement, filename, {language: 'shell'});
|
||||
}
|
||||
|
||||
function initRepoSettingsBranches() {
|
||||
@ -99,7 +99,7 @@ function initRepoSettingsBranches() {
|
||||
|
||||
// show the `Matched` mark for the status checks that match the pattern
|
||||
const markMatchedStatusChecks = () => {
|
||||
const patterns = (document.querySelector('#status_check_contexts').value || '').split(/[\r\n]+/);
|
||||
const patterns = (document.querySelector<HTMLTextAreaElement>('#status_check_contexts').value || '').split(/[\r\n]+/);
|
||||
const validPatterns = patterns.map((item) => item.trim()).filter(Boolean);
|
||||
const marks = document.querySelectorAll('.status-check-matched-mark');
|
||||
|
||||
@ -122,7 +122,7 @@ function initRepoSettingsBranches() {
|
||||
function initRepoSettingsOptions() {
|
||||
if ($('.repository.settings.options').length > 0) {
|
||||
// Enable or select internal/external wiki system and issue tracker.
|
||||
$('.enable-system').on('change', function () {
|
||||
$('.enable-system').on('change', function (this: HTMLInputElement) {
|
||||
if (this.checked) {
|
||||
$($(this).data('target')).removeClass('disabled');
|
||||
if (!$(this).data('context')) $($(this).data('context')).addClass('disabled');
|
||||
@ -131,7 +131,7 @@ function initRepoSettingsOptions() {
|
||||
if (!$(this).data('context')) $($(this).data('context')).removeClass('disabled');
|
||||
}
|
||||
});
|
||||
$('.enable-system-radio').on('change', function () {
|
||||
$('.enable-system-radio').on('change', function (this: HTMLInputElement) {
|
||||
if (this.value === 'false') {
|
||||
$($(this).data('target')).addClass('disabled');
|
||||
if ($(this).data('context') !== undefined) $($(this).data('context')).removeClass('disabled');
|
||||
|
@ -2,7 +2,7 @@ export function initSshKeyFormParser() {
|
||||
// Parse SSH Key
|
||||
document.querySelector('#ssh-key-content')?.addEventListener('input', function () {
|
||||
const arrays = this.value.split(' ');
|
||||
const title = document.querySelector('#ssh-key-title');
|
||||
const title = document.querySelector<HTMLInputElement>('#ssh-key-title');
|
||||
if (!title.value && arrays.length === 3 && arrays[2] !== '') {
|
||||
title.value = arrays[2];
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ function tableSort(normSort, revSort, isDefault) {
|
||||
if (!normSort) return false;
|
||||
if (!revSort) revSort = '';
|
||||
|
||||
const url = new URL(window.location);
|
||||
const url = new URL(window.location.href);
|
||||
let urlSort = url.searchParams.get('sort');
|
||||
if (!urlSort && isDefault) urlSort = normSort;
|
||||
|
||||
|
@ -48,7 +48,7 @@ function makeCollections({mentions, emoji}) {
|
||||
return collections;
|
||||
}
|
||||
|
||||
export async function attachTribute(element, {mentions, emoji} = {}) {
|
||||
export async function attachTribute(element, {mentions, emoji}) {
|
||||
const {default: Tribute} = await import(/* webpackChunkName: "tribute" */'tributejs');
|
||||
const collections = makeCollections({mentions, emoji});
|
||||
const tribute = new Tribute({collection: collections, noMatchTemplate: ''});
|
||||
|
2
web_src/js/globals.d.ts
vendored
2
web_src/js/globals.d.ts
vendored
@ -33,6 +33,7 @@ interface JQuery {
|
||||
modal: any; // fomantic
|
||||
tab: any; // fomantic
|
||||
transition: any, // fomantic
|
||||
search: any, // fomantic
|
||||
}
|
||||
|
||||
interface JQueryStatic {
|
||||
@ -62,4 +63,5 @@ interface Window {
|
||||
turnstile: any,
|
||||
hcaptcha: any,
|
||||
codeEditors: any[],
|
||||
updateCloneStates: () => void,
|
||||
}
|
||||
|
@ -5,6 +5,9 @@ import Toastify from 'toastify-js'; // don't use "async import", because when ne
|
||||
import type {Intent} from '../types.ts';
|
||||
import type {SvgName} from '../svg.ts';
|
||||
import type {Options} from 'toastify-js';
|
||||
import type StartToastifyInstance from 'toastify-js';
|
||||
|
||||
export type Toast = ReturnType<typeof StartToastifyInstance>;
|
||||
|
||||
type ToastLevels = {
|
||||
[intent in Intent]: {
|
||||
@ -38,7 +41,7 @@ type ToastOpts = {
|
||||
} & Options;
|
||||
|
||||
// See https://github.com/apvarun/toastify-js#api for options
|
||||
function showToast(message: string, level: Intent, {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other}: ToastOpts = {}) {
|
||||
function showToast(message: string, level: Intent, {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other}: ToastOpts = {}): Toast {
|
||||
const body = useHtmlBody ? String(message) : htmlEscape(message);
|
||||
const key = `${level}-${body}`;
|
||||
|
||||
@ -75,14 +78,14 @@ function showToast(message: string, level: Intent, {gravity, position, duration,
|
||||
return toast;
|
||||
}
|
||||
|
||||
export function showInfoToast(message: string, opts?: ToastOpts) {
|
||||
export function showInfoToast(message: string, opts?: ToastOpts): Toast {
|
||||
return showToast(message, 'info', opts);
|
||||
}
|
||||
|
||||
export function showWarningToast(message: string, opts?: ToastOpts) {
|
||||
export function showWarningToast(message: string, opts?: ToastOpts): Toast {
|
||||
return showToast(message, 'warning', opts);
|
||||
}
|
||||
|
||||
export function showErrorToast(message: string, opts?: ToastOpts) {
|
||||
export function showErrorToast(message: string, opts?: ToastOpts): Toast {
|
||||
return showToast(message, 'error', opts);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user