mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	Never use /api/v1 from Gitea UI Pages (#19318)
Reusing `/api/v1` from Gitea UI Pages have pros and cons. Pros: 1) Less code copy Cons: 1) API/v1 have to support shared session with page requests. 2) You need to consider for each other when you want to change something about api/v1 or page. This PR moves all dependencies to API/v1 from UI Pages. Partially replace #16052
This commit is contained in:
		@@ -7,6 +7,7 @@ package integrations
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
@@ -20,6 +21,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/indexer/issues"
 | 
			
		||||
	"code.gitea.io/gitea/modules/references"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/test"
 | 
			
		||||
 | 
			
		||||
	"github.com/PuerkitoBio/goquery"
 | 
			
		||||
@@ -347,3 +349,209 @@ func TestIssueRedirect(t *testing.T) {
 | 
			
		||||
	resp = session.MakeRequest(t, req, http.StatusSeeOther)
 | 
			
		||||
	assert.Equal(t, "/"+path.Join("org26", "repo_external_tracker_alpha", "pulls", "1"), test.RedirectURL(resp))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestSearchIssues(t *testing.T) {
 | 
			
		||||
	defer prepareTestEnv(t)()
 | 
			
		||||
 | 
			
		||||
	session := loginUser(t, "user2")
 | 
			
		||||
 | 
			
		||||
	link, _ := url.Parse("/issues/search")
 | 
			
		||||
	req := NewRequest(t, "GET", link.String())
 | 
			
		||||
	resp := session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	var apiIssues []*api.Issue
 | 
			
		||||
	DecodeJSON(t, resp, &apiIssues)
 | 
			
		||||
	assert.Len(t, apiIssues, 10)
 | 
			
		||||
 | 
			
		||||
	req = NewRequest(t, "GET", link.String())
 | 
			
		||||
	resp = session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	DecodeJSON(t, resp, &apiIssues)
 | 
			
		||||
	assert.Len(t, apiIssues, 10)
 | 
			
		||||
 | 
			
		||||
	since := "2000-01-01T00%3A50%3A01%2B00%3A00" // 946687801
 | 
			
		||||
	before := time.Unix(999307200, 0).Format(time.RFC3339)
 | 
			
		||||
	query := url.Values{}
 | 
			
		||||
	query.Add("since", since)
 | 
			
		||||
	query.Add("before", before)
 | 
			
		||||
	link.RawQuery = query.Encode()
 | 
			
		||||
	req = NewRequest(t, "GET", link.String())
 | 
			
		||||
	resp = session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	DecodeJSON(t, resp, &apiIssues)
 | 
			
		||||
	assert.Len(t, apiIssues, 8)
 | 
			
		||||
	query.Del("since")
 | 
			
		||||
	query.Del("before")
 | 
			
		||||
 | 
			
		||||
	query.Add("state", "closed")
 | 
			
		||||
	link.RawQuery = query.Encode()
 | 
			
		||||
	req = NewRequest(t, "GET", link.String())
 | 
			
		||||
	resp = session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	DecodeJSON(t, resp, &apiIssues)
 | 
			
		||||
	assert.Len(t, apiIssues, 2)
 | 
			
		||||
 | 
			
		||||
	query.Set("state", "all")
 | 
			
		||||
	link.RawQuery = query.Encode()
 | 
			
		||||
	req = NewRequest(t, "GET", link.String())
 | 
			
		||||
	resp = session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	DecodeJSON(t, resp, &apiIssues)
 | 
			
		||||
	assert.EqualValues(t, "15", resp.Header().Get("X-Total-Count"))
 | 
			
		||||
	assert.Len(t, apiIssues, 10) // there are more but 10 is page item limit
 | 
			
		||||
 | 
			
		||||
	query.Add("limit", "20")
 | 
			
		||||
	link.RawQuery = query.Encode()
 | 
			
		||||
	req = NewRequest(t, "GET", link.String())
 | 
			
		||||
	resp = session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	DecodeJSON(t, resp, &apiIssues)
 | 
			
		||||
	assert.Len(t, apiIssues, 15)
 | 
			
		||||
 | 
			
		||||
	query = url.Values{"assigned": {"true"}, "state": {"all"}}
 | 
			
		||||
	link.RawQuery = query.Encode()
 | 
			
		||||
	req = NewRequest(t, "GET", link.String())
 | 
			
		||||
	resp = session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	DecodeJSON(t, resp, &apiIssues)
 | 
			
		||||
	assert.Len(t, apiIssues, 1)
 | 
			
		||||
 | 
			
		||||
	query = url.Values{"milestones": {"milestone1"}, "state": {"all"}}
 | 
			
		||||
	link.RawQuery = query.Encode()
 | 
			
		||||
	req = NewRequest(t, "GET", link.String())
 | 
			
		||||
	resp = session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	DecodeJSON(t, resp, &apiIssues)
 | 
			
		||||
	assert.Len(t, apiIssues, 1)
 | 
			
		||||
 | 
			
		||||
	query = url.Values{"milestones": {"milestone1,milestone3"}, "state": {"all"}}
 | 
			
		||||
	link.RawQuery = query.Encode()
 | 
			
		||||
	req = NewRequest(t, "GET", link.String())
 | 
			
		||||
	resp = session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	DecodeJSON(t, resp, &apiIssues)
 | 
			
		||||
	assert.Len(t, apiIssues, 2)
 | 
			
		||||
 | 
			
		||||
	query = url.Values{"owner": {"user2"}} // user
 | 
			
		||||
	link.RawQuery = query.Encode()
 | 
			
		||||
	req = NewRequest(t, "GET", link.String())
 | 
			
		||||
	resp = session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	DecodeJSON(t, resp, &apiIssues)
 | 
			
		||||
	assert.Len(t, apiIssues, 6)
 | 
			
		||||
 | 
			
		||||
	query = url.Values{"owner": {"user3"}} // organization
 | 
			
		||||
	link.RawQuery = query.Encode()
 | 
			
		||||
	req = NewRequest(t, "GET", link.String())
 | 
			
		||||
	resp = session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	DecodeJSON(t, resp, &apiIssues)
 | 
			
		||||
	assert.Len(t, apiIssues, 3)
 | 
			
		||||
 | 
			
		||||
	query = url.Values{"owner": {"user3"}, "team": {"team1"}} // organization + team
 | 
			
		||||
	link.RawQuery = query.Encode()
 | 
			
		||||
	req = NewRequest(t, "GET", link.String())
 | 
			
		||||
	resp = session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	DecodeJSON(t, resp, &apiIssues)
 | 
			
		||||
	assert.Len(t, apiIssues, 2)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestSearchIssuesWithLabels(t *testing.T) {
 | 
			
		||||
	defer prepareTestEnv(t)()
 | 
			
		||||
 | 
			
		||||
	session := loginUser(t, "user1")
 | 
			
		||||
 | 
			
		||||
	link, _ := url.Parse("/api/v1/repos/issues/search")
 | 
			
		||||
	req := NewRequest(t, "GET", link.String())
 | 
			
		||||
	resp := session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	var apiIssues []*api.Issue
 | 
			
		||||
	DecodeJSON(t, resp, &apiIssues)
 | 
			
		||||
 | 
			
		||||
	assert.Len(t, apiIssues, 10)
 | 
			
		||||
 | 
			
		||||
	query := url.Values{}
 | 
			
		||||
	link.RawQuery = query.Encode()
 | 
			
		||||
	req = NewRequest(t, "GET", link.String())
 | 
			
		||||
	resp = session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	DecodeJSON(t, resp, &apiIssues)
 | 
			
		||||
	assert.Len(t, apiIssues, 10)
 | 
			
		||||
 | 
			
		||||
	query.Add("labels", "label1")
 | 
			
		||||
	link.RawQuery = query.Encode()
 | 
			
		||||
	req = NewRequest(t, "GET", link.String())
 | 
			
		||||
	resp = session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	DecodeJSON(t, resp, &apiIssues)
 | 
			
		||||
	assert.Len(t, apiIssues, 2)
 | 
			
		||||
 | 
			
		||||
	// multiple labels
 | 
			
		||||
	query.Set("labels", "label1,label2")
 | 
			
		||||
	link.RawQuery = query.Encode()
 | 
			
		||||
	req = NewRequest(t, "GET", link.String())
 | 
			
		||||
	resp = session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	DecodeJSON(t, resp, &apiIssues)
 | 
			
		||||
	assert.Len(t, apiIssues, 2)
 | 
			
		||||
 | 
			
		||||
	// an org label
 | 
			
		||||
	query.Set("labels", "orglabel4")
 | 
			
		||||
	link.RawQuery = query.Encode()
 | 
			
		||||
	req = NewRequest(t, "GET", link.String())
 | 
			
		||||
	resp = session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	DecodeJSON(t, resp, &apiIssues)
 | 
			
		||||
	assert.Len(t, apiIssues, 1)
 | 
			
		||||
 | 
			
		||||
	// org and repo label
 | 
			
		||||
	query.Set("labels", "label2,orglabel4")
 | 
			
		||||
	query.Add("state", "all")
 | 
			
		||||
	link.RawQuery = query.Encode()
 | 
			
		||||
	req = NewRequest(t, "GET", link.String())
 | 
			
		||||
	resp = session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	DecodeJSON(t, resp, &apiIssues)
 | 
			
		||||
	assert.Len(t, apiIssues, 2)
 | 
			
		||||
 | 
			
		||||
	// org and repo label which share the same issue
 | 
			
		||||
	query.Set("labels", "label1,orglabel4")
 | 
			
		||||
	link.RawQuery = query.Encode()
 | 
			
		||||
	req = NewRequest(t, "GET", link.String())
 | 
			
		||||
	resp = session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	DecodeJSON(t, resp, &apiIssues)
 | 
			
		||||
	assert.Len(t, apiIssues, 2)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetIssueInfo(t *testing.T) {
 | 
			
		||||
	defer prepareTestEnv(t)()
 | 
			
		||||
 | 
			
		||||
	issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 10}).(*models.Issue)
 | 
			
		||||
	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository)
 | 
			
		||||
	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
 | 
			
		||||
	assert.NoError(t, issue.LoadAttributes())
 | 
			
		||||
	assert.Equal(t, int64(1019307200), int64(issue.DeadlineUnix))
 | 
			
		||||
	assert.Equal(t, api.StateOpen, issue.State())
 | 
			
		||||
 | 
			
		||||
	session := loginUser(t, owner.Name)
 | 
			
		||||
 | 
			
		||||
	urlStr := fmt.Sprintf("/%s/%s/issues/%d/info", owner.Name, repo.Name, issue.Index)
 | 
			
		||||
	req := NewRequest(t, "GET", urlStr)
 | 
			
		||||
	resp := session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	var apiIssue api.Issue
 | 
			
		||||
	DecodeJSON(t, resp, &apiIssue)
 | 
			
		||||
 | 
			
		||||
	assert.EqualValues(t, issue.ID, apiIssue.ID)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestUpdateIssueDeadline(t *testing.T) {
 | 
			
		||||
	defer prepareTestEnv(t)()
 | 
			
		||||
 | 
			
		||||
	issueBefore := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: 10}).(*models.Issue)
 | 
			
		||||
	repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID}).(*repo_model.Repository)
 | 
			
		||||
	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID}).(*user_model.User)
 | 
			
		||||
	assert.NoError(t, issueBefore.LoadAttributes())
 | 
			
		||||
	assert.Equal(t, int64(1019307200), int64(issueBefore.DeadlineUnix))
 | 
			
		||||
	assert.Equal(t, api.StateOpen, issueBefore.State())
 | 
			
		||||
 | 
			
		||||
	session := loginUser(t, owner.Name)
 | 
			
		||||
 | 
			
		||||
	issueURL := fmt.Sprintf("%s/%s/issues/%d", owner.Name, repoBefore.Name, issueBefore.Index)
 | 
			
		||||
	req := NewRequest(t, "GET", issueURL)
 | 
			
		||||
	resp := session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	htmlDoc := NewHTMLParser(t, resp.Body)
 | 
			
		||||
 | 
			
		||||
	urlStr := issueURL + "/deadline?_csrf=" + htmlDoc.GetCSRF()
 | 
			
		||||
	req = NewRequestWithJSON(t, "POST", urlStr, map[string]string{
 | 
			
		||||
		"due_date": "2022-04-06T00:00:00.000Z",
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	resp = session.MakeRequest(t, req, http.StatusCreated)
 | 
			
		||||
	var apiIssue api.IssueDeadline
 | 
			
		||||
	DecodeJSON(t, resp, &apiIssue)
 | 
			
		||||
 | 
			
		||||
	assert.EqualValues(t, "2022-04-06", apiIssue.Deadline.Format("2006-01-02"))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,8 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
@@ -173,3 +175,30 @@ func TestOrgRestrictedUser(t *testing.T) {
 | 
			
		||||
	req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s", orgName, repoName))
 | 
			
		||||
	restrictedSession.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestTeamSearch(t *testing.T) {
 | 
			
		||||
	defer prepareTestEnv(t)()
 | 
			
		||||
 | 
			
		||||
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
 | 
			
		||||
	org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}).(*user_model.User)
 | 
			
		||||
 | 
			
		||||
	var results TeamSearchResults
 | 
			
		||||
 | 
			
		||||
	session := loginUser(t, user.Name)
 | 
			
		||||
	csrf := GetCSRF(t, session, "/"+org.Name)
 | 
			
		||||
	req := NewRequestf(t, "GET", "/org/%s/teams/-/search?q=%s", org.Name, "_team")
 | 
			
		||||
	req.Header.Add("X-Csrf-Token", csrf)
 | 
			
		||||
	resp := session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	DecodeJSON(t, resp, &results)
 | 
			
		||||
	assert.NotEmpty(t, results.Data)
 | 
			
		||||
	assert.Len(t, results.Data, 1)
 | 
			
		||||
	assert.Equal(t, "test_team", results.Data[0].Name)
 | 
			
		||||
 | 
			
		||||
	// no access if not organization member
 | 
			
		||||
	user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}).(*user_model.User)
 | 
			
		||||
	session = loginUser(t, user5.Name)
 | 
			
		||||
	csrf = GetCSRF(t, session, "/"+org.Name)
 | 
			
		||||
	req = NewRequestf(t, "GET", "/org/%s/teams/-/search?q=%s", org.Name, "team")
 | 
			
		||||
	req.Header.Add("X-Csrf-Token", csrf)
 | 
			
		||||
	session.MakeRequest(t, req, http.StatusNotFound)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										46
									
								
								integrations/repo_topic_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								integrations/repo_topic_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
			
		||||
