mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09: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 ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
| 	"path" | 	"path" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| @@ -20,6 +21,7 @@ import ( | |||||||
| 	"code.gitea.io/gitea/modules/indexer/issues" | 	"code.gitea.io/gitea/modules/indexer/issues" | ||||||
| 	"code.gitea.io/gitea/modules/references" | 	"code.gitea.io/gitea/modules/references" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	api "code.gitea.io/gitea/modules/structs" | ||||||
| 	"code.gitea.io/gitea/modules/test" | 	"code.gitea.io/gitea/modules/test" | ||||||
|  |  | ||||||
| 	"github.com/PuerkitoBio/goquery" | 	"github.com/PuerkitoBio/goquery" | ||||||
| @@ -347,3 +349,209 @@ func TestIssueRedirect(t *testing.T) { | |||||||
| 	resp = session.MakeRequest(t, req, http.StatusSeeOther) | 	resp = session.MakeRequest(t, req, http.StatusSeeOther) | ||||||
| 	assert.Equal(t, "/"+path.Join("org26", "repo_external_tracker_alpha", "pulls", "1"), test.RedirectURL(resp)) | 	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" | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/models/unittest" | ||||||
|  | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	api "code.gitea.io/gitea/modules/structs" | 	api "code.gitea.io/gitea/modules/structs" | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| @@ -173,3 +175,30 @@ func TestOrgRestrictedUser(t *testing.T) { | |||||||
| 	req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s", orgName, repoName)) | 	req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s", orgName, repoName)) | ||||||
| 	restrictedSession.MakeRequest(t, req, http.StatusOK) | 	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" | 	"net/http" | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/models" | ||||||
|  | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	"code.gitea.io/gitea/models/unittest" | 	"code.gitea.io/gitea/models/unittest" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	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/test" | ||||||
| 	"code.gitea.io/gitea/modules/translation/i18n" | 	"code.gitea.io/gitea/modules/translation/i18n" | ||||||
|  |  | ||||||
| @@ -222,3 +225,26 @@ func testExportUserGPGKeys(t *testing.T, user, expected string) { | |||||||
| 	// t.Log(resp.Body.String()) | 	// t.Log(resp.Body.String()) | ||||||
| 	assert.Equal(t, expected, 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 | // RequireCSRF requires a validated a CSRF token | ||||||
| func (ctx *APIContext) RequireCSRF() { | func (ctx *APIContext) RequireCSRF() { | ||||||
| 	headerToken := ctx.Req.Header.Get(ctx.csrf.GetHeaderName()) | 	headerToken := ctx.Req.Header.Get(ctx.csrf.GetHeaderName()) | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ import ( | |||||||
| 	"crypto/sha256" | 	"crypto/sha256" | ||||||
| 	"encoding/hex" | 	"encoding/hex" | ||||||
| 	"errors" | 	"errors" | ||||||
|  | 	"fmt" | ||||||
| 	"html" | 	"html" | ||||||
| 	"html/template" | 	"html/template" | ||||||
| 	"io" | 	"io" | ||||||
| @@ -21,6 +22,7 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/models/db" | ||||||
| 	"code.gitea.io/gitea/models/unit" | 	"code.gitea.io/gitea/models/unit" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/base" | 	"code.gitea.io/gitea/modules/base" | ||||||
| @@ -577,6 +579,22 @@ func (ctx *Context) Value(key interface{}) interface{} { | |||||||
| 	return ctx.Req.Context().Value(key) | 	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 | // Handler represents a custom handler | ||||||
| type Handler func(*Context) | 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 | // Use of this source code is governed by a MIT-style | ||||||
| // license that can be found in the LICENSE file. | // license that can be found in the LICENSE file. | ||||||
| 
 | 
 | ||||||
| package utils | package context | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"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 | // 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") | 	qCreatedBefore, err := prepareQueryArg(ctx, "before") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return 0, 0, err | 		return 0, 0, err | ||||||
| @@ -53,16 +49,8 @@ func parseTime(value string) (int64, error) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // prepareQueryArg unescape and trim a query arg | // 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, err = url.PathUnescape(ctx.FormString(name)) | ||||||
| 	value = strings.TrimSpace(value) | 	value = strings.TrimSpace(value) | ||||||
| 	return | 	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 { | func getFindNotificationOptions(ctx *context.APIContext) *models.FindNotificationOptions { | ||||||
| 	before, since, err := utils.GetQueryBeforeSince(ctx) | 	before, since, err := context.GetQueryBeforeSince(ctx.Context) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | ||||||
| 		return nil | 		return nil | ||||||
|   | |||||||
| @@ -111,7 +111,7 @@ func SearchIssues(ctx *context.APIContext) { | |||||||
| 	//   "200": | 	//   "200": | ||||||
| 	//     "$ref": "#/responses/IssueList" | 	//     "$ref": "#/responses/IssueList" | ||||||
|  |  | ||||||
| 	before, since, err := utils.GetQueryBeforeSince(ctx) | 	before, since, err := context.GetQueryBeforeSince(ctx.Context) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | ||||||
| 		return | 		return | ||||||
| @@ -359,7 +359,7 @@ func ListIssues(ctx *context.APIContext) { | |||||||
| 	// responses: | 	// responses: | ||||||
| 	//   "200": | 	//   "200": | ||||||
| 	//     "$ref": "#/responses/IssueList" | 	//     "$ref": "#/responses/IssueList" | ||||||
| 	before, since, err := utils.GetQueryBeforeSince(ctx) | 	before, since, err := context.GetQueryBeforeSince(ctx.Context) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | ||||||
| 		return | 		return | ||||||
|   | |||||||
| @@ -58,7 +58,7 @@ func ListIssueComments(ctx *context.APIContext) { | |||||||
| 	//   "200": | 	//   "200": | ||||||
| 	//     "$ref": "#/responses/CommentList" | 	//     "$ref": "#/responses/CommentList" | ||||||
|  |  | ||||||
| 	before, since, err := utils.GetQueryBeforeSince(ctx) | 	before, since, err := context.GetQueryBeforeSince(ctx.Context) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | ||||||
| 		return | 		return | ||||||
| @@ -150,7 +150,7 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) { | |||||||
| 	//   "200": | 	//   "200": | ||||||
| 	//     "$ref": "#/responses/TimelineList" | 	//     "$ref": "#/responses/TimelineList" | ||||||
|  |  | ||||||
| 	before, since, err := utils.GetQueryBeforeSince(ctx) | 	before, since, err := context.GetQueryBeforeSince(ctx.Context) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | ||||||
| 		return | 		return | ||||||
| @@ -253,7 +253,7 @@ func ListRepoIssueComments(ctx *context.APIContext) { | |||||||
| 	//   "200": | 	//   "200": | ||||||
| 	//     "$ref": "#/responses/CommentList" | 	//     "$ref": "#/responses/CommentList" | ||||||
|  |  | ||||||
| 	before, since, err := utils.GetQueryBeforeSince(ctx) | 	before, since, err := context.GetQueryBeforeSince(ctx.Context) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | ||||||
| 		return | 		return | ||||||
|   | |||||||
| @@ -103,7 +103,7 @@ func ListTrackedTimes(ctx *context.APIContext) { | |||||||
| 		opts.UserID = user.ID | 		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) | 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| @@ -522,7 +522,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var err error | 	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) | 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| @@ -597,7 +597,7 @@ func ListMyTrackedTimes(ctx *context.APIContext) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var err error | 	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) | 		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -31,23 +31,6 @@ import ( | |||||||
| 	repo_service "code.gitea.io/gitea/services/repository" | 	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 | // Search repositories via options | ||||||
| func Search(ctx *context.APIContext) { | func Search(ctx *context.APIContext) { | ||||||
| 	// swagger:operation GET /repos/search repository repoSearch | 	// swagger:operation GET /repos/search repository repoSearch | ||||||
| @@ -193,7 +176,7 @@ func Search(ctx *context.APIContext) { | |||||||
| 		if len(sortOrder) == 0 { | 		if len(sortOrder) == 0 { | ||||||
| 			sortOrder = "asc" | 			sortOrder = "asc" | ||||||
| 		} | 		} | ||||||
| 		if searchModeMap, ok := searchOrderByMap[sortOrder]; ok { | 		if searchModeMap, ok := context.SearchOrderByMap[sortOrder]; ok { | ||||||
| 			if orderBy, ok := searchModeMap[sortMode]; ok { | 			if orderBy, ok := searchModeMap[sortMode]; ok { | ||||||
| 				opts.OrderBy = orderBy | 				opts.OrderBy = orderBy | ||||||
| 			} else { | 			} 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" | 	"strings" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
|  | 	"code.gitea.io/gitea/models/db" | ||||||
| 	"code.gitea.io/gitea/models/organization" | 	"code.gitea.io/gitea/models/organization" | ||||||
| 	"code.gitea.io/gitea/models/perm" | 	"code.gitea.io/gitea/models/perm" | ||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| @@ -20,7 +21,9 @@ import ( | |||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/base" | 	"code.gitea.io/gitea/modules/base" | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
|  | 	"code.gitea.io/gitea/modules/convert" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
|  | 	api "code.gitea.io/gitea/modules/structs" | ||||||
| 	"code.gitea.io/gitea/modules/web" | 	"code.gitea.io/gitea/modules/web" | ||||||
| 	"code.gitea.io/gitea/routers/utils" | 	"code.gitea.io/gitea/routers/utils" | ||||||
| 	"code.gitea.io/gitea/services/forms" | 	"code.gitea.io/gitea/services/forms" | ||||||
| @@ -329,6 +332,51 @@ func TeamRepositories(ctx *context.Context) { | |||||||
| 	ctx.HTML(http.StatusOK, tplTeamRepositories) | 	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 | // EditTeam render team edit page | ||||||
| func EditTeam(ctx *context.Context) { | func EditTeam(ctx *context.Context) { | ||||||
| 	ctx.Data["Title"] = ctx.Org.Organization.FullName | 	ctx.Data["Title"] = ctx.Org.Organization.FullName | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ import ( | |||||||
| 	"path" | 	"path" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| @@ -36,6 +37,7 @@ import ( | |||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	api "code.gitea.io/gitea/modules/structs" | 	api "code.gitea.io/gitea/modules/structs" | ||||||
| 	"code.gitea.io/gitea/modules/templates/vars" | 	"code.gitea.io/gitea/modules/templates/vars" | ||||||
|  | 	"code.gitea.io/gitea/modules/timeutil" | ||||||
| 	"code.gitea.io/gitea/modules/upload" | 	"code.gitea.io/gitea/modules/upload" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
| 	"code.gitea.io/gitea/modules/web" | 	"code.gitea.io/gitea/modules/web" | ||||||
| @@ -1762,6 +1764,20 @@ func getActionIssues(ctx *context.Context) []*models.Issue { | |||||||
| 	return issues | 	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 | // UpdateIssueTitle change issue's title | ||||||
| func UpdateIssueTitle(ctx *context.Context) { | func UpdateIssueTitle(ctx *context.Context) { | ||||||
| 	issue := GetActionIssue(ctx) | 	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 | // UpdateIssueMilestone change issue's milestone | ||||||
| func UpdateIssueMilestone(ctx *context.Context) { | func UpdateIssueMilestone(ctx *context.Context) { | ||||||
| 	issues := getActionIssues(ctx) | 	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 | // UpdateIssueStatus change issue's status | ||||||
| func UpdateIssueStatus(ctx *context.Context) { | func UpdateIssueStatus(ctx *context.Context) { | ||||||
| 	issues := getActionIssues(ctx) | 	issues := getActionIssues(ctx) | ||||||
|   | |||||||
| @@ -20,11 +20,14 @@ import ( | |||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/base" | 	"code.gitea.io/gitea/modules/base" | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
|  | 	"code.gitea.io/gitea/modules/convert" | ||||||
| 	"code.gitea.io/gitea/modules/graceful" | 	"code.gitea.io/gitea/modules/graceful" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	repo_module "code.gitea.io/gitea/modules/repository" | 	repo_module "code.gitea.io/gitea/modules/repository" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/storage" | 	"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/modules/web" | ||||||
| 	"code.gitea.io/gitea/services/forms" | 	"code.gitea.io/gitea/services/forms" | ||||||
| 	repo_service "code.gitea.io/gitea/services/repository" | 	repo_service "code.gitea.io/gitea/services/repository" | ||||||
| @@ -503,3 +506,112 @@ func InitiateDownload(ctx *context.Context) { | |||||||
| 		"complete": completed, | 		"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/base" | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	api "code.gitea.io/gitea/modules/structs" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| @@ -191,3 +192,8 @@ func NotificationPurgePost(c *context.Context) { | |||||||
|  |  | ||||||
| 	c.Redirect(setting.AppSubURL+"/notifications", http.StatusSeeOther) | 	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/public" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/storage" | 	"code.gitea.io/gitea/modules/storage" | ||||||
|  | 	"code.gitea.io/gitea/modules/structs" | ||||||
| 	"code.gitea.io/gitea/modules/templates" | 	"code.gitea.io/gitea/modules/templates" | ||||||
| 	"code.gitea.io/gitea/modules/validation" | 	"code.gitea.io/gitea/modules/validation" | ||||||
| 	"code.gitea.io/gitea/modules/web" | 	"code.gitea.io/gitea/modules/web" | ||||||
| @@ -289,8 +290,13 @@ func RegisterRoutes(m *web.Route) { | |||||||
| 		m.Get("/users", explore.Users) | 		m.Get("/users", explore.Users) | ||||||
| 		m.Get("/organizations", explore.Organizations) | 		m.Get("/organizations", explore.Organizations) | ||||||
| 		m.Get("/code", explore.Code) | 		m.Get("/code", explore.Code) | ||||||
|  | 		m.Get("/topics/search", explore.TopicSearch) | ||||||
| 	}, ignExploreSignIn) | 	}, 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("/pulls", reqSignIn, user.Pulls) | ||||||
| 	m.Get("/milestones", reqSignIn, reqMilestonesDashboardPageEnabled, user.Milestones) | 	m.Get("/milestones", reqSignIn, reqMilestonesDashboardPageEnabled, user.Milestones) | ||||||
|  |  | ||||||
| @@ -421,6 +427,8 @@ func RegisterRoutes(m *web.Route) { | |||||||
| 		m.Post("/forgot_password", auth.ForgotPasswdPost) | 		m.Post("/forgot_password", auth.ForgotPasswdPost) | ||||||
| 		m.Post("/logout", auth.SignOut) | 		m.Post("/logout", auth.SignOut) | ||||||
| 		m.Get("/task/{task}", user.TaskStatus) | 		m.Get("/task/{task}", user.TaskStatus) | ||||||
|  | 		m.Get("/stopwatches", user.GetStopwatches, reqSignIn) | ||||||
|  | 		m.Get("/search", user.Search, ignExploreSignIn) | ||||||
| 	}) | 	}) | ||||||
| 	// ***** END: User ***** | 	// ***** END: User ***** | ||||||
|  |  | ||||||
| @@ -605,6 +613,7 @@ func RegisterRoutes(m *web.Route) { | |||||||
| 		m.Group("/{org}", func() { | 		m.Group("/{org}", func() { | ||||||
| 			m.Get("/teams/new", org.NewTeam) | 			m.Get("/teams/new", org.NewTeam) | ||||||
| 			m.Post("/teams/new", bindIgnErr(forms.CreateTeamForm{}), org.NewTeamPost) | 			m.Post("/teams/new", bindIgnErr(forms.CreateTeamForm{}), org.NewTeamPost) | ||||||
|  | 			m.Get("/teams/-/search", org.SearchTeam) | ||||||
| 			m.Get("/teams/{team}/edit", org.EditTeam) | 			m.Get("/teams/{team}/edit", org.EditTeam) | ||||||
| 			m.Post("/teams/{team}/edit", bindIgnErr(forms.CreateTeamForm{}), org.EditTeamPost) | 			m.Post("/teams/{team}/edit", bindIgnErr(forms.CreateTeamForm{}), org.EditTeamPost) | ||||||
| 			m.Post("/teams/{team}/delete", org.DeleteTeam) | 			m.Post("/teams/{team}/delete", org.DeleteTeam) | ||||||
| @@ -669,6 +678,7 @@ func RegisterRoutes(m *web.Route) { | |||||||
| 			m.Combo("/{repoid}").Get(repo.Fork). | 			m.Combo("/{repoid}").Get(repo.Fork). | ||||||
| 				Post(bindIgnErr(forms.CreateRepoForm{}), repo.ForkPost) | 				Post(bindIgnErr(forms.CreateRepoForm{}), repo.ForkPost) | ||||||
| 		}, context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader) | 		}, context.RepoIDAssignment(), context.UnitTypes(), reqRepoCodeReader) | ||||||
|  | 		m.Get("/search", repo.SearchRepo) | ||||||
| 	}, reqSignIn) | 	}, reqSignIn) | ||||||
|  |  | ||||||
| 	m.Group("/{username}/-", func() { | 	m.Group("/{username}/-", func() { | ||||||
| @@ -811,13 +821,16 @@ func RegisterRoutes(m *web.Route) { | |||||||
| 					Post(bindIgnErr(forms.CreateIssueForm{}), repo.NewIssuePost) | 					Post(bindIgnErr(forms.CreateIssueForm{}), repo.NewIssuePost) | ||||||
| 				m.Get("/choose", context.RepoRef(), repo.NewIssueChooseTemplate) | 				m.Get("/choose", context.RepoRef(), repo.NewIssueChooseTemplate) | ||||||
| 			}) | 			}) | ||||||
|  | 			m.Get("/search", repo.ListIssues) | ||||||
| 		}, context.RepoMustNotBeArchived(), reqRepoIssueReader) | 		}, context.RepoMustNotBeArchived(), reqRepoIssueReader) | ||||||
| 		// FIXME: should use different URLs but mostly same logic for comments of issue and pull request. | 		// 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. | 		// So they can apply their own enable/disable logic on routers. | ||||||
| 		m.Group("/{type:issues|pulls}", func() { | 		m.Group("/{type:issues|pulls}", func() { | ||||||
| 			m.Group("/{index}", func() { | 			m.Group("/{index}", func() { | ||||||
|  | 				m.Get("/info", repo.GetIssueInfo) | ||||||
| 				m.Post("/title", repo.UpdateIssueTitle) | 				m.Post("/title", repo.UpdateIssueTitle) | ||||||
| 				m.Post("/content", repo.UpdateIssueContent) | 				m.Post("/content", repo.UpdateIssueContent) | ||||||
|  | 				m.Post("/deadline", bindIgnErr(structs.EditDeadlineOption{}), repo.UpdateIssueDeadline) | ||||||
| 				m.Post("/watch", repo.IssueWatch) | 				m.Post("/watch", repo.IssueWatch) | ||||||
| 				m.Post("/ref", repo.UpdateIssueRef) | 				m.Post("/ref", repo.UpdateIssueRef) | ||||||
| 				m.Group("/dependency", func() { | 				m.Group("/dependency", func() { | ||||||
| @@ -1195,6 +1208,7 @@ func RegisterRoutes(m *web.Route) { | |||||||
| 		m.Get("", user.Notifications) | 		m.Get("", user.Notifications) | ||||||
| 		m.Post("/status", user.NotificationStatusPost) | 		m.Post("/status", user.NotificationStatusPost) | ||||||
| 		m.Post("/purge", user.NotificationPurgePost) | 		m.Post("/purge", user.NotificationPurgePost) | ||||||
|  | 		m.Get("/new", user.NewAvailable) | ||||||
| 	}, reqSignIn) | 	}, reqSignIn) | ||||||
|  |  | ||||||
| 	if setting.API.EnableSwagger { | 	if setting.API.EnableSwagger { | ||||||
|   | |||||||
| @@ -429,7 +429,7 @@ | |||||||
|  |  | ||||||
| 			{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}} | 			{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}} | ||||||
| 				<div {{if ne .Issue.DeadlineUnix 0}} style="display: none;"{{end}} id="deadlineForm"> | 				<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}} | 						{{$.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"> | 						<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"> | 						<button class="ui green icon button"> | ||||||
|   | |||||||
| @@ -120,7 +120,7 @@ export default { | |||||||
|     load(data, callback) { |     load(data, callback) { | ||||||
|       this.loading = true; |       this.loading = true; | ||||||
|       this.i18nErrorMessage = null; |       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; |         this.issue = issue; | ||||||
|       }).fail((jqXHR) => { |       }).fail((jqXHR) => { | ||||||
|         if (jqXHR.responseJSON && jqXHR.responseJSON.message) { |         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}`]; |         return this.repos.length > 0 && this.repos.length < this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`]; | ||||||
|       }, |       }, | ||||||
|       searchURL() { |       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 |         }&page=${this.page}&limit=${this.searchLimit}&mode=${this.repoTypes[this.reposFilter].searchMode | ||||||
|         }${this.reposFilter !== 'all' ? '&exclusive=1' : '' |         }${this.reposFilter !== 'all' ? '&exclusive=1' : '' | ||||||
|         }${this.archivedFilter === 'archived' ? '&archived=true' : ''}${this.archivedFilter === 'unarchived' ? '&archived=false' : '' |         }${this.archivedFilter === 'archived' ? '&archived=true' : ''}${this.archivedFilter === 'unarchived' ? '&archived=false' : '' | ||||||
| @@ -302,7 +302,7 @@ function initVueComponents() { | |||||||
|         this.isLoading = true; |         this.isLoading = true; | ||||||
|  |  | ||||||
|         if (!this.reposTotalCount) { |         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) => { |           $.getJSON(totalCountSearchURL, (_result, _textStatus, request) => { | ||||||
|             this.reposTotalCount = request.getResponseHeader('X-Total-Count'); |             this.reposTotalCount = request.getResponseHeader('X-Total-Count'); | ||||||
|           }); |           }); | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ export function initCompSearchUserBox() { | |||||||
|   $searchUserBox.search({ |   $searchUserBox.search({ | ||||||
|     minCharacters: 2, |     minCharacters: 2, | ||||||
|     apiSettings: { |     apiSettings: { | ||||||
|       url: `${appSubUrl}/api/v1/users/search?q={query}`, |       url: `${appSubUrl}/user/search?q={query}`, | ||||||
|       onResponse(response) { |       onResponse(response) { | ||||||
|         const items = []; |         const items = []; | ||||||
|         const searchQueryUppercase = $searchUserBox.find('input').val().toUpperCase(); |         const searchQueryUppercase = $searchUserBox.find('input').val().toUpperCase(); | ||||||
|   | |||||||
| @@ -158,7 +158,7 @@ async function updateNotificationTable() { | |||||||
| async function updateNotificationCount() { | async function updateNotificationCount() { | ||||||
|   const data = await $.ajax({ |   const data = await $.ajax({ | ||||||
|     type: 'GET', |     type: 'GET', | ||||||
|     url: `${appSubUrl}/api/v1/notifications/new`, |     url: `${appSubUrl}/notifications/new`, | ||||||
|     headers: { |     headers: { | ||||||
|       'X-Csrf-Token': csrfToken, |       'X-Csrf-Token': csrfToken, | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ export function initOrgTeamSearchRepoBox() { | |||||||
|   $searchRepoBox.search({ |   $searchRepoBox.search({ | ||||||
|     minCharacters: 2, |     minCharacters: 2, | ||||||
|     apiSettings: { |     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) { |       onResponse(response) { | ||||||
|         const items = []; |         const items = []; | ||||||
|         $.each(response.data, (_i, item) => { |         $.each(response.data, (_i, item) => { | ||||||
|   | |||||||
| @@ -91,7 +91,7 @@ export function initRepoTopicBar() { | |||||||
|       label: 'ui small label' |       label: 'ui small label' | ||||||
|     }, |     }, | ||||||
|     apiSettings: { |     apiSettings: { | ||||||
|       url: `${appSubUrl}/api/v1/topics/search?q={query}`, |       url: `${appSubUrl}/explore/topics/search?q={query}`, | ||||||
|       throttle: 500, |       throttle: 500, | ||||||
|       cache: false, |       cache: false, | ||||||
|       onResponse(res) { |       onResponse(res) { | ||||||
|   | |||||||
| @@ -54,7 +54,7 @@ function updateDeadline(deadlineString) { | |||||||
|     realDeadline = new Date(newDate); |     realDeadline = new Date(newDate); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   $.ajax(`${$('#update-issue-deadline-form').attr('action')}/deadline`, { |   $.ajax(`${$('#update-issue-deadline-form').attr('action')}`, { | ||||||
|     data: JSON.stringify({ |     data: JSON.stringify({ | ||||||
|       due_date: realDeadline, |       due_date: realDeadline, | ||||||
|     }), |     }), | ||||||
| @@ -91,9 +91,9 @@ export function initRepoIssueList() { | |||||||
|   const repoId = $('#repoId').val(); |   const repoId = $('#repoId').val(); | ||||||
|   const crossRepoSearch = $('#crossRepoSearch').val(); |   const crossRepoSearch = $('#crossRepoSearch').val(); | ||||||
|   const tp = $('#type').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') { |   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') |   $('#new-dependency-drop-list') | ||||||
|     .dropdown({ |     .dropdown({ | ||||||
| @@ -292,7 +292,7 @@ export function initRepoIssueReferenceRepositorySearch() { | |||||||
|   $('.issue_reference_repository_search') |   $('.issue_reference_repository_search') | ||||||
|     .dropdown({ |     .dropdown({ | ||||||
|       apiSettings: { |       apiSettings: { | ||||||
|         url: `${appSubUrl}/api/v1/repos/search?q={query}&limit=20`, |         url: `${appSubUrl}/repo/search?q={query}&limit=20`, | ||||||
|         onResponse(response) { |         onResponse(response) { | ||||||
|           const filteredResponse = {success: true, results: []}; |           const filteredResponse = {success: true, results: []}; | ||||||
|           $.each(response.data, (_r, repo) => { |           $.each(response.data, (_r, repo) => { | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ export function initRepoSettingSearchTeamBox() { | |||||||
|   $searchTeamBox.search({ |   $searchTeamBox.search({ | ||||||
|     minCharacters: 2, |     minCharacters: 2, | ||||||
|     apiSettings: { |     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}, |       headers: {'X-Csrf-Token': csrfToken}, | ||||||
|       onResponse(response) { |       onResponse(response) { | ||||||
|         const items = []; |         const items = []; | ||||||
|   | |||||||
| @@ -23,7 +23,7 @@ export function initRepoTemplateSearch() { | |||||||
|     $('#repo_template_search') |     $('#repo_template_search') | ||||||
|       .dropdown({ |       .dropdown({ | ||||||
|         apiSettings: { |         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) { |           onResponse(response) { | ||||||
|             const filteredResponse = {success: true, results: []}; |             const filteredResponse = {success: true, results: []}; | ||||||
|             filteredResponse.results.push({ |             filteredResponse.results.push({ | ||||||
|   | |||||||
| @@ -111,7 +111,7 @@ async function updateStopwatchWithCallback(callback, timeout) { | |||||||
| async function updateStopwatch() { | async function updateStopwatch() { | ||||||
|   const data = await $.ajax({ |   const data = await $.ajax({ | ||||||
|     type: 'GET', |     type: 'GET', | ||||||
|     url: `${appSubUrl}/api/v1/user/stopwatches`, |     url: `${appSubUrl}/user/stopwatches`, | ||||||
|     headers: {'X-Csrf-Token': csrfToken}, |     headers: {'X-Csrf-Token': csrfToken}, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user