2
0

feat(issues): add external user tracking for app integration

Adds ExternalUserID and ExternalSource fields to issues table to allow external apps to track their own user identifiers when creating issues via API. Includes database migration v336, search filters, JSON API endpoint, and UI filter option. Adds locale strings across all 28 languages for external user filtering.
This commit is contained in:
2026-01-22 15:23:23 -05:00
parent 5b42ffcd92
commit 7debd1ada3
39 changed files with 390 additions and 2 deletions

View File

@@ -122,6 +122,10 @@ type Issue struct {
// Time estimate
TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
// External app integration - allows apps to track their own user IDs
ExternalUserID string `xorm:"INDEX VARCHAR(255)"` // App's custom user identifier
ExternalSource string `xorm:"VARCHAR(100)"` // App/source identifier (e.g., "myapp-ios", "myapp-android")
}
var (

View File

@@ -54,6 +54,10 @@ type IssuesOptions struct { //nolint:revive // export stutter
Owner *user_model.User // issues permission scope, it could be an organization or a user
Team *organization.Team // issues permission scope
Doer *user_model.User // issues permission scope
// External app integration filters
ExternalUserID string // Filter by external user ID from app integration
ExternalSource string // Filter by external source/app identifier
}
// Copy returns a copy of the options.
@@ -286,6 +290,14 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) {
sess.And(builder.Eq{"repository.is_archived": opts.IsArchived.Value()})
}
// External app integration filters
if opts.ExternalUserID != "" {
sess.And(builder.Eq{"issue.external_user_id": opts.ExternalUserID})
}
if opts.ExternalSource != "" {
sess.And(builder.Eq{"issue.external_source": opts.ExternalSource})
}
applyLabelsCondition(sess, opts)
if opts.Owner != nil {

View File

@@ -410,6 +410,7 @@ func prepareMigrationTasks() []*migration {
newMigration(333, "Add group_header to repository", v1_26.AddGroupHeaderToRepository),
newMigration(334, "Add group_header to user for organization grouping", v1_26.AddGroupHeaderToUser),
newMigration(335, "Add is_global to package for global package access", v1_26.AddIsGlobalToPackage),
newMigration(336, "Add external user fields to issue for app integration", v1_26.AddExternalUserFieldsToIssue),
}
return preparedMigrations
}

View File

@@ -0,0 +1,19 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"xorm.io/xorm"
)
// AddExternalUserFieldsToIssue adds ExternalUserID and ExternalSource columns to the issue table
// These fields allow apps to track their own user IDs for issues submitted via the API
func AddExternalUserFieldsToIssue(x *xorm.Engine) error {
type Issue struct {
ExternalUserID string `xorm:"INDEX VARCHAR(255)"`
ExternalSource string `xorm:"VARCHAR(100)"`
}
return x.Sync(new(Issue))
}

View File

@@ -1297,6 +1297,7 @@
"repo.issues.filter_project": "Projekt",
"repo.issues.filter_project_all": "Všechny projekty",
"repo.issues.filter_project_none": "Žádný projekt",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_assignee": "Zpracovatel",
"repo.issues.filter_poster": "Autor",
"repo.issues.filter_user_placeholder": "Hledat uživatele",

View File

@@ -1277,6 +1277,7 @@
"repo.issues.filter_assignee": "Zuständig",
"repo.issues.filter_assignee_no_assignee": "Niemandem zugewiesen",
"repo.issues.filter_assignee_any_assignee": "Jemandem zugewiesen",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_poster": "Autor",
"repo.issues.filter_user_placeholder": "Benutzer suchen",
"repo.issues.filter_user_no_select": "Alle Benutzer",

View File

@@ -1178,6 +1178,7 @@
"repo.issues.filter_project": "Έργο",
"repo.issues.filter_project_all": "Όλα τα έργα",
"repo.issues.filter_project_none": "Χωρίς έργα",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_assignee": "Αποδέκτης",
"repo.issues.filter_poster": "Συγγραφέας",
"repo.issues.filter_type": "Τύπος",

