Allow multiple projects per issue and pull requests (#36784)

Add ability to add and remove multiple projects per issue
and pull request.

Resolve #12974

---------

Signed-off-by: Icy Avocado <avocado@ovacoda.com>
Co-authored-by: Tyrone Yeh <siryeh@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: OpenCode (gpt-5.2-codex) <opencode@openai.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
This commit is contained in:
Icy Avocado
2026-04-30 08:38:05 -06:00
committed by GitHub
parent 52d6baf5a8
commit 81692ceafa
58 changed files with 1597 additions and 430 deletions
+6 -1
View File
@@ -5,6 +5,7 @@ package templates
import (
"context"
"fmt"
"html/template"
"io"
"net/http"
@@ -36,7 +37,11 @@ func (r *pageRenderer) funcMapDummy() template.FuncMap {
}
func (r *pageRenderer) TemplateLookup(tmpl string, templateCtx context.Context) (TemplateExecutor, error) { //nolint:revive // we don't use ctx, only pass it to the template executor
return r.tmplRenderer.Templates().Executor(tmpl, r.funcMap(templateCtx))
tmpls := r.tmplRenderer.Templates()
if tmpls == nil {
return nil, fmt.Errorf("no templates defined for %s", tmpl)
}
return tmpls.Executor(tmpl, r.funcMap(templateCtx))
}
func (r *pageRenderer) HTML(w io.Writer, status int, tplName TplName, data any, templateCtx context.Context) error { //nolint:revive // we don't use ctx, only pass it to the template executor
+31
View File
@@ -6,6 +6,11 @@ package templates
import (
"fmt"
"reflect"
"slices"
"strconv"
"strings"
"code.gitea.io/gitea/modules/util"
)
type SliceUtils struct{}
@@ -33,3 +38,29 @@ func (su *SliceUtils) Contains(s, v any) bool {
}
return false
}
// JoinInt64 joins a slice of int64 values into a comma-separated string.
func (su *SliceUtils) JoinInt64(values []int64) string {
if len(values) == 0 {
return ""
}
strs := make([]string, len(values))
for i, v := range values {
strs[i] = strconv.FormatInt(v, 10)
}
return strings.Join(strs, ",")
}
func (su *SliceUtils) JoinToggleIDs(values []int64, target int64) (ret struct {
IsIncluded bool
ToggledIDs string
},
) {
ret.IsIncluded = slices.Contains(values, target)
if ret.IsIncluded {
ret.ToggledIDs = su.JoinInt64(util.SliceRemoveAll(slices.Clone(values), target))
} else {
ret.ToggledIDs = su.JoinInt64(append(values, target))
}
return ret
}
+10
View File
@@ -70,6 +70,16 @@ func TestUtils(t *testing.T) {
actual = execTmpl("{{StringUtils.Contains .String .Value}}", map[string]any{"String": "abc", "Value": "x"})
assert.Equal(t, "false", actual)
// Test JoinInt64
actual = execTmpl("{{SliceUtils.JoinInt64 .Values}}", map[string]any{"Values": []int64{1, 2, 3}})
assert.Equal(t, "1,2,3", actual)
actual = execTmpl("{{SliceUtils.JoinInt64 .Values}}", map[string]any{"Values": []int64{}})
assert.Empty(t, actual)
actual = execTmpl("{{SliceUtils.JoinInt64 .Values}}", map[string]any{"Values": []int64{42}})
assert.Equal(t, "42", actual)
tmpl := template.New("test")
tmpl.Funcs(template.FuncMap{"SliceUtils": NewSliceUtils, "StringUtils": NewStringUtils})
template.Must(tmpl.Parse("{{SliceUtils.Contains .Slice .Value}}"))