// Copyright 2022 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package integrations
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestTopicSearch(t *testing.T) {
 | 
			
		||||
	defer prepareTestEnv(t)()
 | 
			
		||||
	searchURL, _ := url.Parse("/explore/topics/search")
 | 
			
		||||
	var topics struct {
 | 
			
		||||
		TopicNames []*api.TopicResponse `json:"topics"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	query := url.Values{"page": []string{"1"}, "limit": []string{"4"}}
 | 
			
		||||
 | 
			
		||||
	searchURL.RawQuery = query.Encode()
 | 
			
		||||
	res := MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
 | 
			
		||||
	DecodeJSON(t, res, &topics)
 | 
			
		||||
	assert.Len(t, topics.TopicNames, 4)
 | 
			
		||||
	assert.EqualValues(t, "6", res.Header().Get("x-total-count"))
 | 
			
		||||
 | 
			
		||||
	query.Add("q", "topic")
 | 
			
		||||
	searchURL.RawQuery = query.Encode()
 | 
			
		||||
	res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
 | 
			
		||||
	DecodeJSON(t, res, &topics)
 | 
			
		||||
	assert.Len(t, topics.TopicNames, 2)
 | 
			
		||||
 | 
			
		||||
	query.Set("q", "database")
 | 
			
		||||
	searchURL.RawQuery = query.Encode()
 | 
			
		||||
	res = MakeRequest(t, NewRequest(t, "GET", searchURL.String()), http.StatusOK)
 | 
			
		||||
	DecodeJSON(t, res, &topics)
 | 
			
		||||
	if assert.Len(t, topics.TopicNames, 1) {
 | 
			
		||||
		assert.EqualValues(t, 2, topics.TopicNames[0].ID)
 | 
			
		||||
		assert.EqualValues(t, "database", topics.TopicNames[0].Name)
 | 
			
		||||
		assert.EqualValues(t, 1, topics.TopicNames[0].RepoCount)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -8,8 +8,11 @@ import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models"
 | 
			
		||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/test"
 | 
			
		||||
	"code.gitea.io/gitea/modules/translation/i18n"
 | 
			
		||||
 | 
			
		||||
@@ -222,3 +225,26 @@ func testExportUserGPGKeys(t *testing.T, user, expected string) {
 | 
			
		||||
	// t.Log(resp.Body.String())
 | 
			
		||||
	assert.Equal(t, expected, resp.Body.String())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestListStopWatches(t *testing.T) {
 | 
			
		||||
	defer prepareTestEnv(t)()
 | 
			
		||||
 | 
			
		||||
	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
 | 
			
		||||
	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
 | 
			
		||||
 | 
			
		||||
	session := loginUser(t, owner.Name)
 | 
			
		||||
	req := NewRequestf(t, "GET", "/user/stopwatches")
 | 
			
		||||
	resp := session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
	var apiWatches []*api.StopWatch
 | 
			
		||||
	DecodeJSON(t, resp, &apiWatches)
 | 
			
		||||
	stopwatch := unittest.AssertExistsAndLoadBean(t, &models.Stopwatch{UserID: owner.ID}).(*models.Stopwatch)
 | 
			
		||||
	issue := unittest.AssertExistsAndLoadBean(t, &models.Issue{ID: stopwatch.IssueID}).(*models.Issue)
 | 
			
		||||
	if assert.Len(t, apiWatches, 1) {
 | 
			
		||||
		assert.EqualValues(t, stopwatch.CreatedUnix.AsTime().Unix(), apiWatches[0].Created.Unix())
 | 
			
		||||
		assert.EqualValues(t, issue.Index, apiWatches[0].IssueIndex)
 | 
			
		||||
		assert.EqualValues(t, issue.Title, apiWatches[0].IssueTitle)
 | 
			
		||||
		assert.EqualValues(t, repo.Name, apiWatches[0].RepoName)
 | 
			
		||||
		assert.EqualValues(t, repo.OwnerName, apiWatches[0].RepoOwnerName)
 | 
			
		||||
		assert.Greater(t, int64(apiWatches[0].Seconds), int64(0))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -191,22 +191,6 @@ func (ctx *APIContext) SetLinkHeader(total, pageSize int) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetTotalCountHeader set "X-Total-Count" header
 | 
			
		||||
func (ctx *APIContext) SetTotalCountHeader(total int64) {
 | 
			
		||||
	ctx.RespHeader().Set("X-Total-Count", fmt.Sprint(total))
 | 
			
		||||
	ctx.AppendAccessControlExposeHeaders("X-Total-Count")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header
 | 
			
		||||
func (ctx *APIContext) AppendAccessControlExposeHeaders(names ...string) {
 | 
			
		||||
	val := ctx.RespHeader().Get("Access-Control-Expose-Headers")
 | 
			
		||||
	if len(val) != 0 {
 | 
			
		||||
		ctx.RespHeader().Set("Access-Control-Expose-Headers", fmt.Sprintf("%s, %s", val, strings.Join(names, ", ")))
 | 
			
		||||
	} else {
 | 
			
		||||
		ctx.RespHeader().Set("Access-Control-Expose-Headers", strings.Join(names, ", "))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RequireCSRF requires a validated a CSRF token
 | 
			
		||||
func (ctx *APIContext) RequireCSRF() {
 | 
			
		||||
	headerToken := ctx.Req.Header.Get(ctx.csrf.GetHeaderName())
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import (
 | 
			
		||||
	"crypto/sha256"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"io"
 | 
			
		||||
@@ -21,6 +22,7 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/models/unit"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/base"
 | 
			
		||||
@@ -577,6 +579,22 @@ func (ctx *Context) Value(key interface{}) interface{} {
 | 
			
		||||
	return ctx.Req.Context().Value(key)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetTotalCountHeader set "X-Total-Count" header
 | 
			
		||||
func (ctx *Context) SetTotalCountHeader(total int64) {
 | 
			
		||||
	ctx.RespHeader().Set("X-Total-Count", fmt.Sprint(total))
 | 
			
		||||
	ctx.AppendAccessControlExposeHeaders("X-Total-Count")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AppendAccessControlExposeHeaders append headers by name to "Access-Control-Expose-Headers" header
 | 
			
		||||
func (ctx *Context) AppendAccessControlExposeHeaders(names ...string) {
 | 
			
		||||
	val := ctx.RespHeader().Get("Access-Control-Expose-Headers")
 | 
			
		||||
	if len(val) != 0 {
 | 
			
		||||
		ctx.RespHeader().Set("Access-Control-Expose-Headers", fmt.Sprintf("%s, %s", val, strings.Join(names, ", ")))
 | 
			
		||||
	} else {
 | 
			
		||||
		ctx.RespHeader().Set("Access-Control-Expose-Headers", strings.Join(names, ", "))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Handler represents a custom handler
 | 
			
		||||
type Handler func(*Context)
 | 
			
		||||
 | 
			
		||||
@@ -780,3 +798,21 @@ func Contexter() func(next http.Handler) http.Handler {
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SearchOrderByMap represents all possible search order
 | 
			
		||||
var SearchOrderByMap = map[string]map[string]db.SearchOrderBy{
 | 
			
		||||
	"asc": {
 | 
			
		||||
		"alpha":   db.SearchOrderByAlphabetically,
 | 
			
		||||
		"created": db.SearchOrderByOldest,
 | 
			
		||||
		"updated": db.SearchOrderByLeastUpdated,
 | 
			
		||||
		"size":    db.SearchOrderBySize,
 | 
			
		||||
		"id":      db.SearchOrderByID,
 | 
			
		||||
	},
 | 
			
		||||
	"desc": {
 | 
			
		||||
		"alpha":   db.SearchOrderByAlphabeticallyReverse,
 | 
			
		||||
		"created": db.SearchOrderByNewest,
 | 
			
		||||
		"updated": db.SearchOrderByRecentUpdated,
 | 
			
		||||
		"size":    db.SearchOrderBySizeReverse,
 | 
			
		||||
		"id":      db.SearchOrderByIDReverse,
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,20 +2,16 @@
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package utils
 | 
			
		||||
package context
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/modules/context"
 | 
			
		||||
	"code.gitea.io/gitea/modules/convert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GetQueryBeforeSince return parsed time (unix format) from URL query's before and since
 | 
			
		||||
func GetQueryBeforeSince(ctx *context.APIContext) (before, since int64, err error) {
 | 
			
		||||
func GetQueryBeforeSince(ctx *Context) (before, since int64, err error) {
 | 
			
		||||
	qCreatedBefore, err := prepareQueryArg(ctx, "before")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, 0, err
 | 
			
		||||
@@ -53,16 +49,8 @@ func parseTime(value string) (int64, error) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// prepareQueryArg unescape and trim a query arg
 | 
			
		||||
func prepareQueryArg(ctx *context.APIContext, name string) (value string, err error) {
 | 
			
		||||
func prepareQueryArg(ctx *Context, name string) (value string, err error) {
 | 
			
		||||
	value, err = url.PathUnescape(ctx.FormString(name))
 | 
			
		||||
	value = strings.TrimSpace(value)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetListOptions returns list options using the page and limit parameters
 | 
			
		||||
func GetListOptions(ctx *context.APIContext) db.ListOptions {
 | 
			
		||||
	return db.ListOptions{
 | 
			
		||||
		Page:     ctx.FormInt("page"),
 | 
			
		||||
		PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -26,7 +26,7 @@ func NewAvailable(ctx *context.APIContext) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getFindNotificationOptions(ctx *context.APIContext) *models.FindNotificationOptions {
 | 
			
		||||
	before, since, err := utils.GetQueryBeforeSince(ctx)
 | 
			
		||||
	before, since, err := context.GetQueryBeforeSince(ctx.Context)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
 | 
			
		||||
		return nil
 | 
			
		||||
 
 | 
			
		||||
@@ -111,7 +111,7 @@ func SearchIssues(ctx *context.APIContext) {
 | 
			
		||||
	//   "200":
 | 
			
		||||
	//     "$ref": "#/responses/IssueList"
 | 
			
		||||
 | 
			
		||||
	before, since, err := utils.GetQueryBeforeSince(ctx)
 | 
			
		||||
	before, since, err := context.GetQueryBeforeSince(ctx.Context)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
 | 
			
		||||
		return
 | 
			
		||||
@@ -359,7 +359,7 @@ func ListIssues(ctx *context.APIContext) {
 | 
			
		||||
	// responses:
 | 
			
		||||
	//   "200":
 | 
			
		||||
	//     "$ref": "#/responses/IssueList"
 | 
			
		||||
	before, since, err := utils.GetQueryBeforeSince(ctx)
 | 
			
		||||
	before, since, err := context.GetQueryBeforeSince(ctx.Context)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
 | 
			
		||||
		return
 | 
			
		||||
 
 | 
			
		||||
@@ -58,7 +58,7 @@ func ListIssueComments(ctx *context.APIContext) {
 | 
			
		||||
	//   "200":
 | 
			
		||||
	//     "$ref": "#/responses/CommentList"
 | 
			
		||||
 | 
			
		||||
	before, since, err := utils.GetQueryBeforeSince(ctx)
 | 
			
		||||
	before, since, err := context.GetQueryBeforeSince(ctx.Context)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
 | 
			
		||||
		return
 | 
			
		||||
@@ -150,7 +150,7 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) {
 | 
			
		||||
	//   "200":
 | 
			
		||||
	//     "$ref": "#/responses/TimelineList"
 | 
			
		||||
 | 
			
		||||
	before, since, err := utils.GetQueryBeforeSince(ctx)
 | 
			
		||||
	before, since, err := context.GetQueryBeforeSince(ctx.Context)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
 | 
			
		||||
		return
 | 
			
		||||
@@ -253,7 +253,7 @@ func ListRepoIssueComments(ctx *context.APIContext) {
 | 
			
		||||
	//   "200":
 | 
			
		||||
	//     "$ref": "#/responses/CommentList"
 | 
			
		||||
 | 
			
		||||
	before, since, err := utils.GetQueryBeforeSince(ctx)
 | 
			
		||||
	before, since, err := context.GetQueryBeforeSince(ctx.Context)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
 | 
			
		||||
		return
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,7 @@ func ListTrackedTimes(ctx *context.APIContext) {
 | 
			
		||||
		opts.UserID = user.ID
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil {
 | 
			
		||||
	if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Context); err != nil {
 | 
			
		||||
		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
@@ -522,7 +522,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var err error
 | 
			
		||||
	if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil {
 | 
			
		||||
	if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Context); err != nil {
 | 
			
		||||
		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
@@ -597,7 +597,7 @@ func ListMyTrackedTimes(ctx *context.APIContext) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var err error
 | 
			
		||||
	if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = utils.GetQueryBeforeSince(ctx); err != nil {
 | 
			
		||||
	if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Context); err != nil {
 | 
			
		||||
		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -31,23 +31,6 @@ import (
 | 
			
		||||
	repo_service "code.gitea.io/gitea/services/repository"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var searchOrderByMap = map[string]map[string]db.SearchOrderBy{
 | 
			
		||||
	"asc": {
 | 
			
		||||
		"alpha":   db.SearchOrderByAlphabetically,
 | 
			
		||||
		"created": db.SearchOrderByOldest,
 | 
			
		||||
		"updated": db.SearchOrderByLeastUpdated,
 | 
			
		||||
		"size":    db.SearchOrderBySize,
 | 
			
		||||
		"id":      db.SearchOrderByID,
 | 
			
		||||
	},
 | 
			
		||||
	"desc": {
 | 
			
		||||
		"alpha":   db.SearchOrderByAlphabeticallyReverse,
 | 
			
		||||
		"created": db.SearchOrderByNewest,
 | 
			
		||||
		"updated": db.SearchOrderByRecentUpdated,
 | 
			
		||||
		"size":    db.SearchOrderBySizeReverse,
 | 
			
		||||
		"id":      db.SearchOrderByIDReverse,
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Search repositories via options
 | 
			
		||||
func Search(ctx *context.APIContext) {
 | 
			
		||||
	// swagger:operation GET /repos/search repository repoSearch
 | 
			
		||||
@@ -193,7 +176,7 @@ func Search(ctx *context.APIContext) {
 | 
			
		||||
		if len(sortOrder) == 0 {
 | 
			
		||||
			sortOrder = "asc"
 | 
			
		||||
		}
 | 
			
		||||
		if searchModeMap, ok := searchOrderByMap[sortOrder]; ok {
 | 
			
		||||
		if searchModeMap, ok := context.SearchOrderByMap[sortOrder]; ok {
 | 
			
		||||
			if orderBy, ok := searchModeMap[sortMode]; ok {
 | 
			
		||||
				opts.OrderBy = orderBy
 | 
			
		||||
			} else {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								routers/api/v1/utils/page.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								routers/api/v1/utils/page.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
// Copyright 2017 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/modules/context"
 | 
			
		||||
	"code.gitea.io/gitea/modules/convert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GetListOptions returns list options using the page and limit parameters
 | 
			
		||||
func GetListOptions(ctx *context.APIContext) db.ListOptions {
 | 
			
		||||
	return db.ListOptions{
 | 
			
		||||
		Page:     ctx.FormInt("page"),
 | 
			
		||||
		PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										42
									
								
								routers/web/explore/topic.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								routers/web/explore/topic.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
// Copyright 2022 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package explore
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	"code.gitea.io/gitea/modules/context"
 | 
			
		||||
	"code.gitea.io/gitea/modules/convert"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// TopicSearch search for creating topic
 | 
			
		||||
func TopicSearch(ctx *context.Context) {
 | 
			
		||||
	opts := &repo_model.FindTopicOptions{
 | 
			
		||||
		Keyword: ctx.FormString("q"),
 | 
			
		||||
		ListOptions: db.ListOptions{
 | 
			
		||||
			Page:     ctx.FormInt("page"),
 | 
			
		||||
			PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	topics, total, err := repo_model.FindTopics(opts)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.Error(http.StatusInternalServerError)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	topicResponses := make([]*api.TopicResponse, len(topics))
 | 
			
		||||
	for i, topic := range topics {
 | 
			
		||||
		topicResponses[i] = convert.ToTopicResponse(topic)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.SetTotalCountHeader(total)
 | 
			
		||||
	ctx.JSON(http.StatusOK, map[string]interface{}{
 | 
			
		||||
		"topics": topicResponses,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -13,6 +13,7 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models"
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/models/organization"
 | 
			
		||||
	"code.gitea.io/gitea/models/perm"
 | 
			
		||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
@@ -20,7 +21,9 @@ import (
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/base"
 | 
			
		||||
	"code.gitea.io/gitea/modules/context"
 | 
			
		||||
	"code.gitea.io/gitea/modules/convert"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/web"
 | 
			
		||||
	"code.gitea.io/gitea/routers/utils"
 | 
			
		||||
	"code.gitea.io/gitea/services/forms"
 | 
			
		||||
@@ -329,6 +332,51 @@ func TeamRepositories(ctx *context.Context) {
 | 
			
		||||
	ctx.HTML(http.StatusOK, tplTeamRepositories)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SearchTeam api for searching teams
 | 
			
		||||
func SearchTeam(ctx *context.Context) {
 | 
			
		||||
	listOptions := db.ListOptions{
 | 
			
		||||
		Page:     ctx.FormInt("page"),
 | 
			
		||||
		PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	opts := &organization.SearchTeamOptions{
 | 
			
		||||
		UserID:      ctx.Doer.ID,
 | 
			
		||||
		Keyword:     ctx.FormTrim("q"),
 | 
			
		||||
		OrgID:       ctx.Org.Organization.ID,
 | 
			
		||||
		IncludeDesc: ctx.FormString("include_desc") == "" || ctx.FormBool("include_desc"),
 | 
			
		||||
		ListOptions: listOptions,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	teams, maxResults, err := organization.SearchTeam(opts)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("SearchTeam failed: %v", err)
 | 
			
		||||
		ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
 | 
			
		||||
			"ok":    false,
 | 
			
		||||
			"error": "SearchTeam internal failure",
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	apiTeams := make([]*api.Team, len(teams))
 | 
			
		||||
	for i := range teams {
 | 
			
		||||
		if err := teams[i].GetUnits(); err != nil {
 | 
			
		||||
			log.Error("Team GetUnits failed: %v", err)
 | 
			
		||||
			ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
 | 
			
		||||
				"ok":    false,
 | 
			
		||||
				"error": "SearchTeam failed to get units",
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		apiTeams[i] = convert.ToTeam(teams[i])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.SetTotalCountHeader(maxResults)
 | 
			
		||||
	ctx.JSON(http.StatusOK, map[string]interface{}{
 | 
			
		||||
		"ok":   true,
 | 
			
		||||
		"data": apiTeams,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// EditTeam render team edit page
 | 
			
		||||
func EditTeam(ctx *context.Context) {
 | 
			
		||||
	ctx.Data["Title"] = ctx.Org.Organization.FullName
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@ import (
 | 
			
		||||
	"path"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models"
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
@@ -36,6 +37,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/templates/vars"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/upload"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/modules/web"
 | 
			
		||||
@@ -1762,6 +1764,20 @@ func getActionIssues(ctx *context.Context) []*models.Issue {
 | 
			
		||||
	return issues
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetIssueInfo get an issue of a repository
 | 
			
		||||
func GetIssueInfo(ctx *context.Context) {
 | 
			
		||||
	issue, err := models.GetIssueWithAttrsByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if models.IsErrIssueNotExist(err) {
 | 
			
		||||
			ctx.Error(http.StatusNotFound)
 | 
			
		||||
		} else {
 | 
			
		||||
			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error())
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	ctx.JSON(http.StatusOK, convert.ToAPIIssue(issue))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateIssueTitle change issue's title
 | 
			
		||||
func UpdateIssueTitle(ctx *context.Context) {
 | 
			
		||||
	issue := GetActionIssue(ctx)
 | 
			
		||||
@@ -1856,6 +1872,40 @@ func UpdateIssueContent(ctx *context.Context) {
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateIssueDeadline updates an issue deadline
 | 
			
		||||
func UpdateIssueDeadline(ctx *context.Context) {
 | 
			
		||||
	form := web.GetForm(ctx).(*api.EditDeadlineOption)
 | 
			
		||||
	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if models.IsErrIssueNotExist(err) {
 | 
			
		||||
			ctx.NotFound("GetIssueByIndex", err)
 | 
			
		||||
		} else {
 | 
			
		||||
			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err.Error())
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
 | 
			
		||||
		ctx.Error(http.StatusForbidden, "", "Not repo writer")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var deadlineUnix timeutil.TimeStamp
 | 
			
		||||
	var deadline time.Time
 | 
			
		||||
	if form.Deadline != nil && !form.Deadline.IsZero() {
 | 
			
		||||
		deadline = time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
 | 
			
		||||
			23, 59, 59, 0, time.Local)
 | 
			
		||||
		deadlineUnix = timeutil.TimeStamp(deadline.Unix())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := models.UpdateIssueDeadline(issue, deadlineUnix, ctx.Doer); err != nil {
 | 
			
		||||
		ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: &deadline})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateIssueMilestone change issue's milestone
 | 
			
		||||
func UpdateIssueMilestone(ctx *context.Context) {
 | 
			
		||||
	issues := getActionIssues(ctx)
 | 
			
		||||
@@ -2052,6 +2102,338 @@ func UpdatePullReviewRequest(ctx *context.Context) {
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SearchIssues searches for issues across the repositories that the user has access to
 | 
			
		||||
func SearchIssues(ctx *context.Context) {
 | 
			
		||||
	before, since, err := context.GetQueryBeforeSince(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.Error(http.StatusUnprocessableEntity, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var isClosed util.OptionalBool
 | 
			
		||||
	switch ctx.FormString("state") {
 | 
			
		||||
	case "closed":
 | 
			
		||||
		isClosed = util.OptionalBoolTrue
 | 
			
		||||
	case "all":
 | 
			
		||||
		isClosed = util.OptionalBoolNone
 | 
			
		||||
	default:
 | 
			
		||||
		isClosed = util.OptionalBoolFalse
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// find repos user can access (for issue search)
 | 
			
		||||
	opts := &models.SearchRepoOptions{
 | 
			
		||||
		Private:     false,
 | 
			
		||||
		AllPublic:   true,
 | 
			
		||||
		TopicOnly:   false,
 | 
			
		||||
		Collaborate: util.OptionalBoolNone,
 | 
			
		||||
		// This needs to be a column that is not nil in fixtures or
 | 
			
		||||
		// MySQL will return different results when sorting by null in some cases
 | 
			
		||||
		OrderBy: db.SearchOrderByAlphabetically,
 | 
			
		||||
		Actor:   ctx.Doer,
 | 
			
		||||
	}
 | 
			
		||||
	if ctx.IsSigned {
 | 
			
		||||
		opts.Private = true
 | 
			
		||||
		opts.AllLimited = true
 | 
			
		||||
	}
 | 
			
		||||
	if ctx.FormString("owner") != "" {
 | 
			
		||||
		owner, err := user_model.GetUserByName(ctx.FormString("owner"))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			if user_model.IsErrUserNotExist(err) {
 | 
			
		||||
				ctx.Error(http.StatusBadRequest, "Owner not found", err.Error())
 | 
			
		||||
			} else {
 | 
			
		||||
				ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error())
 | 
			
		||||
			}
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		opts.OwnerID = owner.ID
 | 
			
		||||
		opts.AllLimited = false
 | 
			
		||||
		opts.AllPublic = false
 | 
			
		||||
		opts.Collaborate = util.OptionalBoolFalse
 | 
			
		||||
	}
 | 
			
		||||
	if ctx.FormString("team") != "" {
 | 
			
		||||
		if ctx.FormString("owner") == "" {
 | 
			
		||||
			ctx.Error(http.StatusBadRequest, "", "Owner organisation is required for filtering on team")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		team, err := organization.GetTeam(opts.OwnerID, ctx.FormString("team"))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			if organization.IsErrTeamNotExist(err) {
 | 
			
		||||
				ctx.Error(http.StatusBadRequest, "Team not found", err.Error())
 | 
			
		||||
			} else {
 | 
			
		||||
				ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error())
 | 
			
		||||
			}
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		opts.TeamID = team.ID
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	repoIDs, _, err := models.SearchRepositoryIDs(opts)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.Error(http.StatusInternalServerError, "SearchRepositoryByName", err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var issues []*models.Issue
 | 
			
		||||
	var filteredCount int64
 | 
			
		||||
 | 
			
		||||
	keyword := ctx.FormTrim("q")
 | 
			
		||||
	if strings.IndexByte(keyword, 0) >= 0 {
 | 
			
		||||
		keyword = ""
 | 
			
		||||
	}
 | 
			
		||||
	var issueIDs []int64
 | 
			
		||||
	if len(keyword) > 0 && len(repoIDs) > 0 {
 | 
			
		||||
		if issueIDs, err = issue_indexer.SearchIssuesByKeyword(ctx, repoIDs, keyword); err != nil {
 | 
			
		||||
			ctx.Error(http.StatusInternalServerError, "SearchIssuesByKeyword", err.Error())
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var isPull util.OptionalBool
 | 
			
		||||
	switch ctx.FormString("type") {
 | 
			
		||||
	case "pulls":
 | 
			
		||||
		isPull = util.OptionalBoolTrue
 | 
			
		||||
	case "issues":
 | 
			
		||||
		isPull = util.OptionalBoolFalse
 | 
			
		||||
	default:
 | 
			
		||||
		isPull = util.OptionalBoolNone
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	labels := ctx.FormTrim("labels")
 | 
			
		||||
	var includedLabelNames []string
 | 
			
		||||
	if len(labels) > 0 {
 | 
			
		||||
		includedLabelNames = strings.Split(labels, ",")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	milestones := ctx.FormTrim("milestones")
 | 
			
		||||
	var includedMilestones []string
 | 
			
		||||
	if len(milestones) > 0 {
 | 
			
		||||
		includedMilestones = strings.Split(milestones, ",")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// this api is also used in UI,
 | 
			
		||||
	// so the default limit is set to fit UI needs
 | 
			
		||||
	limit := ctx.FormInt("limit")
 | 
			
		||||
	if limit == 0 {
 | 
			
		||||
		limit = setting.UI.IssuePagingNum
 | 
			
		||||
	} else if limit > setting.API.MaxResponseItems {
 | 
			
		||||
		limit = setting.API.MaxResponseItems
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Only fetch the issues if we either don't have a keyword or the search returned issues
 | 
			
		||||
	// This would otherwise return all issues if no issues were found by the search.
 | 
			
		||||
	if len(keyword) == 0 || len(issueIDs) > 0 || len(includedLabelNames) > 0 || len(includedMilestones) > 0 {
 | 
			
		||||
		issuesOpt := &models.IssuesOptions{
 | 
			
		||||
			ListOptions: db.ListOptions{
 | 
			
		||||
				Page:     ctx.FormInt("page"),
 | 
			
		||||
				PageSize: limit,
 | 
			
		||||
			},
 | 
			
		||||
			RepoIDs:            repoIDs,
 | 
			
		||||
			IsClosed:           isClosed,
 | 
			
		||||
			IssueIDs:           issueIDs,
 | 
			
		||||
			IncludedLabelNames: includedLabelNames,
 | 
			
		||||
			IncludeMilestones:  includedMilestones,
 | 
			
		||||
			SortType:           "priorityrepo",
 | 
			
		||||
			PriorityRepoID:     ctx.FormInt64("priority_repo_id"),
 | 
			
		||||
			IsPull:             isPull,
 | 
			
		||||
			UpdatedBeforeUnix:  before,
 | 
			
		||||
			UpdatedAfterUnix:   since,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ctxUserID := int64(0)
 | 
			
		||||
		if ctx.IsSigned {
 | 
			
		||||
			ctxUserID = ctx.Doer.ID
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Filter for: Created by User, Assigned to User, Mentioning User, Review of User Requested
 | 
			
		||||
		if ctx.FormBool("created") {
 | 
			
		||||
			issuesOpt.PosterID = ctxUserID
 | 
			
		||||
		}
 | 
			
		||||
		if ctx.FormBool("assigned") {
 | 
			
		||||
			issuesOpt.AssigneeID = ctxUserID
 | 
			
		||||
		}
 | 
			
		||||
		if ctx.FormBool("mentioned") {
 | 
			
		||||
			issuesOpt.MentionedID = ctxUserID
 | 
			
		||||
		}
 | 
			
		||||
		if ctx.FormBool("review_requested") {
 | 
			
		||||
			issuesOpt.ReviewRequestedID = ctxUserID
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if issues, err = models.Issues(issuesOpt); err != nil {
 | 
			
		||||
			ctx.Error(http.StatusInternalServerError, "Issues", err.Error())
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		issuesOpt.ListOptions = db.ListOptions{
 | 
			
		||||
			Page: -1,
 | 
			
		||||
		}
 | 
			
		||||
		if filteredCount, err = models.CountIssues(issuesOpt); err != nil {
 | 
			
		||||
			ctx.Error(http.StatusInternalServerError, "CountIssues", err.Error())
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.SetTotalCountHeader(filteredCount)
 | 
			
		||||
	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getUserIDForFilter(ctx *context.Context, queryName string) int64 {
 | 
			
		||||
	userName := ctx.FormString(queryName)
 | 
			
		||||
	if len(userName) == 0 {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user, err := user_model.GetUserByName(userName)
 | 
			
		||||
	if user_model.IsErrUserNotExist(err) {
 | 
			
		||||
		ctx.NotFound("", err)
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.Error(http.StatusInternalServerError, err.Error())
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return user.ID
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ListIssues list the issues of a repository
 | 
			
		||||
func ListIssues(ctx *context.Context) {
 | 
			
		||||
	before, since, err := context.GetQueryBeforeSince(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.Error(http.StatusUnprocessableEntity, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var isClosed util.OptionalBool
 | 
			
		||||
	switch ctx.FormString("state") {
 | 
			
		||||
	case "closed":
 | 
			
		||||
		isClosed = util.OptionalBoolTrue
 | 
			
		||||
	case "all":
 | 
			
		||||
		isClosed = util.OptionalBoolNone
 | 
			
		||||
	default:
 | 
			
		||||
		isClosed = util.OptionalBoolFalse
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var issues []*models.Issue
 | 
			
		||||
	var filteredCount int64
 | 
			
		||||
 | 
			
		||||
	keyword := ctx.FormTrim("q")
 | 
			
		||||
	if strings.IndexByte(keyword, 0) >= 0 {
 | 
			
		||||
		keyword = ""
 | 
			
		||||
	}
 | 
			
		||||
	var issueIDs []int64
 | 
			
		||||
	var labelIDs []int64
 | 
			
		||||
	if len(keyword) > 0 {
 | 
			
		||||
		issueIDs, err = issue_indexer.SearchIssuesByKeyword(ctx, []int64{ctx.Repo.Repository.ID}, keyword)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			ctx.Error(http.StatusInternalServerError, err.Error())
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
 | 
			
		||||
		labelIDs, err = models.GetLabelIDsInRepoByNames(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
 | 
			
		||||
			mile, err := models.GetMilestoneByRepoIDANDName(ctx.Repo.Repository.ID, part[i])
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				mileIDs = append(mileIDs, mile.ID)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			if !models.IsErrMilestoneNotExist(err) {
 | 
			
		||||
				ctx.Error(http.StatusInternalServerError, err.Error())
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			id, err := strconv.ParseInt(part[i], 10, 64)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			mile, err = models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, id)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				mileIDs = append(mileIDs, mile.ID)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			if models.IsErrMilestoneNotExist(err) {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			ctx.Error(http.StatusInternalServerError, err.Error())
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	listOptions := db.ListOptions{
 | 
			
		||||
		Page:     ctx.FormInt("page"),
 | 
			
		||||
		PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var isPull util.OptionalBool
 | 
			
		||||
	switch ctx.FormString("type") {
 | 
			
		||||
	case "pulls":
 | 
			
		||||
		isPull = util.OptionalBoolTrue
 | 
			
		||||
	case "issues":
 | 
			
		||||
		isPull = util.OptionalBoolFalse
 | 
			
		||||
	default:
 | 
			
		||||
		isPull = util.OptionalBoolNone
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// FIXME: we should be more efficient here
 | 
			
		||||
	createdByID := getUserIDForFilter(ctx, "created_by")
 | 
			
		||||
	if ctx.Written() {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	assignedByID := getUserIDForFilter(ctx, "assigned_by")
 | 
			
		||||
	if ctx.Written() {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	mentionedByID := getUserIDForFilter(ctx, "mentioned_by")
 | 
			
		||||
	if ctx.Written() {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Only fetch the issues if we either don't have a keyword or the search returned issues
 | 
			
		||||
	// This would otherwise return all issues if no issues were found by the search.
 | 
			
		||||
	if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
 | 
			
		||||
		issuesOpt := &models.IssuesOptions{
 | 
			
		||||
			ListOptions:       listOptions,
 | 
			
		||||
			RepoIDs:           []int64{ctx.Repo.Repository.ID},
 | 
			
		||||
			IsClosed:          isClosed,
 | 
			
		||||
			IssueIDs:          issueIDs,
 | 
			
		||||
			LabelIDs:          labelIDs,
 | 
			
		||||
			MilestoneIDs:      mileIDs,
 | 
			
		||||
			IsPull:            isPull,
 | 
			
		||||
			UpdatedBeforeUnix: before,
 | 
			
		||||
			UpdatedAfterUnix:  since,
 | 
			
		||||
			PosterID:          createdByID,
 | 
			
		||||
			AssigneeID:        assignedByID,
 | 
			
		||||
			MentionedID:       mentionedByID,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if issues, err = models.Issues(issuesOpt); err != nil {
 | 
			
		||||
			ctx.Error(http.StatusInternalServerError, err.Error())
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		issuesOpt.ListOptions = db.ListOptions{
 | 
			
		||||
			Page: -1,
 | 
			
		||||
		}
 | 
			
		||||
		if filteredCount, err = models.CountIssues(issuesOpt); err != nil {
 | 
			
		||||
			ctx.Error(http.StatusInternalServerError, err.Error())
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.SetTotalCountHeader(filteredCount)
 | 
			
		||||
	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateIssueStatus change issue's status
 | 
			
		||||
func UpdateIssueStatus(ctx *context.Context) {
 | 
			
		||||
	issues := getActionIssues(ctx)
 | 
			
		||||
 
 | 
			
		||||
@@ -20,11 +20,14 @@ import (
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/base"
 | 
			
		||||
	"code.gitea.io/gitea/modules/context"
 | 
			
		||||
	"code.gitea.io/gitea/modules/convert"
 | 
			
		||||
	"code.gitea.io/gitea/modules/graceful"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	repo_module "code.gitea.io/gitea/modules/repository"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/storage"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/modules/web"
 | 
			
		||||
	"code.gitea.io/gitea/services/forms"
 | 
			
		||||
	repo_service "code.gitea.io/gitea/services/repository"
 | 
			
		||||
@@ -503,3 +506,112 @@ func InitiateDownload(ctx *context.Context) {
 | 
			
		||||
		"complete": completed,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SearchRepo repositories via options
 | 
			
		||||
func SearchRepo(ctx *context.Context) {
 | 
			
		||||
	opts := &models.SearchRepoOptions{
 | 
			
		||||
		ListOptions: db.ListOptions{
 | 
			
		||||
			Page:     ctx.FormInt("page"),
 | 
			
		||||
			PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
 | 
			
		||||
		},
 | 
			
		||||
		Actor:              ctx.Doer,
 | 
			
		||||
		Keyword:            ctx.FormTrim("q"),
 | 
			
		||||
		OwnerID:            ctx.FormInt64("uid"),
 | 
			
		||||
		PriorityOwnerID:    ctx.FormInt64("priority_owner_id"),
 | 
			
		||||
		TeamID:             ctx.FormInt64("team_id"),
 | 
			
		||||
		TopicOnly:          ctx.FormBool("topic"),
 | 
			
		||||
		Collaborate:        util.OptionalBoolNone,
 | 
			
		||||
		Private:            ctx.IsSigned && (ctx.FormString("private") == "" || ctx.FormBool("private")),
 | 
			
		||||
		Template:           util.OptionalBoolNone,
 | 
			
		||||
		StarredByID:        ctx.FormInt64("starredBy"),
 | 
			
		||||
		IncludeDescription: ctx.FormBool("includeDesc"),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ctx.FormString("template") != "" {
 | 
			
		||||
		opts.Template = util.OptionalBoolOf(ctx.FormBool("template"))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ctx.FormBool("exclusive") {
 | 
			
		||||
		opts.Collaborate = util.OptionalBoolFalse
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	mode := ctx.FormString("mode")
 | 
			
		||||
	switch mode {
 | 
			
		||||
	case "source":
 | 
			
		||||
		opts.Fork = util.OptionalBoolFalse
 | 
			
		||||
		opts.Mirror = util.OptionalBoolFalse
 | 
			
		||||
	case "fork":
 | 
			
		||||
		opts.Fork = util.OptionalBoolTrue
 | 
			
		||||
	case "mirror":
 | 
			
		||||
		opts.Mirror = util.OptionalBoolTrue
 | 
			
		||||
	case "collaborative":
 | 
			
		||||
		opts.Mirror = util.OptionalBoolFalse
 | 
			
		||||
		opts.Collaborate = util.OptionalBoolTrue
 | 
			
		||||
	case "":
 | 
			
		||||
	default:
 | 
			
		||||
		ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid search mode: \"%s\"", mode))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ctx.FormString("archived") != "" {
 | 
			
		||||
		opts.Archived = util.OptionalBoolOf(ctx.FormBool("archived"))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ctx.FormString("is_private") != "" {
 | 
			
		||||
		opts.IsPrivate = util.OptionalBoolOf(ctx.FormBool("is_private"))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sortMode := ctx.FormString("sort")
 | 
			
		||||
	if len(sortMode) > 0 {
 | 
			
		||||
		sortOrder := ctx.FormString("order")
 | 
			
		||||
		if len(sortOrder) == 0 {
 | 
			
		||||
			sortOrder = "asc"
 | 
			
		||||
		}
 | 
			
		||||
		if searchModeMap, ok := context.SearchOrderByMap[sortOrder]; ok {
 | 
			
		||||
			if orderBy, ok := searchModeMap[sortMode]; ok {
 | 
			
		||||
				opts.OrderBy = orderBy
 | 
			
		||||
			} else {
 | 
			
		||||
				ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort mode: \"%s\"", sortMode))
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Invalid sort order: \"%s\"", sortOrder))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var err error
 | 
			
		||||
	repos, count, err := models.SearchRepository(opts)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.JSON(http.StatusInternalServerError, api.SearchError{
 | 
			
		||||
			OK:    false,
 | 
			
		||||
			Error: err.Error(),
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	results := make([]*api.Repository, len(repos))
 | 
			
		||||
	for i, repo := range repos {
 | 
			
		||||
		if err = repo.GetOwner(ctx); err != nil {
 | 
			
		||||
			ctx.JSON(http.StatusInternalServerError, api.SearchError{
 | 
			
		||||
				OK:    false,
 | 
			
		||||
				Error: err.Error(),
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		accessMode, err := models.AccessLevel(ctx.Doer, repo)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			ctx.JSON(http.StatusInternalServerError, api.SearchError{
 | 
			
		||||
				OK:    false,
 | 
			
		||||
				Error: err.Error(),
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
		results[i] = convert.ToRepo(repo, accessMode)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.SetTotalCountHeader(count)
 | 
			
		||||
	ctx.JSON(http.StatusOK, api.SearchResults{
 | 
			
		||||
		OK:   true,
 | 
			
		||||
		Data: results,
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/base"
 | 
			
		||||
	"code.gitea.io/gitea/modules/context"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
@@ -191,3 +192,8 @@ func NotificationPurgePost(c *context.Context) {
 | 
			
		||||
 | 
			
		||||
	c.Redirect(setting.AppSubURL+"/notifications", http.StatusSeeOther)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewAvailable returns the notification counts
 | 
			
		||||
func NewAvailable(ctx *context.APIContext) {
 | 
			
		||||
	ctx.JSON(http.StatusOK, api.NotificationCount{New: models.CountUnread(ctx.Doer)})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										44
									
								
								routers/web/user/search.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								routers/web/user/search.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
// Copyright 2022 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package user
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/context"
 | 
			
		||||
	"code.gitea.io/gitea/modules/convert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Search search users
 | 
			
		||||
func Search(ctx *context.Context) {
 | 
			
		||||
	listOptions := db.ListOptions{
 | 
			
		||||
		Page:     ctx.FormInt("page"),
 | 
			
		||||
		PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	users, maxResults, err := user_model.SearchUsers(&user_model.SearchUserOptions{
 | 
			
		||||
		Actor:       ctx.Doer,
 | 
			
		||||
		Keyword:     ctx.FormTrim("q"),
 | 
			
		||||
		UID:         ctx.FormInt64("uid"),
 | 
			
		||||
		Type:        user_model.UserTypeIndividual,
 | 
			
		||||
		ListOptions: listOptions,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
 | 
			
		||||
			"ok":    false,
 | 
			
		||||
			"error": err.Error(),
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.SetTotalCountHeader(maxResults)
 | 
			
		||||
 | 
			
		||||
	ctx.JSON(http.StatusOK, map[string]interface{}{
 | 
			
		||||
		"ok":   true,
 | 
			
		||||
		"data": convert.ToUsers(ctx.Doer, users),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										41
									
								
								routers/web/user/stop_watch.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								routers/web/user/stop_watch.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
// Copyright 2022 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package user
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models"
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/modules/context"
 | 
			
		||||
	"code.gitea.io/gitea/modules/convert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GetStopwatches get all stopwatches
 | 
			
		||||
func GetStopwatches(ctx *context.Context) {
 | 
			
		||||
	sws, err := models.GetUserStopwatches(ctx.Doer.ID, db.ListOptions{
 | 
			
		||||
		Page:     ctx.FormInt("page"),
 | 
			
		||||
		PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.Error(http.StatusInternalServerError, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	count, err := models.CountUserStopwatches(ctx.Doer.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.Error(http.StatusInternalServerError, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	apiSWs, err := convert.ToStopWatches(sws)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.Error(http.StatusInternalServerError, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.SetTotalCountHeader(count)
 | 
			
		||||
	ctx.JSON(http.StatusOK, apiSWs)
 | 
			
		||||
}
 | 
			
		||||
@@ -20,6 +20,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/public"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/storage"
 | 
			
		||||
	"code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/templates"
 | 
			
		||||
	"code.gitea.io/gitea/modules/validation"
 | 
			
		||||
	"code.gitea.io/gitea/modules/web"
 | 
			
		||||
@@ -289,8 +290,13 @@ func RegisterRoutes(m *web.Route) {
 | 
			
		||||
		m.Get("/users", explore.Users)
 | 
			
		||||
		m.Get("/organizations", explore.Organizations)
 | 
			
		||||
		m.Get("/code", explore.Code)
 | 
			
		||||
		m.Get("/topics/search", explore.TopicSearch)
 | 
			
		||||
	}, ignExploreSignIn)
 | 
			
		||||
	m.Get("/issues", reqSignIn, user.Issues)
 | 
			
		||||
	m.Group("/issues", func() {
 | 
			
		||||
		m.Get("", user.Issues)
 | 
			
		||||
		m.Get("/search", repo.SearchIssues)
 | 
			
		||||
	}, reqSignIn)
 | 
			
		||||
 | 
			
		||||
	m.Get("/pulls", reqSignIn, user.Pulls)
 | 
			
		||||
	m.Get("/milestones", reqSignIn, reqMilestonesDashboardPageEnabled, user.Milestones)
 | 
			
		||||
 | 
			
		||||
@@ -421,6 +427,8 @@ func RegisterRoutes(m *web.Route) {
 | 
			
		||||
		m.Post("/forgot_password", auth.ForgotPasswdPost)
 | 
			
		||||
		m.Post("/logout", auth.SignOut)
 | 
			
		||||
		m.Get("/task/{task}", user.TaskStatus)
 | 
			
		||||
		m.Get("/stopwatches", user.GetStopwatches, reqSignIn)
 | 
			
		||||
		m.Get("/search", user.Search, ignExploreSignIn)
 | 
			
		||||
	})
 | 
			
		||||
	// ***** END: User *****
 | 
			
		||||
 | 
			
		||||
@@ -605,6 +613,7 @@ func RegisterRoutes(m *web.Route) {
 | 
			
		||||
		m.Group("/{org}", func() {
 | 
			
		||||
			m.Get("/teams/new", org.NewTeam)
 | 
			
		||||
			m.Post("/teams/new", bindIgnErr(forms.CreateTeamForm{}), org.NewTeamPost)
 | 
			
		||||
			m.Get("/teams/-/search", org.SearchTeam)
 | 
			
		||||
			m.Get("/teams/{team}/edit", org.EditTeam)
 | 
			
		||||
			m.Post("/teams/{team}/edit", bindIgnErr(forms.CreateTeamForm{}), org.EditTeamPost)
 | 
			
		||||
			m.Post("/teams/{team}/delete", org.DeleteTeam)
 | 
			
		||||
@@ -669,6 +678,7 @@ func RegisterRoutes(m *web.Route) {
 | 
			
		||||
			m.Combo("/{repoid}").Get(repo.Fork).
 | 
			
		||||
				Post(bindIgnErr(forms.CreateRepoForm{}), repo.ForkPost)
 | 
			
		||||
		}, context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader)
 | 
			
		||||
		m.Get("/search", repo.SearchRepo)
 | 
			
		||||
	}, reqSignIn)
 | 
			
		||||
 | 
			
		||||
	m.Group("/{username}/-", func() {
 | 
			
		||||
@@ -811,13 +821,16 @@ func RegisterRoutes(m *web.Route) {
 | 
			
		||||
					Post(bindIgnErr(forms.CreateIssueForm{}), repo.NewIssuePost)
 | 
			
		||||
				m.Get("/choose", context.RepoRef(), repo.NewIssueChooseTemplate)
 | 
			
		||||
			})
 | 
			
		||||
			m.Get("/search", repo.ListIssues)
 | 
			
		||||
		}, context.RepoMustNotBeArchived(), reqRepoIssueReader)
 | 
			
		||||
		// FIXME: should use different URLs but mostly same logic for comments of issue and pull request.
 | 
			
		||||
		// So they can apply their own enable/disable logic on routers.
 | 
			
		||||
		m.Group("/{type:issues|pulls}", func() {
 | 
			
		||||
			m.Group("/{index}", func() {
 | 
			
		||||
				m.Get("/info", repo.GetIssueInfo)
 | 
			
		||||
				m.Post("/title", repo.UpdateIssueTitle)
 | 
			
		||||
				m.Post("/content", repo.UpdateIssueContent)
 | 
			
		||||
				m.Post("/deadline", bindIgnErr(structs.EditDeadlineOption{}), repo.UpdateIssueDeadline)
 | 
			
		||||
				m.Post("/watch", repo.IssueWatch)
 | 
			
		||||
				m.Post("/ref", repo.UpdateIssueRef)
 | 
			
		||||
				m.Group("/dependency", func() {
 | 
			
		||||
@@ -1195,6 +1208,7 @@ func RegisterRoutes(m *web.Route) {
 | 
			
		||||
		m.Get("", user.Notifications)
 | 
			
		||||
		m.Post("/status", user.NotificationStatusPost)
 | 
			
		||||
		m.Post("/purge", user.NotificationPurgePost)
 | 
			
		||||
		m.Get("/new", user.NewAvailable)
 | 
			
		||||
	}, reqSignIn)
 | 
			
		||||
 | 
			
		||||
	if setting.API.EnableSwagger {
 | 
			
		||||
 
 | 
			
		||||
@@ -429,7 +429,7 @@
 | 
			
		||||
 | 
			
		||||
			{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
 | 
			
		||||
				<div {{if ne .Issue.DeadlineUnix 0}} style="display: none;"{{end}} id="deadlineForm">
 | 
			
		||||
					<form class="ui fluid action input issue-due-form" action="{{AppSubUrl}}/api/v1/repos/{{PathEscape .Repository.Owner.Name}}/{{PathEscape .Repository.Name}}/issues/{{.Issue.Index}}" method="post" id="update-issue-deadline-form">
 | 
			
		||||
					<form class="ui fluid action input issue-due-form" action="{{AppSubUrl}}/{{PathEscape .Repository.Owner.Name}}/{{PathEscape .Repository.Name}}/issues/{{.Issue.Index}}/deadline" method="post" id="update-issue-deadline-form">
 | 
			
		||||
						{{$.CsrfTokenHtml}}
 | 
			
		||||
						<input required placeholder="{{.i18n.Tr "repo.issues.due_date_form"}}" {{if gt .Issue.DeadlineUnix 0}}value="{{.Issue.DeadlineUnix.Format "2006-01-02"}}"{{end}} type="date" name="deadlineDate" id="deadlineDate">
 | 
			
		||||
						<button class="ui green icon button">
 | 
			
		||||
 
 | 
			
		||||
@@ -120,7 +120,7 @@ export default {
 | 
			
		||||
    load(data, callback) {
 | 
			
		||||
      this.loading = true;
 | 
			
		||||
      this.i18nErrorMessage = null;
 | 
			
		||||
      $.get(`${appSubUrl}/api/v1/repos/${data.owner}/${data.repo}/issues/${data.index}`).done((issue) => {
 | 
			
		||||
      $.get(`${appSubUrl}/${data.owner}/${data.repo}/issues/${data.index}/info`).done((issue) => {
 | 
			
		||||
        this.issue = issue;
 | 
			
		||||
      }).fail((jqXHR) => {
 | 
			
		||||
        if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
 | 
			
		||||
 
 | 
			
		||||
@@ -124,7 +124,7 @@ function initVueComponents() {
 | 
			
		||||
        return this.repos.length > 0 && this.repos.length < this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`];
 | 
			
		||||
      },
 | 
			
		||||
      searchURL() {
 | 
			
		||||
        return `${this.subUrl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=${this.searchQuery
 | 
			
		||||
        return `${this.subUrl}/repo/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=${this.searchQuery
 | 
			
		||||
        }&page=${this.page}&limit=${this.searchLimit}&mode=${this.repoTypes[this.reposFilter].searchMode
 | 
			
		||||
        }${this.reposFilter !== 'all' ? '&exclusive=1' : ''
 | 
			
		||||
        }${this.archivedFilter === 'archived' ? '&archived=true' : ''}${this.archivedFilter === 'unarchived' ? '&archived=false' : ''
 | 
			
		||||
@@ -302,7 +302,7 @@ function initVueComponents() {
 | 
			
		||||
        this.isLoading = true;
 | 
			
		||||
 | 
			
		||||
        if (!this.reposTotalCount) {
 | 
			
		||||
          const totalCountSearchURL = `${this.subUrl}/api/v1/repos/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`;
 | 
			
		||||
          const totalCountSearchURL = `${this.subUrl}/repo/search?sort=updated&order=desc&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`;
 | 
			
		||||
          $.getJSON(totalCountSearchURL, (_result, _textStatus, request) => {
 | 
			
		||||
            this.reposTotalCount = request.getResponseHeader('X-Total-Count');
 | 
			
		||||
          });
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ export function initCompSearchUserBox() {
 | 
			
		||||
  $searchUserBox.search({
 | 
			
		||||
    minCharacters: 2,
 | 
			
		||||
    apiSettings: {
 | 
			
		||||
      url: `${appSubUrl}/api/v1/users/search?q={query}`,
 | 
			
		||||
      url: `${appSubUrl}/user/search?q={query}`,
 | 
			
		||||
      onResponse(response) {
 | 
			
		||||
        const items = [];
 | 
			
		||||
        const searchQueryUppercase = $searchUserBox.find('input').val().toUpperCase();
 | 
			
		||||
 
 | 
			
		||||
@@ -158,7 +158,7 @@ async function updateNotificationTable() {
 | 
			
		||||
async function updateNotificationCount() {
 | 
			
		||||
  const data = await $.ajax({
 | 
			
		||||
    type: 'GET',
 | 
			
		||||
    url: `${appSubUrl}/api/v1/notifications/new`,
 | 
			
		||||
    url: `${appSubUrl}/notifications/new`,
 | 
			
		||||
    headers: {
 | 
			
		||||
      'X-Csrf-Token': csrfToken,
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ export function initOrgTeamSearchRepoBox() {
 | 
			
		||||
  $searchRepoBox.search({
 | 
			
		||||
    minCharacters: 2,
 | 
			
		||||
    apiSettings: {
 | 
			
		||||
      url: `${appSubUrl}/api/v1/repos/search?q={query}&uid=${$searchRepoBox.data('uid')}`,
 | 
			
		||||
      url: `${appSubUrl}/repo/search?q={query}&uid=${$searchRepoBox.data('uid')}`,
 | 
			
		||||
      onResponse(response) {
 | 
			
		||||
        const items = [];
 | 
			
		||||
        $.each(response.data, (_i, item) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -91,7 +91,7 @@ export function initRepoTopicBar() {
 | 
			
		||||
      label: 'ui small label'
 | 
			
		||||
    },
 | 
			
		||||
    apiSettings: {
 | 
			
		||||
      url: `${appSubUrl}/api/v1/topics/search?q={query}`,
 | 
			
		||||
      url: `${appSubUrl}/explore/topics/search?q={query}`,
 | 
			
		||||
      throttle: 500,
 | 
			
		||||
      cache: false,
 | 
			
		||||
      onResponse(res) {
 | 
			
		||||
 
 | 
			
		||||
@@ -54,7 +54,7 @@ function updateDeadline(deadlineString) {
 | 
			
		||||
    realDeadline = new Date(newDate);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $.ajax(`${$('#update-issue-deadline-form').attr('action')}/deadline`, {
 | 
			
		||||
  $.ajax(`${$('#update-issue-deadline-form').attr('action')}`, {
 | 
			
		||||
    data: JSON.stringify({
 | 
			
		||||
      due_date: realDeadline,
 | 
			
		||||
    }),
 | 
			
		||||
@@ -91,9 +91,9 @@ export function initRepoIssueList() {
 | 
			
		||||
  const repoId = $('#repoId').val();
 | 
			
		||||
  const crossRepoSearch = $('#crossRepoSearch').val();
 | 
			
		||||
  const tp = $('#type').val();
 | 
			
		||||
  let issueSearchUrl = `${appSubUrl}/api/v1/repos/${repolink}/issues?q={query}&type=${tp}`;
 | 
			
		||||
  let issueSearchUrl = `${appSubUrl}/${repolink}/issues/search?q={query}&type=${tp}`;
 | 
			
		||||
  if (crossRepoSearch === 'true') {
 | 
			
		||||
    issueSearchUrl = `${appSubUrl}/api/v1/repos/issues/search?q={query}&priority_repo_id=${repoId}&type=${tp}`;
 | 
			
		||||
    issueSearchUrl = `${appSubUrl}/issues/search?q={query}&priority_repo_id=${repoId}&type=${tp}`;
 | 
			
		||||
  }
 | 
			
		||||
  $('#new-dependency-drop-list')
 | 
			
		||||
    .dropdown({
 | 
			
		||||
@@ -292,7 +292,7 @@ export function initRepoIssueReferenceRepositorySearch() {
 | 
			
		||||
  $('.issue_reference_repository_search')
 | 
			
		||||
    .dropdown({
 | 
			
		||||
      apiSettings: {
 | 
			
		||||
        url: `${appSubUrl}/api/v1/repos/search?q={query}&limit=20`,
 | 
			
		||||
        url: `${appSubUrl}/repo/search?q={query}&limit=20`,
 | 
			
		||||
        onResponse(response) {
 | 
			
		||||
          const filteredResponse = {success: true, results: []};
 | 
			
		||||
          $.each(response.data, (_r, repo) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ export function initRepoSettingSearchTeamBox() {
 | 
			
		||||
  $searchTeamBox.search({
 | 
			
		||||
    minCharacters: 2,
 | 
			
		||||
    apiSettings: {
 | 
			
		||||
      url: `${appSubUrl}/api/v1/orgs/${$searchTeamBox.data('org')}/teams/search?q={query}`,
 | 
			
		||||
      url: `${appSubUrl}/org/${$searchTeamBox.data('org')}/teams/-/search?q={query}`,
 | 
			
		||||
      headers: {'X-Csrf-Token': csrfToken},
 | 
			
		||||
      onResponse(response) {
 | 
			
		||||
        const items = [];
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ export function initRepoTemplateSearch() {
 | 
			
		||||
    $('#repo_template_search')
 | 
			
		||||
      .dropdown({
 | 
			
		||||
        apiSettings: {
 | 
			
		||||
          url: `${appSubUrl}/api/v1/repos/search?q={query}&template=true&priority_owner_id=${$('#uid').val()}`,
 | 
			
		||||
          url: `${appSubUrl}/repo/search?q={query}&template=true&priority_owner_id=${$('#uid').val()}`,
 | 
			
		||||
          onResponse(response) {
 | 
			
		||||
            const filteredResponse = {success: true, results: []};
 | 
			
		||||
            filteredResponse.results.push({
 | 
			
		||||
 
 | 
			
		||||
@@ -111,7 +111,7 @@ async function updateStopwatchWithCallback(callback, timeout) {
 | 
			
		||||
async function updateStopwatch() {
 | 
			
		||||
  const data = await $.ajax({
 | 
			
		||||
    type: 'GET',
 | 
			
		||||
    url: `${appSubUrl}/api/v1/user/stopwatches`,
 | 
			
		||||
    url: `${appSubUrl}/user/stopwatches`,
 | 
			
		||||
    headers: {'X-Csrf-Token': csrfToken},
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user