View File

@@ -1489,6 +1489,7 @@
"repo.issues.filter_assignee": "Assignee",
"repo.issues.filter_assignee_no_assignee": "Assigned to nobody",
"repo.issues.filter_assignee_any_assignee": "Assigned to anybody",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_poster": "Author",
"repo.issues.filter_user_placeholder": "Search users",
"repo.issues.filter_user_no_select": "All users",

View File

@@ -1161,6 +1161,7 @@
"repo.issues.filter_project": "Proyecto",
"repo.issues.filter_project_all": "Todos los proyectos",
"repo.issues.filter_project_none": "Ningún proyecto",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_assignee": "Asignada a",
"repo.issues.filter_poster": "Autor",
"repo.issues.filter_type": "Tipo",

View File

@@ -913,6 +913,7 @@
"repo.issues.filter_label_no_select": "تمامی برچسب‎ها",
"repo.issues.filter_milestone": "نقطه عطف",
"repo.issues.filter_project_none": "هیچ پروژه ثبت نشده",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_assignee": "مسئول رسیدگی",
"repo.issues.filter_poster": "مولف",
"repo.issues.filter_type": "نوع",

View File

@@ -699,6 +699,7 @@
"repo.issues.filter_label_no_select": "Kaikki tunnisteet",
"repo.issues.filter_milestone": "Merkkipaalu",
"repo.issues.filter_project": "Projekti",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_assignee": "Osoitettu",
"repo.issues.filter_poster": "Tekijä",
"repo.issues.filter_type": "Tyyppi",

View File

@@ -1425,6 +1425,7 @@
"repo.issues.filter_assignee": "Assigné",
"repo.issues.filter_assignee_no_assignee": "Non-assigné",
"repo.issues.filter_assignee_any_assignee": "Assigné",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_poster": "Auteur",
"repo.issues.filter_user_placeholder": "Rechercher des utilisateurs",
"repo.issues.filter_user_no_select": "Tous les utilisateurs",

View File

@@ -1460,6 +1460,7 @@
"repo.issues.filter_assignee": "Sannaitheoir",
"repo.issues.filter_assignee_no_assignee": "Sannta do dhuine ar bith",
"repo.issues.filter_assignee_any_assignee": "Sannta do dhuine ar bith",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_poster": "Údar",
"repo.issues.filter_user_placeholder": "Cuardaigh úsáideoirí",
"repo.issues.filter_user_no_select": "Gach úsáideoir",

View File

@@ -1470,6 +1470,7 @@
"repo.issues.filter_assignee": "Assignee",
"repo.issues.filter_assignee_no_assignee": "Assigned to nobody",
"repo.issues.filter_assignee_any_assignee": "Assigned to anybody",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_poster": "Author",
"repo.issues.filter_user_placeholder": "Search users",
"repo.issues.filter_user_no_select": "All users",

View File

@@ -644,6 +644,7 @@
"repo.issues.filter_label": "Címke",
"repo.issues.filter_label_no_select": "Minden címke",
"repo.issues.filter_milestone": "MérföldkÅ",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_assignee": "Megbízott",
"repo.issues.filter_poster": "SzerzÅ",
"repo.issues.filter_type": "Típus",

View File

@@ -614,6 +614,7 @@
"repo.issues.add_assignee_at": "telah ditugaskan oleh <b>%s</b> %s",
"repo.issues.delete_branch_at": "telah dihapus cabang <b>%s</b> %s",
"repo.issues.filter_milestone": "Tonggak",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_assignee": "Menerima",
"repo.issues.filter_poster": "Penulis",
"repo.issues.filter_type": "Tipe",

View File

@@ -616,6 +616,7 @@
"repo.issues.filter_milestone": "Tímamót",
"repo.issues.filter_project": "Verkefni",
"repo.issues.filter_project_none": "Ekkert verkefni",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_assignee": "Úthlutað að",
"repo.issues.filter_poster": "Höfundur",
"repo.issues.filter_type": "Tegund",

View File

