From f244cce347d090d75cc1e3c1c3edc3beb55c7eac Mon Sep 17 00:00:00 2001 From: Timotheus Pokorra Date: Mon, 18 Aug 2025 23:24:39 +0200 Subject: [PATCH 01/23] add button to export issues to Excel --- go.mod | 6 ++++ go.sum | 13 +++++++ options/locale/locale_en-US.ini | 1 + routers/web/repo/issue_list.go | 39 ++++++++++++++++----- routers/web/web.go | 1 + services/export/excel.go | 62 +++++++++++++++++++++++++++++++++ templates/repo/issue/list.tmpl | 1 + 7 files changed, 114 insertions(+), 9 deletions(-) create mode 100644 services/export/excel.go diff --git a/go.mod b/go.mod index 6806e76ffc5e8..9c2d684de053b 100644 --- a/go.mod +++ b/go.mod @@ -256,6 +256,8 @@ require ( github.com/prometheus/common v0.65.0 // indirect github.com/prometheus/procfs v0.17.0 // indirect github.com/rhysd/actionlint v1.7.7 // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.4 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rs/xid v1.6.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect @@ -265,6 +267,7 @@ require ( github.com/spf13/afero v1.15.0 // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/tinylib/msgp v1.4.0 // indirect + github.com/tiendc/go-deepcopy v1.6.0 // indirect github.com/unknwon/com v1.0.1 // indirect github.com/valyala/fastjson v1.6.4 // indirect github.com/x448/float16 v0.8.4 // indirect @@ -272,6 +275,9 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect + github.com/xuri/efp v0.0.1 // indirect + github.com/xuri/excelize/v2 v2.9.1 // indirect + github.com/xuri/nfp v0.0.1 // indirect github.com/zeebo/assert v1.3.0 // indirect github.com/zeebo/blake3 v0.2.4 // indirect go.etcd.io/bbolt v1.4.3 // indirect diff --git a/go.sum b/go.sum index 86fe782ae7ba7..ddc35d3c0ecf6 100644 --- a/go.sum +++ b/go.sum @@ -678,6 +678,11 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6O github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rhysd/actionlint v1.7.7 h1:0KgkoNTrYY7vmOCs9BW2AHxLvvpoY9nEUzgBHiPUr0k= github.com/rhysd/actionlint v1.7.7/go.mod h1:AE6I6vJEkNaIfWqC2GNE5spIJNhxf8NCtLEKU4NnUXg= +github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= +github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 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 github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/tiendc/go-deepcopy v1.6.0 h1:0UtfV/imoCwlLxVsyfUd4hNHnB3drXsfle+wzSCA5Wo= +github.com/tiendc/go-deepcopy v1.6.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I= github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8= 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 github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= +github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw= +github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s= +github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q= +github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yohcop/openid-go v1.0.1 h1:DPRd3iPO5F6O5zX2e62XpVAbPT6wV51cuucH0z9g3js= diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 981d9de2f8623..cf37152f5b507 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3565,6 +3565,7 @@ review_dismissed_reason = Reason: create_branch = created branch %[3]s in %[4]s starred_repo = starred %[2]s watched_repo = started watching %[2]s +export_to_excel = Export to Excel [tool] now = now diff --git a/routers/web/repo/issue_list.go b/routers/web/repo/issue_list.go index a11d35da1eb85..43def9ec8412b 100644 --- a/routers/web/repo/issue_list.go +++ b/routers/web/repo/issue_list.go @@ -29,6 +29,7 @@ import ( shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" + "code.gitea.io/gitea/services/export" issue_service "code.gitea.io/gitea/services/issue" pull_service "code.gitea.io/gitea/services/pull" ) @@ -258,14 +259,13 @@ func getUserIDForFilter(ctx *context.Context, queryName string) int64 { return user.ID } -// SearchRepoIssuesJSON lists the issues of a repository // This function was copied from API (decouple the web and API routes), // it is only used by frontend to search some dependency or related issues -func SearchRepoIssuesJSON(ctx *context.Context) { +func SearchRepoIssues(ctx *context.Context) (issues_model.IssueList, int64) { before, since, err := context.GetQueryBeforeSince(ctx.Base) if err != nil { ctx.HTTPError(http.StatusUnprocessableEntity, err.Error()) - return + return nil, 0 } var isClosed optional.Option[bool] @@ -295,7 +295,7 @@ func SearchRepoIssuesJSON(ctx *context.Context) { } if !issues_model.IsErrMilestoneNotExist(err) { ctx.HTTPError(http.StatusInternalServerError, err.Error()) - return + return nil, 0 } id, err := strconv.ParseInt(part[i], 10, 64) if err != nil { @@ -329,15 +329,15 @@ func SearchRepoIssuesJSON(ctx *context.Context) { // FIXME: we should be more efficient here createdByID := getUserIDForFilter(ctx, "created_by") if ctx.Written() { - return + return nil, 0 } assignedByID := getUserIDForFilter(ctx, "assigned_by") if ctx.Written() { - return + return nil, 0 } mentionedByID := getUserIDForFilter(ctx, "mentioned_by") if ctx.Written() { - return + return nil, 0 } searchOpt := &issue_indexer.SearchOptions{ @@ -380,18 +380,39 @@ func SearchRepoIssuesJSON(ctx *context.Context) { ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt) if err != nil { ctx.HTTPError(http.StatusInternalServerError, "SearchIssues", err.Error()) - return + return nil, 0 } issues, err := issues_model.GetIssuesByIDs(ctx, ids, true) if err != nil { ctx.HTTPError(http.StatusInternalServerError, "FindIssuesByIDs", err.Error()) - return + return nil, 0 } + return issues, total +} + +// SearchRepoIssuesJSON lists the issues of a repository +func SearchRepoIssuesJSON(ctx *context.Context) { + issues, total := SearchRepoIssues(ctx) + ctx.SetTotalCountHeader(total) ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, ctx.Doer, issues)) } +func ExportIssues(ctx *context.Context) { + issues, total := SearchRepoIssues(ctx) + + if total == 0 { + return + } + + f := export.IssuesToExcel(ctx, issues) + + ctx.Resp.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + ctx.Resp.Header().Set("Content-Disposition", `attachment; filename="issues.xlsx"`) + _ = f.Write(ctx.Resp) +} + func BatchDeleteIssues(ctx *context.Context) { issues := getActionIssues(ctx) if ctx.Written() { diff --git a/routers/web/web.go b/routers/web/web.go index 89a570dce0773..2cf3959d844d3 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1244,6 +1244,7 @@ func registerWebRoutes(m *web.Router) { m.Get("/choose", repo.NewIssueChooseTemplate) }) m.Get("/search", repo.SearchRepoIssuesJSON) + m.Get("/export", reqRepoAdmin, repo.ExportIssues) }, reqUnitIssuesReader) addIssuesPullsUpdateRoutes := func() { diff --git a/services/export/excel.go b/services/export/excel.go new file mode 100644 index 0000000000000..4e4f1ab5b977f --- /dev/null +++ b/services/export/excel.go @@ -0,0 +1,62 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package export + +import ( + "fmt" + "github.com/xuri/excelize/v2" + + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/services/context" +) + +func IssuesToExcel(ctx *context.Context, issues issues_model.IssueList) *excelize.File { + f := excelize.NewFile() + sheet := f.GetSheetName(f.GetActiveSheetIndex()) + + headers := []string{"ID", "Title", "Status", "Assignee(s)", "Label(s)", "Created At"} + for col, h := range headers { + cell, _ := excelize.CoordinatesToCellName(col+1, 1) + f.SetCellValue(sheet, cell, h) + } + + for i, issue := range issues { + + assignees := "" + if err := issue.LoadAssignees(ctx); err == nil { + if len(issue.Assignees) > 0 { + for _, assignee := range issue.Assignees { + if assignees != "" { + assignees += ", " + } + if assignee.FullName != "" { + assignees += assignee.FullName + } else { + assignees += assignee.Name + } + } + } + } + + labels := "" + if err := issue.LoadLabels(ctx); err == nil { + if len(issue.Labels) > 0 { + for _, label := range issue.Labels { + if labels != "" { + labels += ", " + } + labels += label.Name + } + } + } + + f.SetCellValue(sheet, fmt.Sprintf("A%d", i+2), issue.Index) + f.SetCellValue(sheet, fmt.Sprintf("B%d", i+2), issue.Title) + f.SetCellValue(sheet, fmt.Sprintf("C%d", i+2), issue.State()) + f.SetCellValue(sheet, fmt.Sprintf("D%d", i+2), assignees) + f.SetCellValue(sheet, fmt.Sprintf("E%d", i+2), labels) + f.SetCellValue(sheet, fmt.Sprintf("F%d", i+2), issue.CreatedUnix.AsTime()) // .Format("2006-01-02")) + } + return f +} \ No newline at end of file diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl index 1fe220e1b8b80..243d0372ccaea 100644 --- a/templates/repo/issue/list.tmpl +++ b/templates/repo/issue/list.tmpl @@ -31,6 +31,7 @@ {{ctx.Locale.Tr "action.compare_commits_general"}} {{end}} {{end}} + {{ctx.Locale.Tr "action.export_to_excel"}} {{template "repo/issue/filters" .}} From ff98c04cd34fc8cccd84d93f86890f4b2122af1b Mon Sep 17 00:00:00 2001 From: Timotheus Pokorra Date: Tue, 7 Oct 2025 15:06:40 +0200 Subject: [PATCH 02/23] add excelize to go.mod --- go.mod | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 9c2d684de053b..804849580c02b 100644 --- a/go.mod +++ b/go.mod @@ -112,6 +112,7 @@ require ( github.com/urfave/cli/v3 v3.4.1 github.com/wneessen/go-mail v0.7.2 github.com/xeipuuv/gojsonschema v1.2.0 + github.com/xuri/excelize/v2 v2.9.1 github.com/yohcop/openid-go v1.0.1 github.com/yuin/goldmark v1.7.13 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc @@ -266,6 +267,7 @@ require ( github.com/sorairolake/lzip-go v0.3.8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect + github.com/tiendc/go-deepcopy v1.6.0 // indirect github.com/tinylib/msgp v1.4.0 // indirect github.com/tiendc/go-deepcopy v1.6.0 // indirect github.com/unknwon/com v1.0.1 // indirect @@ -276,7 +278,6 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xuri/efp v0.0.1 // indirect - github.com/xuri/excelize/v2 v2.9.1 // indirect github.com/xuri/nfp v0.0.1 // indirect github.com/zeebo/assert v1.3.0 // indirect github.com/zeebo/blake3 v0.2.4 // indirect From 0ac00ffc92a736302149bf62f9478ce9c5a14195 Mon Sep 17 00:00:00 2001 From: Timotheus Pokorra Date: Tue, 7 Oct 2025 22:00:37 +0200 Subject: [PATCH 03/23] export to excel: use StreamWriter --- services/export/excel.go | 51 ++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/services/export/excel.go b/services/export/excel.go index 4e4f1ab5b977f..b3c219fdd8e09 100644 --- a/services/export/excel.go +++ b/services/export/excel.go @@ -5,6 +5,7 @@ package export import ( "fmt" + "github.com/xuri/excelize/v2" issues_model "code.gitea.io/gitea/models/issues" @@ -13,12 +14,31 @@ import ( func IssuesToExcel(ctx *context.Context, issues issues_model.IssueList) *excelize.File { f := excelize.NewFile() - sheet := f.GetSheetName(f.GetActiveSheetIndex()) + sw, err := f.NewStreamWriter("Sheet1") + if err != nil { + fmt.Println(err) + return f + } + // print headers + cell, err := excelize.CoordinatesToCellName(1, 1) + if err != nil { + fmt.Println(err) + return f + } + sw.SetRow(cell, []interface{}{ + excelize.Cell{Value: "ID"}, + excelize.Cell{Value: "Title"}, + excelize.Cell{Value: "Status"}, + excelize.Cell{Value: "Assignee(s)"}, + excelize.Cell{Value: "Label(s)"}, + excelize.Cell{Value: "Created At"}, + }) - headers := []string{"ID", "Title", "Status", "Assignee(s)", "Label(s)", "Created At"} - for col, h := range headers { - cell, _ := excelize.CoordinatesToCellName(col+1, 1) - f.SetCellValue(sheet, cell, h) + // built-in format ID 22 ("m/d/yy h:mm") + datetimeStyleID, err := f.NewStyle(&excelize.Style{NumFmt: 22}) + if err != nil { + fmt.Println(err) + return f } for i, issue := range issues { @@ -51,12 +71,19 @@ func IssuesToExcel(ctx *context.Context, issues issues_model.IssueList) *exceliz } } - f.SetCellValue(sheet, fmt.Sprintf("A%d", i+2), issue.Index) - f.SetCellValue(sheet, fmt.Sprintf("B%d", i+2), issue.Title) - f.SetCellValue(sheet, fmt.Sprintf("C%d", i+2), issue.State()) - f.SetCellValue(sheet, fmt.Sprintf("D%d", i+2), assignees) - f.SetCellValue(sheet, fmt.Sprintf("E%d", i+2), labels) - f.SetCellValue(sheet, fmt.Sprintf("F%d", i+2), issue.CreatedUnix.AsTime()) // .Format("2006-01-02")) + cell, _ := excelize.CoordinatesToCellName(1, i+1) + sw.SetRow(cell, []interface{}{ + excelize.Cell{Value: issue.Index}, + excelize.Cell{Value: issue.Title}, + excelize.Cell{Value: issue.State()}, + excelize.Cell{Value: assignees}, + excelize.Cell{Value: labels}, + excelize.Cell{StyleID: datetimeStyleID, Value: issue.CreatedUnix.AsTime()}, + }) + } + + sw.Flush() + return f -} \ No newline at end of file +} From 929a2f8cef2f538dabf96a79dfce4e9b0d2a012c Mon Sep 17 00:00:00 2001 From: Timotheus Pokorra Date: Tue, 11 Nov 2025 14:25:22 +0000 Subject: [PATCH 04/23] WIP: add tippy button to filters --- templates/repo/issue/filters.tmpl | 11 +++++++++++ templates/repo/issue/list.tmpl | 26 +++++++++++++++++++++++++- web_src/css/repo/issue-list.css | 26 ++++++++++++++++++++++++++ web_src/js/features/repo-issue-list.ts | 18 ++++++++++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) diff --git a/templates/repo/issue/filters.tmpl b/templates/repo/issue/filters.tmpl index 409ec876e6c2c..5115031db1c9e 100644 --- a/templates/repo/issue/filters.tmpl +++ b/templates/repo/issue/filters.tmpl @@ -21,6 +21,17 @@ {{else}} {{template "repo/issue/filter_list" .}} {{end}} + + + + + diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl index 243d0372ccaea..fe7d59b530802 100644 --- a/templates/repo/issue/list.tmpl +++ b/templates/repo/issue/list.tmpl @@ -31,11 +31,12 @@ {{ctx.Locale.Tr "action.compare_commits_general"}} {{end}} {{end}} - {{ctx.Locale.Tr "action.export_to_excel"}} + {{template "repo/issue/filters" .}} +
{{template "repo/issue/openclose" .}} @@ -51,7 +52,30 @@
{{template "repo/issue/filter_actions" .}} + + +
+ + + + + + +
+
{{template "shared/issuelist" dict "." . "listType" "repo"}} diff --git a/web_src/css/repo/issue-list.css b/web_src/css/repo/issue-list.css index 33888101b6d74..cb5bd2da154d5 100644 --- a/web_src/css/repo/issue-list.css +++ b/web_src/css/repo/issue-list.css @@ -84,3 +84,29 @@ margin-right: 8px; text-align: left; } + +/* used by the misc-actions popup */ +.misc-actions-panel-field, +.misc-actions-list { + margin: 10px; +} + +.misc-actions-tab .item { + padding: 5px 10px; + background: none; + color: var(--color-text-light-2); +} + +.misc-actions-tab .item.active { + color: var(--color-text-dark); + border-bottom: 3px solid currentcolor; +} + +.misc-actions-tab + .divider { + margin: -1px 0 0; +} + +.misc-actions-panel-list .item { + margin: 5px 0; +} + diff --git a/web_src/js/features/repo-issue-list.ts b/web_src/js/features/repo-issue-list.ts index 1416d0ee705b5..485ca8657824e 100644 --- a/web_src/js/features/repo-issue-list.ts +++ b/web_src/js/features/repo-issue-list.ts @@ -8,6 +8,7 @@ import {DELETE, POST} from '../modules/fetch.ts'; import {parseDom} from '../utils.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; import type {SortableEvent} from 'sortablejs'; +import {createTippy} from '../modules/tippy.ts'; function initRepoIssueListCheckboxes() { const issueSelectAll = document.querySelector('.issue-checkbox-all'); @@ -223,11 +224,28 @@ async function initIssuePinSort() { }); } +function initMiscActionsButton(btn: HTMLButtonElement) { + const elPanel = btn.nextElementSibling; + createTippy(btn, { + content: elPanel, + trigger: 'click', + placement: 'bottom-end', + interactive: true, + hideOnClick: true, + arrow: false, + }); +} + +async function initRepoIssueMiscActions() { + queryElems(document, '.js-btn-misc-actions', initMiscActionsButton); +} + export function initRepoIssueList() { if (document.querySelector('.page-content.repository.issue-list, .page-content.repository.milestone-issue-list')) { initRepoIssueListCheckboxes(); queryElems(document, '.ui.dropdown.user-remote-search', (el) => initDropdownUserRemoteSearch(el)); initIssuePinSort(); + initRepoIssueMiscActions(); } else if (document.querySelector('.page-content.dashboard.issues')) { // user or org home: issue list, pull request list queryElems(document, '.ui.dropdown.user-remote-search', (el) => initDropdownUserRemoteSearch(el)); From 2b003bb9ccdce462f0c796b2b491e2723a90c766 Mon Sep 17 00:00:00 2001 From: Timotheus Pokorra Date: Tue, 18 Nov 2025 14:45:45 +0000 Subject: [PATCH 05/23] fix layout of menu item for export to excel --- templates/repo/issue/filter_actions.tmpl | 14 +++++++++++++ templates/repo/issue/filters.tmpl | 6 ++++-- templates/repo/issue/list.tmpl | 25 ------------------------ 3 files changed, 18 insertions(+), 27 deletions(-) diff --git a/templates/repo/issue/filter_actions.tmpl b/templates/repo/issue/filter_actions.tmpl index 8e2410393d871..6f1813c3469f0 100644 --- a/templates/repo/issue/filter_actions.tmpl +++ b/templates/repo/issue/filter_actions.tmpl @@ -123,6 +123,20 @@ {{end}} + + + {{end}} diff --git a/templates/repo/issue/filters.tmpl b/templates/repo/issue/filters.tmpl index 5115031db1c9e..91c1774ce06a8 100644 --- a/templates/repo/issue/filters.tmpl +++ b/templates/repo/issue/filters.tmpl @@ -27,8 +27,10 @@
- diff --git a/templates/repo/issue/list.tmpl b/templates/repo/issue/list.tmpl index fe7d59b530802..1fe220e1b8b80 100644 --- a/templates/repo/issue/list.tmpl +++ b/templates/repo/issue/list.tmpl @@ -31,12 +31,10 @@ {{ctx.Locale.Tr "action.compare_commits_general"}} {{end}} {{end}} -
{{template "repo/issue/filters" .}} -
{{template "repo/issue/openclose" .}} @@ -52,30 +50,7 @@
{{template "repo/issue/filter_actions" .}} - - -
- - - - - - -
-
{{template "shared/issuelist" dict "." . "listType" "repo"}} From c004ba809dfb82e30ee6215dbb98d4c49d269531 Mon Sep 17 00:00:00 2001 From: Timotheus Pokorra Date: Tue, 25 Nov 2025 13:23:54 +0000 Subject: [PATCH 06/23] fix lint-templates: do not use entity references --- templates/repo/issue/filter_actions.tmpl | 2 +- templates/repo/issue/filters.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/repo/issue/filter_actions.tmpl b/templates/repo/issue/filter_actions.tmpl index 6f1813c3469f0..2de0089ed26cb 100644 --- a/templates/repo/issue/filter_actions.tmpl +++ b/templates/repo/issue/filter_actions.tmpl @@ -126,7 +126,7 @@ - - - {{end}} From 305069a712988ca3f4fc3407f2bed62cfca89b44 Mon Sep 17 00:00:00 2001 From: Timotheus Pokorra Date: Tue, 9 Dec 2025 13:28:39 +0000 Subject: [PATCH 19/23] fix lint-frontend error --- web_src/js/features/repo-issue-list.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_src/js/features/repo-issue-list.ts b/web_src/js/features/repo-issue-list.ts index 485ca8657824e..3623f944282cb 100644 --- a/web_src/js/features/repo-issue-list.ts +++ b/web_src/js/features/repo-issue-list.ts @@ -225,7 +225,7 @@ async function initIssuePinSort() { } function initMiscActionsButton(btn: HTMLButtonElement) { - const elPanel = btn.nextElementSibling; + const elPanel = btn.nextElementSibling!; createTippy(btn, { content: elPanel, trigger: 'click', From 7f7ddfffe62f10a65f5540d62db22342889e5f56 Mon Sep 17 00:00:00 2001 From: Timotheus Pokorra Date: Tue, 9 Dec 2025 13:52:25 +0000 Subject: [PATCH 20/23] add a test for exporting the issues to excel --- tests/integration/issue_test.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go index 0da08796dcb39..4a9be2c7a2ef8 100644 --- a/tests/integration/issue_test.go +++ b/tests/integration/issue_test.go @@ -371,6 +371,27 @@ func TestIssueReaction(t *testing.T) { session.MakeRequest(t, req, http.StatusOK) } +func TestIssueListExport(t *testing.T) { + defer tests.PrepareTestEnv(t)() + session := loginUser(t, "user2") + _ = testNewIssue(t, session, "user2", "repo1", "Title", "Description") + _ = testNewIssue(t, session, "user2", "repo1", "Title2", "Description2") + _ = testNewIssue(t, session, "user2", "repo1", "Title3", "Description3") + + // trying to export all open issues of the given repository + req := NewRequestWithValues(t, "GET", fmt.Sprintf("/%s/%s/issues/export?%s", "user2", "repo1", "type=all&state=open")) + resp := session.MakeRequest(t, req, http.StatusOK) + + // Content-Type should be an Excel file (XLSX) + ct := strings.Split(resp.Header().Get("Content-Type"), ";")[0] + assert.Equal(t, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ct) + + // Content-Disposition should indicate attachment with .xlsx + cd := resp.Header().Get("Content-Disposition") + assert.Contains(t, cd, "attachment") + assert.True(t, strings.Contains(cd, ".xlsx")) +} + func TestIssueCrossReference(t *testing.T) { defer tests.PrepareTestEnv(t)() From 56d253571a613a515351d6e4b6ae18011be50de3 Mon Sep 17 00:00:00 2001 From: Timotheus Pokorra Date: Tue, 9 Dec 2025 14:08:13 +0000 Subject: [PATCH 21/23] improve test for exporting of issues: check content of excel file --- tests/integration/issue_test.go | 39 ++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go index 4a9be2c7a2ef8..c62eac5d2d154 100644 --- a/tests/integration/issue_test.go +++ b/tests/integration/issue_test.go @@ -4,6 +4,7 @@ package integration import ( + "bytes" "fmt" "html/template" "net/http" @@ -29,6 +30,7 @@ import ( "github.com/PuerkitoBio/goquery" "github.com/stretchr/testify/assert" + "github.com/xuri/excelize/v2" ) func getIssuesSelection(t testing.TB, htmlDoc *HTMLDoc) *goquery.Selection { @@ -374,7 +376,7 @@ func TestIssueReaction(t *testing.T) { func TestIssueListExport(t *testing.T) { defer tests.PrepareTestEnv(t)() session := loginUser(t, "user2") - _ = testNewIssue(t, session, "user2", "repo1", "Title", "Description") + _ = testNewIssue(t, session, "user2", "repo1", "Title1", "Description1") _ = testNewIssue(t, session, "user2", "repo1", "Title2", "Description2") _ = testNewIssue(t, session, "user2", "repo1", "Title3", "Description3") @@ -390,6 +392,41 @@ func TestIssueListExport(t *testing.T) { cd := resp.Header().Get("Content-Disposition") assert.Contains(t, cd, "attachment") assert.True(t, strings.Contains(cd, ".xlsx")) + + // open bytes as XLSX with excelize + data := resp.Body.Bytes() + f, err := excelize.OpenReader(bytes.NewReader(data)) + assert.NoError(t, err) + defer func() { _ = f.Close() }() + + // get first sheet and rows + sheets := f.GetSheetList() + assert.NotEmpty(t, sheets) + rows, err := f.GetRows(sheets[0]) + assert.NoError(t, err) + // there should be at least a header row + our three issues + assert.GreaterOrEqual(t, len(rows), 4) + + // ensure header has some cells and that our created titles appear somewhere + foundTitle1, foundTitle2, foundTitle3 := false, false, false + if len(rows) > 0 { + for _, row := range rows { + for _, cell := range row { + if strings.Contains(cell, "Title1") { + foundTitle1 = true + } + if strings.Contains(cell, "Title2") { + foundTitle2 = true + } + if strings.Contains(cell, "Title3") { + foundTitle3 = true + } + } + } + } + assert.True(t, foundTitle1, "Exported XLSX should contain Title1") + assert.True(t, foundTitle2, "Exported XLSX should contain Title2") + assert.True(t, foundTitle3, "Exported XLSX should contain Title3") } func TestIssueCrossReference(t *testing.T) { From 056a0d6c65307128b40a4a4d4c4e1f52e716d14c Mon Sep 17 00:00:00 2001 From: Timotheus Pokorra Date: Tue, 9 Dec 2025 14:17:41 +0000 Subject: [PATCH 22/23] fix exporting issues: output to correct row --- services/export/excel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/export/excel.go b/services/export/excel.go index 83c5cf6d390d8..f86692bf1d20a 100644 --- a/services/export/excel.go +++ b/services/export/excel.go @@ -73,7 +73,7 @@ func IssuesToExcel(ctx *context.Context, issues issues_model.IssueList) *exceliz } } - cell, _ := excelize.CoordinatesToCellName(1, i+1) + cell, _ := excelize.CoordinatesToCellName(1, i+2) err = sw.SetRow(cell, []any{ excelize.Cell{Value: issue.Index}, excelize.Cell{Value: issue.Title}, From 0a93b9ed34b09b4715b97c7f32545ffa01674a38 Mon Sep 17 00:00:00 2001 From: Timotheus Pokorra Date: Tue, 9 Dec 2025 15:53:34 +0000 Subject: [PATCH 23/23] fix for testing export of issues --- tests/integration/issue_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go index c62eac5d2d154..15dd863b3dbb6 100644 --- a/tests/integration/issue_test.go +++ b/tests/integration/issue_test.go @@ -381,7 +381,7 @@ func TestIssueListExport(t *testing.T) { _ = testNewIssue(t, session, "user2", "repo1", "Title3", "Description3") // trying to export all open issues of the given repository - req := NewRequestWithValues(t, "GET", fmt.Sprintf("/%s/%s/issues/export?%s", "user2", "repo1", "type=all&state=open")) + req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/issues/export?%s", "user2", "repo1", "type=all&state=open")) resp := session.MakeRequest(t, req, http.StatusOK) // Content-Type should be an Excel file (XLSX) @@ -391,7 +391,7 @@ func TestIssueListExport(t *testing.T) { // Content-Disposition should indicate attachment with .xlsx cd := resp.Header().Get("Content-Disposition") assert.Contains(t, cd, "attachment") - assert.True(t, strings.Contains(cd, ".xlsx")) + assert.Contains(t, cd, ".xlsx") // open bytes as XLSX with excelize data := resp.Body.Bytes()