Skip to content

Commit 1d87056

Browse files
committed
add button to export issues to Excel
1 parent 463016b commit 1d87056

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
@@ -245,19 +245,25 @@ require (
245245
github.com/prometheus/common v0.63.0 // indirect
246246
github.com/prometheus/procfs v0.16.1 // indirect
247247
github.com/rhysd/actionlint v1.7.7 // indirect
248+
github.com/richardlehane/mscfb v1.0.4 // indirect
249+
github.com/richardlehane/msoleps v1.0.4 // indirect
248250
github.com/rivo/uniseg v0.4.7 // indirect
249251
github.com/rs/xid v1.6.0 // indirect
250252
github.com/russross/blackfriday/v2 v2.1.0 // indirect
251253
github.com/sirupsen/logrus v1.9.3 // indirect
252254
github.com/skeema/knownhosts v1.3.1 // indirect
253255
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
256+
github.com/tiendc/go-deepcopy v1.6.0 // indirect
254257
github.com/unknwon/com v1.0.1 // indirect
255258
github.com/valyala/fastjson v1.6.4 // indirect
256259
github.com/x448/float16 v0.8.4 // indirect
257260
github.com/xanzy/ssh-agent v0.3.3 // indirect
258261
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
259262
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
260263
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
264+
github.com/xuri/efp v0.0.1 // indirect
265+
github.com/xuri/excelize/v2 v2.9.1 // indirect
266+
github.com/xuri/nfp v0.0.1 // indirect
261267
github.com/zeebo/assert v1.3.0 // indirect
262268
github.com/zeebo/blake3 v0.2.4 // indirect
263269
go.etcd.io/bbolt v1.4.0 // indirect

go.sum

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,11 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6O
614614
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
615615
github.com/rhysd/actionlint v1.7.7 h1:0KgkoNTrYY7vmOCs9BW2AHxLvvpoY9nEUzgBHiPUr0k=
616616
github.com/rhysd/actionlint v1.7.7/go.mod h1:AE6I6vJEkNaIfWqC2GNE5spIJNhxf8NCtLEKU4NnUXg=
617+
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
618+
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
619+
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
620+
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
621+
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
617622
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
618623
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
619624
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -678,6 +683,8 @@ github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08Yu
678683
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8=
679684
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
680685
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
686+
github.com/tiendc/go-deepcopy v1.6.0 h1:0UtfV/imoCwlLxVsyfUd4hNHnB3drXsfle+wzSCA5Wo=
687+
github.com/tiendc/go-deepcopy v1.6.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
681688
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
682689
github.com/tstranex/u2f v1.0.0 h1:HhJkSzDDlVSVIVt7pDJwCHQj67k7A5EeBgPmeD+pVsQ=
683690
github.com/tstranex/u2f v1.0.0/go.mod h1:eahSLaqAS0zsIEv80+vXT7WanXs7MQQDg3j3wGBSayo=
@@ -711,6 +718,12 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
711718
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
712719
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
713720
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
721+
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
722+
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
723+
github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
724+
github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s=
725+
github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
726+
github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
714727
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
715728
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
716729
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
@@ -3539,6 +3539,7 @@ review_dismissed_reason = Reason:
35393539
create_branch = created branch <a href="%[2]s">%[3]s</a> in <a href="%[1]s">%[4]s</a>
35403540
starred_repo = starred <a href="%[1]s">%[2]s</a>
35413541
watched_repo = started watching <a href="%[1]s">%[2]s</a>
3542+
export_to_excel = Export to Excel
35423543
35433544
[tool]
35443545
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
@@ -1237,6 +1237,7 @@ func registerWebRoutes(m *web.Router) {
12371237
m.Get("/choose", repo.NewIssueChooseTemplate)
12381238
})
12391239
m.Get("/search", repo.SearchRepoIssuesJSON)
1240+
m.Get("/export", reqRepoAdmin, repo.ExportIssues)
12401241
}, reqUnitIssuesReader)
12411242

12421243
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)