@@ -947,6 +947,7 @@
"repo.issues.filter_milestone": "Traguardo",
"repo.issues.filter_project": "Progetto",
"repo.issues.filter_project_none": "Nessun progetto",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_assignee": "Assegnatario",
"repo.issues.filter_poster": "Autore",
"repo.issues.filter_type": "Tipo",

View File

@@ -1447,6 +1447,7 @@
"repo.issues.filter_assignee": "担当者",
"repo.issues.filter_assignee_no_assignee": "担当者なし",
"repo.issues.filter_assignee_any_assignee": "担当者あり",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_poster": "作成者",
"repo.issues.filter_user_placeholder": "ユーザーを検索",
"repo.issues.filter_user_no_select": "すべてのユーザー",

View File

@@ -580,6 +580,7 @@
"repo.issues.filter_label": "레이블",
"repo.issues.filter_label_no_select": "모든 레이블",
"repo.issues.filter_milestone": "마일스톤",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_assignee": "담당자",
"repo.issues.filter_poster": "작성자",
"repo.issues.filter_type": "유형",

View File

@@ -1201,6 +1201,7 @@
"repo.issues.filter_project": "Projekts",
"repo.issues.filter_project_all": "Visi projekti",
"repo.issues.filter_project_none": "Nav projekta",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_assignee": "Atbildīgais",
"repo.issues.filter_poster": "Autors",
"repo.issues.filter_type": "Veids",

View File

@@ -931,6 +931,7 @@
"repo.issues.filter_label_no_select": "Alle labels",
"repo.issues.filter_milestone": "Mijlpaal",
"repo.issues.filter_project_none": "Geen project",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_assignee": "Aangewezene",
"repo.issues.filter_poster": "Auteur",
"repo.issues.filter_type.all_issues": "Alle kwesties",

View File

@@ -894,6 +894,7 @@
"repo.issues.filter_label_no_select": "Wszystkie etykiety",
"repo.issues.filter_milestone": "Kamień milowy",
"repo.issues.filter_project_none": "Brak projektu",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_assignee": "Przypisany",
"repo.issues.filter_poster": "Autor",
"repo.issues.filter_type": "Typ",

View File

@@ -1353,6 +1353,7 @@
"repo.issues.filter_project": "Projeto",
"repo.issues.filter_project_all": "Todos os projetos",
"repo.issues.filter_project_none": "Sem projeto",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_assignee": "Atribuído",
"repo.issues.filter_poster": "Autor",
"repo.issues.filter_type": "Tipo",

View File

@@ -1468,6 +1468,7 @@
"repo.issues.filter_assignee": "Encarregado",
"repo.issues.filter_assignee_no_assignee": "Não atribuída",
"repo.issues.filter_assignee_any_assignee": "Atribuída a alguém",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_poster": "Autor(a)",
"repo.issues.filter_user_placeholder": "Procurar utilizadores",
"repo.issues.filter_user_no_select": "Todos os utilizadores",

View File

@@ -1179,6 +1179,7 @@
"repo.issues.filter_project": "Проект",
"repo.issues.filter_project_all": "Все проекты",
"repo.issues.filter_project_none": "Нет проекта",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_assignee": "Назначено",
"repo.issues.filter_poster": "Автор",
"repo.issues.filter_type": "Тип",

View File

@@ -884,6 +884,7 @@
"repo.issues.filter_label_no_select": "සියලු ලේබල",
"repo.issues.filter_milestone": "සන්ධිස්ථානය",
"repo.issues.filter_project_none": "ව්‍යාපෘති නැත",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_assignee": "අස්ගිනී",
"repo.issues.filter_poster": "à¶šà¶­à·˜",
"repo.issues.filter_type": "වර්ගය",

View File

@@ -892,6 +892,7 @@
"repo.issues.filter_label":  ­tok",
"repo.issues.filter_milestone": "Míľnik",
"repo.issues.filter_project": "Projekt",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_poster": "Autor",
"repo.issues.filter_type.review_requested": "Požiadané o revíziu",
"repo.issues.action_open": "Otvoriť",

