Skip to content

Commit 75111ea

Browse files
committed
add button to export issues to Excel
1 parent de69e7f commit 75111ea

File tree

7 files changed

+114
-9
lines changed

7 files changed

+114
-9
lines changed

go.mod

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,8 @@ require (
256256
github.com/prometheus/common v0.65.0 // indirect
257257
github.com/prometheus/procfs v0.17.0 // indirect
258258
github.com/rhysd/actionlint v1.7.7 // indirect
259+
github.com/richardlehane/mscfb v1.0.4 // indirect
260+
github.com/richardlehane/msoleps v1.0.4 // indirect
259261
github.com/rivo/uniseg v0.4.7 // indirect
260262
github.com/rs/xid v1.6.0 // indirect
261263
github.com/russross/blackfriday/v2 v2.1.0 // indirect
@@ -265,13 +267,17 @@ require (
265267
github.com/spf13/afero v1.15.0 // indirect
266268
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
267269
github.com/tinylib/msgp v1.4.0 // indirect
270+
github.com/tiendc/go-deepcopy v1.6.0 // indirect
268271
github.com/unknwon/com v1.0.1 // indirect
269272
github.com/valyala/fastjson v1.6.4 // indirect
270273
github.com/x448/float16 v0.8.4 // indirect
271274
github.com/xanzy/ssh-agent v0.3.3 // indirect
272275
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
273276
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
274277
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
278+
github.com/xuri/efp v0.0.1 // indirect
279+
github.com/xuri/excelize/v2 v2.9.1 // indirect
280+
github.com/xuri/nfp v0.0.1 // indirect
275281
github.com/zeebo/assert v1.3.0 // indirect
276282
github.com/zeebo/blake3 v0.2.4 // indirect
277283
go.etcd.io/bbolt v1.4.3 // indirect

go.sum

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,11 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6O
678678
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
679679
github.com/rhysd/actionlint v1.7.7 h1:0KgkoNTrYY7vmOCs9BW2AHxLvvpoY9nEUzgBHiPUr0k=
680680
github.com/rhysd/actionlint v1.7.7/go.mod h1:AE6I6vJEkNaIfWqC2GNE5spIJNhxf8NCtLEKU4NnUXg=
681+
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
682+
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
683+
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
684+
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
685+
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
681686
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
682687
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
683688
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -750,6 +755,8 @@ github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08Yu
750755
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8=
751756
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
752757
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
758+
github.com/tiendc/go-deepcopy v1.6.0 h1:0UtfV/imoCwlLxVsyfUd4hNHnB3drXsfle+wzSCA5Wo=
759+
github.com/tiendc/go-deepcopy v1.6.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
753760
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
754761
github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8=
755762
github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o=
@@ -784,6 +791,12 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
784791
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
785792
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
786793
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
794+
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
795+
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
796+
github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
797+
github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s=
798+
github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
799+
github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
787800
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
788801
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
789802
github.com/yohcop/openid-go v1.0.1 h1:DPRd3iPO5F6O5zX2e62XpVAbPT6wV51cuucH0z9g3js=

options/locale/locale_en-US.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3561,6 +3561,7 @@ review_dismissed_reason = Reason:
35613561
create_branch = created branch <a href="%[2]s">%[3]s</a> in <a href="%[1]s">%[4]s</a>
35623562
starred_repo = starred <a href="%[1]s">%[2]s</a>
35633563
watched_repo = started watching <a href="%[1]s">%[2]s</a>
3564+
export_to_excel = Export to Excel
35643565
35653566
[tool]
35663567
now = now

routers/web/repo/issue_list.go

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
shared_user "code.gitea.io/gitea/routers/web/shared/user"
3030
"code.gitea.io/gitea/services/context"
3131
"code.gitea.io/gitea/services/convert"
32+
"code.gitea.io/gitea/services/export"
3233
issue_service "code.gitea.io/gitea/services/issue"
3334
pull_service "code.gitea.io/gitea/services/pull"
3435
)
@@ -258,14 +259,13 @@ func getUserIDForFilter(ctx *context.Context, queryName string) int64 {
258259
return user.ID
259260
}
260261

261-
// SearchRepoIssuesJSON lists the issues of a repository
262262
// This function was copied from API (decouple the web and API routes),
263263
// it is only used by frontend to search some dependency or related issues
264-
func SearchRepoIssuesJSON(ctx *context.Context) {
264+
func SearchRepoIssues(ctx *context.Context) (issues_model.IssueList, int64) {
265265
before, since, err := context.GetQueryBeforeSince(ctx.Base)
266266
if err != nil {
267267
ctx.HTTPError(http.StatusUnprocessableEntity, err.Error())
268-
return
268+
return nil, 0
269269
}
270270

271271
var isClosed optional.Option[bool]
@@ -295,7 +295,7 @@ func SearchRepoIssuesJSON(ctx *context.Context) {
295295
}
296296
if !issues_model.IsErrMilestoneNotExist(err) {
297297
ctx.HTTPError(http.StatusInternalServerError, err.Error())
298-
return
298+
return nil, 0
299299
}
300300
id, err := strconv.ParseInt(part[i], 10, 64)
301301
if err != nil {
@@ -329,15 +329,15 @@ func SearchRepoIssuesJSON(ctx *context.Context) {
329329
// FIXME: we should be more efficient here
330330
createdByID := getUserIDForFilter(ctx, "created_by")
331331
if ctx.Written() {
332-
return
332+
return nil, 0
333333
}
334334
assignedByID := getUserIDForFilter(ctx, "assigned_by")
335335
if ctx.Written() {
336-
return
336+
return nil, 0
337337
}
338338
mentionedByID := getUserIDForFilter(ctx, "mentioned_by")
339339
if ctx.Written() {
340-
return
340+
return nil, 0
341341
}
342342

343343
searchOpt := &issue_indexer.SearchOptions{
@@ -380,18 +380,39 @@ func SearchRepoIssuesJSON(ctx *context.Context) {
380380
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
381381
if err != nil {
382382
ctx.HTTPError(http.StatusInternalServerError, "SearchIssues", err.Error())
383-
return
383+
return nil, 0
384384
}
385385
issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
386386
if err != nil {
387387
ctx.HTTPError(http.StatusInternalServerError, "FindIssuesByIDs", err.Error())
388-
return
388+
return nil, 0
389389
}
390390

391+
return issues, total
392+
}
393+
394+
// SearchRepoIssuesJSON lists the issues of a repository
395+
func SearchRepoIssuesJSON(ctx *context.Context) {
396+
issues, total := SearchRepoIssues(ctx)
397+
391398
ctx.SetTotalCountHeader(total)
392399
ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, ctx.Doer, issues))
393400
}
394401

402+
func ExportIssues(ctx *context.Context) {
403+
issues, total := SearchRepoIssues(ctx)
404+
405+
if total == 0 {
406+
return
407+
}
408+
409+
f := export.IssuesToExcel(ctx, issues)
410+
411+
ctx.Resp.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
412+
ctx.Resp.Header().Set("Content-Disposition", `attachment; filename="issues.xlsx"`)
413+
_ = f.Write(ctx.Resp)
414+
}
415+
395416
func BatchDeleteIssues(ctx *context.Context) {
396417
issues := getActionIssues(ctx)
397418
if ctx.Written() {

routers/web/web.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1249,6 +1249,7 @@ func registerWebRoutes(m *web.Router) {
12491249
m.Get("/choose", repo.NewIssueChooseTemplate)
12501250
})
12511251
m.Get("/search", repo.SearchRepoIssuesJSON)
1252+
m.Get("/export", reqRepoAdmin, repo.ExportIssues)
12521253
}, reqUnitIssuesReader)
12531254

12541255
addIssuesPullsUpdateRoutes := func() {

services/export/excel.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package export
5+
6+
import (
7+
"fmt"
8+
"github.com/xuri/excelize/v2"
9+
10+
issues_model "code.gitea.io/gitea/models/issues"
11+
"code.gitea.io/gitea/services/context"
12+
)
13+
14+
func IssuesToExcel(ctx *context.Context, issues issues_model.IssueList) *excelize.File {
15+
f := excelize.NewFile()
16+
sheet := f.GetSheetName(f.GetActiveSheetIndex())
17+
18+
headers := []string{"ID", "Title", "Status", "Assignee(s)", "Label(s)", "Created At"}
19+
for col, h := range headers {
20+
cell, _ := excelize.CoordinatesToCellName(col+1, 1)
21+
f.SetCellValue(sheet, cell, h)
22+
}
23+
24+
for i, issue := range issues {
25+
26+
assignees := ""
27+
if err := issue.LoadAssignees(ctx); err == nil {
28+
if len(issue.Assignees) > 0 {
29+
for _, assignee := range issue.Assignees {
30+
if assignees != "" {
31+
assignees += ", "
32+
}
33+
if assignee.FullName != "" {
34+
assignees += assignee.FullName
35+
} else {
36+
assignees += assignee.Name
37+
}
38+
}
39+
}
40+
}
41+
42+
labels := ""
43+
if err := issue.LoadLabels(ctx); err == nil {
44+
if len(issue.Labels) > 0 {
45+
for _, label := range issue.Labels {
46+
if labels != "" {
47+
labels += ", "
48+
}
49+
labels += label.Name
50+
}
51+
}
52+
}
53+
54+
f.SetCellValue(sheet, fmt.Sprintf("A%d", i+2), issue.Index)
55+
f.SetCellValue(sheet, fmt.Sprintf("B%d", i+2), issue.Title)
56+
f.SetCellValue(sheet, fmt.Sprintf("C%d", i+2), issue.State())
57+
f.SetCellValue(sheet, fmt.Sprintf("D%d", i+2), assignees)
58+
f.SetCellValue(sheet, fmt.Sprintf("E%d", i+2), labels)
59+
f.SetCellValue(sheet, fmt.Sprintf("F%d", i+2), issue.CreatedUnix.AsTime()) // .Format("2006-01-02"))
60+
}
61+
return f
62+
}

templates/repo/issue/list.tmpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
<a class="ui small primary small button issue-list-new{{if not .PullRequestCtx.Allowed}} disabled{{end}}" href="{{if .PullRequestCtx.Allowed}}{{.PullRequestCtx.BaseRepo.Link}}/compare/{{.PullRequestCtx.BaseRepo.DefaultBranch | PathEscapeSegments}}...{{if ne .Repository.Owner.Name .PullRequestCtx.BaseRepo.Owner.Name}}{{PathEscape .Repository.Owner.Name}}:{{end}}{{.Repository.DefaultBranch | PathEscapeSegments}}{{end}}">{{ctx.Locale.Tr "action.compare_commits_general"}}</a>
3232
{{end}}
3333
{{end}}
34+
<a class="ui small primary button issue-list-export" href="{{.RepoLink}}/issues/export?{{.Page.GetParams}}">{{ctx.Locale.Tr "action.export_to_excel"}}</a>
3435
</div>
3536

3637
{{template "repo/issue/filters" .}}

0 commit comments

Comments
 (0)