View File

@@ -730,6 +730,7 @@
"repo.issues.filter_label_no_select": "Alla etiketter",
"repo.issues.filter_milestone": "Milsten",
"repo.issues.filter_project_none": "Inget projekt",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_assignee": "Förvärvare",
"repo.issues.filter_poster": "Upphovsman",
"repo.issues.filter_type": "Typ",

View File

@@ -1453,6 +1453,7 @@
"repo.issues.filter_assignee": "Atanan",
"repo.issues.filter_assignee_no_assignee": "Hiç kimseye atanmamış",
"repo.issues.filter_assignee_any_assignee": "Herhangi bir kimseye atanmış",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_poster": "Yazar",
"repo.issues.filter_user_placeholder": "Kullanıcıları ara",
"repo.issues.filter_user_no_select": "Tüm kullanıcılar",

View File

@@ -1309,6 +1309,7 @@
"repo.issues.filter_assignee": "Виконавець",
"repo.issues.filter_assignee_no_assignee": "Нікому не присвоєно",
"repo.issues.filter_assignee_any_assignee": "Призначено будь-кому",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_poster": "Автор",
"repo.issues.filter_user_placeholder": "Пошук користувачів",
"repo.issues.filter_user_no_select": "Усі користувачі",

View File

@@ -1468,6 +1468,7 @@
"repo.issues.filter_assignee": "指派人筛选",
"repo.issues.filter_assignee_no_assignee": "未指派给任何人",
"repo.issues.filter_assignee_any_assignee": "已有指派",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_poster": "作者",
"repo.issues.filter_user_placeholder": "搜索用户",
"repo.issues.filter_user_no_select": "所有用户",

View File

@@ -1303,6 +1303,7 @@
"repo.issues.filter_project": "專案",
"repo.issues.filter_project_all": "所有專案",
"repo.issues.filter_project_none": "未選擇專案",
"repo.issues.filter_external_user": "External User",
"repo.issues.filter_assignee": "負責人",
"repo.issues.filter_poster": "作者",
"repo.issues.filter_user_placeholder": "搜尋使用者",

View File

@@ -0,0 +1,300 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"fmt"
"net/http"
"strings"
"code.gitcaddy.com/server/v3/models/db"
issues_model "code.gitcaddy.com/server/v3/models/issues"
user_model "code.gitcaddy.com/server/v3/models/user"
"code.gitcaddy.com/server/v3/modules/optional"
"code.gitcaddy.com/server/v3/modules/setting"
"code.gitcaddy.com/server/v3/modules/web"
"code.gitcaddy.com/server/v3/services/context"
issue_service "code.gitcaddy.com/server/v3/services/issue"
vault_service "code.gitcaddy.com/server/v3/services/vault"
)
// IssueSubmitJSON is the request body for submitting an issue via JSON API
type IssueSubmitJSON struct {
Title string `json:"title" binding:"Required"`
Body string `json:"body"`
Labels []string `json:"labels"`
ExternalUserID string `json:"external_user_id"`
ExternalSource string `json:"external_source"`
}
// IssueJSON is a lightweight JSON response for issues
type IssueJSON struct {
Number int64 `json:"number"`
Title string `json:"title"`
Body string `json:"body"`
State string `json:"state"` // "open" or "closed"
Labels []string `json:"labels"`
CommentsCount int `json:"comments_count"`
ExternalUserID string `json:"external_user_id,omitempty"`
ExternalSource string `json:"external_source,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
ClosedAt string `json:"closed_at,omitempty"`
HTMLURL string `json:"html_url"`
}
// IssueListJSON is a response containing a list of issues
type IssueListJSON struct {
Issues []IssueJSON `json:"issues"`
TotalCount int64 `json:"total_count"`
}
// checkVaultTokenForRepo validates vault token for private repo access
func checkVaultTokenForRepo(ctx *context.Context) bool {
// If user is logged in, they have access through normal means
if ctx.Doer != nil {
return true
}
// For private repos, check vault token
if ctx.Repo.Repository.IsPrivate {
token := vaultTokenFromHeader(ctx.Req)
if token == "" {
ctx.JSON(http.StatusUnauthorized, map[string]string{
"error": "unauthorized",
"message": "This repository is private. Provide a vault token via Authorization header.",
})
return false
}
validToken, err := validateVaultTokenForRepo(ctx, token, ctx.Repo.Repository.ID)
if err != nil || validToken == nil {
ctx.JSON(http.StatusUnauthorized, map[string]string{
"error": "invalid_token",
"message": "Invalid or expired vault token",
})
return false
}
}
return true
}
// issueToJSON converts an issue model to JSON response
func issueToJSON(issue *issues_model.Issue, baseURL string) IssueJSON {
state := "open"
if issue.IsClosed {
state = "closed"
}
labels := make([]string, 0, len(issue.Labels))
for _, label := range issue.Labels {
labels = append(labels, label.Name)
}
result := IssueJSON{
Number: issue.Index,
Title: issue.Title,
Body: issue.Content,
State: state,
Labels: labels,
CommentsCount: issue.NumComments,
ExternalUserID: issue.ExternalUserID,
ExternalSource: issue.ExternalSource,
CreatedAt: issue.CreatedUnix.AsTime().Format("2006-01-02T15:04:05Z"),
UpdatedAt: issue.UpdatedUnix.AsTime().Format("2006-01-02T15:04:05Z"),
HTMLURL: fmt.Sprintf("%s/issues/%d", baseURL, issue.Index),
}
if issue.IsClosed && issue.ClosedUnix > 0 {
result.ClosedAt = issue.ClosedUnix.AsTime().Format("2006-01-02T15:04:05Z")
}
return result
}
// IssueSubmitJSONEndpoint handles POST requests to submit new issues via JSON
// URL: POST /{owner}/{repo}/issues/submit.json
//
// Supports authentication via:
// - Normal user session
// - Vault token in Authorization header (Bearer gvt_xxx)
func IssueSubmitJSONEndpoint(ctx *context.Context) {
// Set rate limit exemption headers
ctx.Resp.Header().Set("X-RateLimit-Exempt", "app-integration")
// Check access
if !checkVaultTokenForRepo(ctx) {
return
}
// Parse JSON body
form := web.GetForm(ctx).(*IssueSubmitJSON)
// Validate title
if strings.TrimSpace(form.Title) == "" {
ctx.JSON(http.StatusBadRequest, map[string]string{
"error": "validation_error",
"message": "Title is required",
})
return
}
// Get or create a system user for external submissions
var poster *user_model.User
var err error
if ctx.Doer != nil {
poster = ctx.Doer
} else {
// Use ghost user for vault token submissions
poster = user_model.NewGhostUser()
}
// Create the issue
issue := &issues_model.Issue{
RepoID: ctx.Repo.Repository.ID,
Repo: ctx.Repo.Repository,
Title: form.Title,
Content: form.Body,
PosterID: poster.ID,
Poster: poster,
ExternalUserID: form.ExternalUserID,
ExternalSource: form.ExternalSource,
}
// Get label IDs from label names
var labelIDs []int64
if len(form.Labels) > 0 {
labels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, "", db.ListOptions{})
if err == nil {
labelMap := make(map[string]int64)
for _, label := range labels {
labelMap[strings.ToLower(label.Name)] = label.ID
}
for _, labelName := range form.Labels {
if id, ok := labelMap[strings.ToLower(labelName)]; ok {
labelIDs = append(labelIDs, id)
}
}
}
}
// Create the issue using the service
if err = issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, labelIDs, nil, nil, 0); err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]string{
"error": "create_failed",
"message": "Failed to create issue: " + err.Error(),
})
return
}
// Load labels for response
if err = issue.LoadLabels(ctx); err != nil {
// Non-fatal, continue without labels
issue.Labels = nil
}
// Build response
baseURL := setting.AppURL + ctx.Repo.Repository.FullName()
ctx.JSON(http.StatusCreated, issueToJSON(issue, baseURL))
}
// IssueStatusJSONEndpoint returns the status of a specific issue
// URL: GET /{owner}/{repo}/issues/{index}/status.json
func IssueStatusJSONEndpoint(ctx *context.Context) {
// Set rate limit exemption headers
ctx.Resp.Header().Set("X-RateLimit-Exempt", "app-integration")
// Check access
if !checkVaultTokenForRepo(ctx) {
return
}
// Get issue by index
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
if issues_model.IsErrIssueNotExist(err) {
ctx.JSON(http.StatusNotFound, map[string]string{
"error": "not_found",
"message": "Issue not found",
})
return
}
ctx.JSON(http.StatusInternalServerError, map[string]string{
"error": "internal_error",
"message": "Failed to get issue",
})
return
}
// Load labels
if err = issue.LoadLabels(ctx); err != nil {
issue.Labels = nil
}
baseURL := setting.AppURL + ctx.Repo.Repository.FullName()
ctx.JSON(http.StatusOK, issueToJSON(issue, baseURL))
}
// IssuesByExternalUserJSONEndpoint returns issues for a specific external user ID
// URL: GET /{owner}/{repo}/issues/external/{external_user_id}.json
func IssuesByExternalUserJSONEndpoint(ctx *context.Context) {
// Set rate limit exemption headers
ctx.Resp.Header().Set("X-RateLimit-Exempt", "app-integration")
// Check access
if !checkVaultTokenForRepo(ctx) {
return
}
externalUserID := ctx.PathParam("external_user_id")
if externalUserID == "" {
ctx.JSON(http.StatusBadRequest, map[string]string{
"error": "validation_error",
"message": "External user ID is required",
})
return
}
// Query issues by external user ID
issues, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
RepoIDs: []int64{ctx.Repo.Repository.ID},
ExternalUserID: externalUserID,
IsPull: optional.Some(false),
SortType: "newest",
})
if err != nil {
ctx.JSON(http.StatusInternalServerError, map[string]string{
"error": "internal_error",
"message": "Failed to get issues",
})
return
}
// Load labels for all issues
for _, issue := range issues {
if err = issue.LoadLabels(ctx); err != nil {
issue.Labels = nil
}
}
// Build response
baseURL := setting.AppURL + ctx.Repo.Repository.FullName()
issueList := make([]IssueJSON, 0, len(issues))
for _, issue := range issues {
issueList = append(issueList, issueToJSON(issue, baseURL))
}
ctx.JSON(http.StatusOK, IssueListJSON{
Issues: issueList,
TotalCount: int64(len(issueList)),
})
}
// validateVaultTokenForRepo is defined in release.go
// vaultTokenFromHeader is defined in release.go
// Ensure vault_service is used (avoid unused import error)
var _ = vault_service.ErrAccessDenied

View File

@@ -486,6 +486,7 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6
assigneeID := ctx.FormString("assignee")
posterUsername := ctx.FormString("poster")
posterUserID := shared_user.GetFilterUserIDByName(ctx, posterUsername)
externalUserID := ctx.FormString("external_user")
var mentionedID, reviewRequestedID, reviewedID int64
if ctx.IsSigned {
@@ -535,6 +536,7 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6
ReviewedID: reviewedID,
IsPull: isPullOption,
IssueIDs: nil,
ExternalUserID: externalUserID,
}
if keyword != "" {
keywordMatchedIssueIDs, _, err = issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, statsOpts))
@@ -721,6 +723,7 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int6
ctx.Data["ProjectID"] = projectID
ctx.Data["AssigneeID"] = assigneeID
ctx.Data["PosterUsername"] = posterUsername
ctx.Data["ExternalUserID"] = externalUserID
ctx.Data["Keyword"] = keyword
ctx.Data["IsShowClosed"] = isShowClosed
switch {

View File

@@ -1537,6 +1537,15 @@ func registerWebRoutes(m *web.Router) {
}, optSignIn, context.RepoAssignment, repo.MustBeNotEmpty, reqRepoReleaseReader)
// end "/{username}/{reponame}": repo releases
m.Group("/{username}/{reponame}", func() { // issue JSON API for app integration
m.Group("/issues", func() {
m.Post("/submit.json", web.Bind(repo.IssueSubmitJSON{}), repo.IssueSubmitJSONEndpoint)
m.Get("/external/{external_user_id}.json", repo.IssuesByExternalUserJSONEndpoint)
m.Get("/{index}/status.json", repo.IssueStatusJSONEndpoint)
})
}, optSignIn, context.RepoAssignment, reqRepoIssuesOrPullsReader)
// end "/{username}/{reponame}": issue JSON API
m.Group("/{username}/{reponame}", func() { // to maintain compatibility with old attachments
m.Get("/attachments/{uuid}", repo.GetAttachment)
}, optSignIn, context.RepoAssignment)

View File

@@ -1,4 +1,4 @@
{{$queryLink := QueryBuild "?" "q" $.Keyword "type" $.ViewType "sort" $.SortType "state" $.State "labels" $.SelectLabels "milestone" $.MilestoneID "project" $.ProjectID "assignee" $.AssigneeID "poster" $.PosterUsername "archived_labels" (Iif $.ShowArchivedLabels "true")}}
{{$queryLink := QueryBuild "?" "q" $.Keyword "type" $.ViewType "sort" $.SortType "state" $.State "labels" $.SelectLabels "milestone" $.MilestoneID "project" $.ProjectID "assignee" $.AssigneeID "poster" $.PosterUsername "external_user" $.ExternalUserID "archived_labels" (Iif $.ShowArchivedLabels "true")}}
{{template "repo/issue/filter_item_label" dict "Labels" .Labels "QueryLink" $queryLink "SupportArchivedLabel" true}}
@@ -98,6 +98,16 @@
"TextFilterMatchAny" (ctx.Locale.Tr "repo.issues.filter_assignee_any_assignee")
}}
<!-- External User ID Filter (for app integration) -->
{{if $.ExternalUserID}}
<div class="item">
<a class="ui label" href="{{QueryBuild $queryLink "external_user" NIL}}">
{{ctx.Locale.Tr "repo.issues.filter_external_user"}}: {{$.ExternalUserID}}
{{svg "octicon-x" 14}}
</a>
</div>
{{end}}
{{if .IsSigned}}
<!-- Type -->
<div class="item ui dropdown jump">

View File

@@ -3,7 +3,7 @@
{{if .PageIsMilestones}}
{{$allStatesLink = QueryBuild "?" "q" $.Keyword "sort" $.SortType "state" "all"}}
{{else}}
{{$allStatesLink = QueryBuild "?" "q" $.Keyword "type" $.ViewType "sort" $.SortType "state" "all" "labels" $.SelectLabels "milestone" $.MilestoneID "project" $.ProjectID "assignee" $.AssigneeID "poster" $.PosterUsername "archived_labels" (Iif $.ShowArchivedLabels "true")}}
{{$allStatesLink = QueryBuild "?" "q" $.Keyword "type" $.ViewType "sort" $.SortType "state" "all" "labels" $.SelectLabels "milestone" $.MilestoneID "project" $.ProjectID "assignee" $.AssigneeID "poster" $.PosterUsername "external_user" $.ExternalUserID "archived_labels" (Iif $.ShowArchivedLabels "true")}}
{{end}}
{{$openLink = QueryBuild $allStatesLink "state" "open"}}
{{$closedLink = QueryBuild $allStatesLink "state" "closed"}}

View File

@@ -8,6 +8,7 @@
<input type="hidden" name="project" value="{{$.ProjectID}}">
<input type="hidden" name="assignee" value="{{$.AssigneeID}}">
<input type="hidden" name="poster" value="{{$.PosterUsername}}">
<input type="hidden" name="external_user" value="{{$.ExternalUserID}}">
<input type="hidden" name="sort" value="{{$.SortType}}">
{{end}}
{{template "shared/search/input" dict "Value" .Keyword}}