Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e998c494b2 | |||
| eafd5320e3 | |||
| 1ed8e463df | |||
| cd0a803341 | |||
| 278645113b | |||
| 7e312077e7 | |||
| 40130d1793 | |||
| 79724b5bc9 | |||
| 88e210bffb | |||
| f73f628403 | |||
| c07443aa87 | |||
| c1c7556916 | |||
| 889f64009b |
@@ -3,6 +3,51 @@
|
||||
This changelog goes through the changes that have been made in each release
|
||||
without substantial changes to our git log; to see the highlights of what has
|
||||
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
## [v1.26.1-moko.05.15.00] - 2026-05-31
|
||||
|
||||
* BREAKING CHANGES
|
||||
* Deprecated Issue.Ref branch selector UI (#307)
|
||||
* Removed branch/tag selector from issue sidebar and new issue form
|
||||
* Removed ref badge from issue lists
|
||||
* Removed POST /ref web route and UpdateIssueRef handler
|
||||
* DB column and commit-close logic preserved for backward compatibility
|
||||
* API create/edit still accept `ref` field (no-op) for backward compat
|
||||
* FEATURES
|
||||
* feat(ui): add generic combo-multiselect component (#361)
|
||||
* Reusable dropdown with search, checkable items, and selected-items display
|
||||
* Template: `shared/combolist.tmpl` — accepts Items, Name, Title, SelectedValues
|
||||
* Decoupled from issue sidebar — works in any form context
|
||||
* feat(updates): extension metadata settings for update feed generation
|
||||
* feat(licenses): platform enforcement, key deletion, expired key cleanup
|
||||
* feat(licenses): store keys in plaintext, show full key with copy button
|
||||
* TECH DEBT
|
||||
* chore: full namespace migration from git.mokoconsulting.tech to code.mokoconsulting.tech (#336, #337, #344)
|
||||
* Go module path, all imports, template URLs, workflow configs (2,276 files)
|
||||
* fix(blame): set HasSourceRenderedToggle for renderable files (#344)
|
||||
* fix(settings): translate team permission strings via data-locale attributes (#344)
|
||||
* fix(dropzone): use relative path for non-image attachment markdown links (#344)
|
||||
* fix(templates): add required validation to issue dropdown fields (#350)
|
||||
* refactor(ts): remove redundant `handled` field from MarkdownHandleIndentionResult (#350)
|
||||
* refactor(go): rename HasOrgOrUserVisible to IsOwnerVisibleToDoer (#350)
|
||||
* refactor(go): replace ValuesRepository with maps.Values (Go 1.21+) (#357)
|
||||
* refactor(go): remove CanEnableEditor wrapper, use CanContentChange directly (#357)
|
||||
* fix(ts): parseIssueHref now uses URL pathname and trims appSubUrl (#360)
|
||||
* fix(actions): enforce MaxJobNumPerRun (256) limit when creating jobs (#360)
|
||||
* fix(css): use calc(infinity * 1px) for --border-radius-full (#361)
|
||||
* fix(css): remove legacy .center class from 2015, replace with tw-text-center (#361)
|
||||
* chore: remove stale TODO from OAuth2 regenerate secret (already implemented) (#332)
|
||||
* chore: remove stale pull request test stub TODOs (#328)
|
||||
* chore: remove stale GetProjectsMode TODO
|
||||
* chore: remove stale mustNotBeArchived/mustEnableEditor FIXME from API
|
||||
* fix(routes): remove dead legacy /cherry-pick/{sha} route (replaced by /_cherrypick/)
|
||||
* fix(feed): use full ref name instead of ShortName for file feed revision
|
||||
* BUGFIXES
|
||||
* fix(build): use slices.Collect for maps.Values (Go 1.23+ compat)
|
||||
* fix(licenses): remove duplicate DeleteLicenseKey declaration
|
||||
* fix(licenses): only show licenses tab when licensing is enabled
|
||||
* fix(licenses): show feed URLs based on repo update platform setting
|
||||
* fix(updates): correct dlid prefix and align XML with Joomla standard
|
||||
|
||||
## [v1.26.1-moko.05.06.00] - 2026-05-30
|
||||
|
||||
* FEATURES
|
||||
|
||||
@@ -77,6 +77,9 @@ RUN addgroup \
|
||||
COPY --from=build-env /tmp/local /
|
||||
COPY --from=build-env /go/src/code.mokoconsulting.tech/MokoConsulting/MokoGitea/gitea /app/gitea/gitea
|
||||
|
||||
# Disable openssh s6 service — we use external SSH (port 2222 via host).
|
||||
RUN printf '#!/bin/sh\nexec sleep infinity\n' > /etc/s6/openssh/run && chmod 755 /etc/s6/openssh/run
|
||||
|
||||
ENV USER=git
|
||||
ENV GITEA_CUSTOM=/data/gitea
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
|
||||
// MaxJobNumPerRun is the maximum number of jobs in a single run.
|
||||
// https://docs.github.com/en/actions/reference/limits#existing-system-limits
|
||||
// TODO: check this limit when creating jobs
|
||||
const MaxJobNumPerRun = 256
|
||||
|
||||
// ActionRunJob represents a job of a run
|
||||
|
||||
@@ -80,10 +80,6 @@ func testPullRequestLoadHeadRepo(t *testing.T) {
|
||||
assert.Equal(t, pr.HeadRepoID, pr.HeadRepo.ID)
|
||||
}
|
||||
|
||||
// TODO TestMerge
|
||||
|
||||
// TODO TestNewPullRequest
|
||||
|
||||
func testPullRequestsNewest(t *testing.T) {
|
||||
prs, count, err := issues_model.PullRequests(t.Context(), 1, &issues_model.PullRequestsOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
@@ -274,8 +270,6 @@ func testPullRequestUpdateCols(t *testing.T) {
|
||||
unittest.CheckConsistencyFor(t, pr)
|
||||
}
|
||||
|
||||
// TODO TestAddTestPullRequestTask
|
||||
|
||||
func testPullRequestIsWorkInProgress(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||
pr.LoadIssue(t.Context())
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
|
||||
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/container"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/optional"
|
||||
@@ -47,12 +48,12 @@ func (repos RepositoryList) Swap(i, j int) {
|
||||
|
||||
// ValuesRepository converts a repository map to a list
|
||||
func ValuesRepository(m map[int64]*Repository) []*Repository {
|
||||
return maps.Values(m)
|
||||
return slices.Collect(maps.Values(m))
|
||||
}
|
||||
|
||||
// RepositoryListOfMap make list from values of map
|
||||
func RepositoryListOfMap(repoMap map[int64]*Repository) RepositoryList {
|
||||
return RepositoryList(maps.Values(repoMap))
|
||||
return RepositoryList(slices.Collect(maps.Values(repoMap)))
|
||||
}
|
||||
|
||||
func (repos RepositoryList) LoadUnits(ctx context.Context) error {
|
||||
|
||||
@@ -195,7 +195,6 @@ type ProjectsConfig struct {
|
||||
|
||||
// FromDB fills up a ProjectsConfig from serialized format.
|
||||
func (cfg *ProjectsConfig) FromDB(bs []byte) error {
|
||||
// TODO: remove GetProjectsMode, only use ProjectsMode
|
||||
cfg.ProjectsMode = ProjectsModeAll
|
||||
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
}
|
||||
@@ -205,11 +204,11 @@ func (cfg *ProjectsConfig) ToDB() ([]byte, error) {
|
||||
return json.Marshal(cfg)
|
||||
}
|
||||
|
||||
// GetProjectsMode returns the configured projects mode, defaulting to "all".
|
||||
func (cfg *ProjectsConfig) GetProjectsMode() ProjectsMode {
|
||||
if cfg.ProjectsMode != "" {
|
||||
return cfg.ProjectsMode
|
||||
}
|
||||
|
||||
return ProjectsModeAll
|
||||
}
|
||||
|
||||
|
||||
@@ -3075,6 +3075,7 @@
|
||||
"admin.dashboard.reinit_missing_repos": "Reinitialize all missing Git repositories for which records exist",
|
||||
"admin.dashboard.sync_external_users": "Synchronize external user data",
|
||||
"admin.dashboard.cleanup_hook_task_table": "Clean up hook_task table",
|
||||
"admin.dashboard.cleanup_expired_license_keys": "Clean up expired license keys (older than 1 year)",
|
||||
"admin.dashboard.cleanup_packages": "Clean up expired packages",
|
||||
"admin.dashboard.cleanup_actions": "Clean up expired actions' resources",
|
||||
"admin.dashboard.server_uptime": "Server Uptime",
|
||||
|
||||
@@ -731,7 +731,6 @@ func mustEnableWiki(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: for consistency, maybe most mustNotBeArchived checks should be replaced with mustEnableEditor
|
||||
func mustNotBeArchived(ctx *context.APIContext) {
|
||||
if ctx.Repo.Repository.IsArchived {
|
||||
ctx.APIError(http.StatusLocked, fmt.Errorf("%s is archived", ctx.Repo.Repository.FullName()))
|
||||
|
||||
@@ -22,7 +22,7 @@ func ShowFileFeed(ctx *context.Context, repo *repo.Repository, formatType string
|
||||
}
|
||||
commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange(
|
||||
git.CommitsByFileAndRangeOptions{
|
||||
Revision: ctx.Repo.RefFullName.ShortName(), // FIXME: legacy code used ShortName
|
||||
Revision: ctx.Repo.RefFullName.String()
|
||||
File: fileName,
|
||||
Page: 1,
|
||||
})
|
||||
|
||||
@@ -306,29 +306,7 @@ func UpdateIssueTitle(ctx *context.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateIssueRef change issue's ref (branch)
|
||||
func UpdateIssueRef(ctx *context.Context) {
|
||||
issue := GetActionIssue(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)) || issue.IsPull {
|
||||
ctx.HTTPError(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
ref := ctx.FormTrim("ref")
|
||||
|
||||
if err := issue_service.ChangeIssueRef(ctx, issue, ctx.Doer, ref); err != nil {
|
||||
ctx.ServerError("ChangeRef", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"ref": ref,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateIssueContent change issue's content
|
||||
func UpdateIssueContent(ctx *context.Context) {
|
||||
|
||||
@@ -672,8 +672,6 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID int64, projectI
|
||||
}
|
||||
ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
|
||||
|
||||
ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, ctx.Repo.RepoLink)
|
||||
|
||||
ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 {
|
||||
counts, ok := approvalCounts[issueID]
|
||||
if !ok || len(counts) == 0 {
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/base"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/container"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
|
||||
issue_template "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/issue/template"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
|
||||
@@ -85,12 +84,6 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles
|
||||
}
|
||||
metaData.AssigneesData.SelectedAssigneeIDs = strings.Join(selectedAssigneeIDStrings, ",")
|
||||
|
||||
if template.Ref != "" && !strings.HasPrefix(template.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/<ref>
|
||||
template.Ref = git.BranchPrefix + template.Ref
|
||||
}
|
||||
|
||||
ctx.Data["Reference"] = template.Ref
|
||||
ctx.Data["RefEndName"] = git.RefName(template.Ref).ShortName()
|
||||
return true, templateErrs
|
||||
}
|
||||
return false, templateErrs
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/unit"
|
||||
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/emoji"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/markup"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/markup/markdown"
|
||||
@@ -397,14 +396,12 @@ func ViewIssue(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["Reference"] = issue.Ref
|
||||
ctx.Data["SignInLink"] = middleware.RedirectLinkUserLogin(ctx.Req)
|
||||
ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID)
|
||||
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)
|
||||
ctx.Data["HasProjectsWritePermission"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
|
||||
ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.Permission.IsAdmin() || ctx.Doer.IsAdmin)
|
||||
ctx.Data["LockReasons"] = setting.Repository.Issue.LockReasons
|
||||
ctx.Data["RefEndName"] = git.RefName(issue.Ref).ShortName()
|
||||
|
||||
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
|
||||
@@ -39,7 +39,6 @@ import (
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/routers/web/shared/user"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
feed_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/feed"
|
||||
issue_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/issue"
|
||||
pull_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/pull"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
@@ -587,8 +586,6 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
||||
|
||||
ctx.Data["IsShowClosed"] = isShowClosed
|
||||
|
||||
ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, ctx.FormString("RepoLink"))
|
||||
|
||||
if err := issues.LoadAttributes(ctx); err != nil {
|
||||
ctx.ServerError("issues.LoadAttributes", err)
|
||||
return
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
issue_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/issue"
|
||||
pull_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/pull"
|
||||
)
|
||||
|
||||
@@ -255,7 +254,6 @@ func NotificationSubscriptions(ctx *context.Context) {
|
||||
ctx.Data["CommitLastStatus"] = lastStatus
|
||||
ctx.Data["CommitStatuses"] = commitStatuses
|
||||
ctx.Data["Issues"] = issues
|
||||
ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, "")
|
||||
|
||||
approvalCounts, err := issues.GetApprovalCounts(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -595,7 +595,6 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Group("", func() {
|
||||
m.Get("/authorize", web.Bind(forms.AuthorizationForm{}), auth.AuthorizeOAuth)
|
||||
m.Post("/grant", web.Bind(forms.GrantApplicationForm{}), auth.GrantApplicationOAuth)
|
||||
// TODO manage redirection
|
||||
m.Post("/authorize", web.Bind(forms.AuthorizationForm{}), auth.AuthorizeOAuth)
|
||||
}, reqSignIn)
|
||||
|
||||
@@ -1353,7 +1352,6 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Post("/content", repo.UpdateIssueContent)
|
||||
m.Post("/deadline", repo.UpdateIssueDeadline)
|
||||
m.Post("/watch", repo.IssueWatch)
|
||||
m.Post("/ref", repo.UpdateIssueRef)
|
||||
m.Post("/pin", reqRepoAdmin, repo.IssuePinOrUnpin)
|
||||
m.Post("/viewed-files", repo.UpdateViewedFiles)
|
||||
m.Group("/dependency", func() {
|
||||
@@ -1750,8 +1748,6 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
m.Get("/commit/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff)
|
||||
m.Get("/commit/{sha:([a-f0-9]{7,64})$}/load-branches-and-tags", repo.LoadBranchesAndTags)
|
||||
|
||||
// FIXME: this route `/cherry-pick/{sha}` doesn't seem useful or right, the new code always uses `/_cherrypick/` which could handle branch name correctly
|
||||
m.Get("/cherry-pick/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, context.RepoRefByDefaultBranch(), repo.CherryPick)
|
||||
}, repo.MustBeNotEmpty)
|
||||
|
||||
m.Get("/rss/branch/*", context.RepoRefByType(git.RefTypeBranch), webAuth.AllowBasic, feedEnabled, feed.RenderBranchFeedRSS)
|
||||
|
||||
@@ -82,6 +82,10 @@ func InsertRun(ctx context.Context, run *actions_model.ActionRun, content []byte
|
||||
return fmt.Errorf("parse workflow: %w", err)
|
||||
}
|
||||
|
||||
if len(jobs) > actions_model.MaxJobNumPerRun {
|
||||
return fmt.Errorf("workflow has too many jobs (%d), maximum is %d", len(jobs), actions_model.MaxJobNumPerRun)
|
||||
}
|
||||
|
||||
titleChanged := len(jobs) > 0 && jobs[0].RunName != ""
|
||||
if titleChanged {
|
||||
run.Title = util.EllipsisDisplayString(jobs[0].RunName, 255)
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
{{ctx.Locale.Tr "admin.emails.change_email_header"}}
|
||||
</div>
|
||||
<form class="content ui form" action="{{AppSubUrl}}/-/admin/emails/activate" method="post">
|
||||
<p class="center">{{ctx.Locale.Tr "admin.emails.change_email_text"}}</p>
|
||||
<p class="tw-text-center">{{ctx.Locale.Tr "admin.emails.change_email_text"}}</p>
|
||||
|
||||
<input type="hidden" name="sort" value="{{.SortType}}">
|
||||
<input type="hidden" name="q" value="{{.Keyword}}">
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home_title"}}{{end}}" class="page-content home">
|
||||
<div class="tw-mb-8 tw-px-8">
|
||||
<div class="center">
|
||||
<div class="tw-text-center">
|
||||
<img class="logo" width="220" height="220" src="{{AssetUrlPrefix}}/img/login-logo.png" alt="{{ctx.Locale.Tr "logo"}}" onerror="this.style.display='none'">
|
||||
<div class="hero">
|
||||
<h1 class="ui icon header title tw-text-balance">
|
||||
|
||||
@@ -275,7 +275,7 @@
|
||||
<summary class="right-content tw-py-2{{if .Err_Admin}} tw-text-red{{end}}">
|
||||
{{ctx.Locale.Tr "install.admin_title"}}
|
||||
</summary>
|
||||
<p class="center">{{ctx.Locale.Tr "install.admin_setting_desc"}}</p>
|
||||
<p class="tw-text-center">{{ctx.Locale.Tr "install.admin_setting_desc"}}</p>
|
||||
<div class="inline field {{if .Err_AdminName}}error{{end}}">
|
||||
<label for="admin_name">{{ctx.Locale.Tr "install.admin_name"}}</label>
|
||||
<input id="admin_name" name="admin_name" value="{{.admin_name}}">
|
||||
|
||||
@@ -1,63 +1 @@
|
||||
{{/* TODO: RemoveIssueRef: the Issue.Ref will be removed in 1.24 or 1.25 if no end user really needs it or there could be better alternative then.
|
||||
PR: https://github.com/go-gitea/gitea/pull/32744
|
||||
|
||||
The Issue.Ref was added by Add possibility to record branch or tag information in an issue (#780)
|
||||
After 8 years, this "branch selector" does nothing more than saving the branch/tag name into database and displays it,
|
||||
or sometimes auto-close a ref-matched issue by a commit message when CloseIssuesViaCommitInAnyBranch=false.
|
||||
|
||||
There are still users using it:
|
||||
* @didim99: it is a really useful feature to specify a branch in which issue found.
|
||||
|
||||
Still needs to figure out:
|
||||
* Could the "recording branch/tag name" be replaced by other approaches?
|
||||
* Write the branch name in the issue title/body then it will still be displayed, eg: `[bug] (fix/ui-broken-bug) there is a bug ....`
|
||||
* Is "GitHub-like development sidebar (`#31899`)" good enough (or better) for your usage?
|
||||
*/}}
|
||||
{{if and (not .Issue.IsPull) (not .PageIsComparePull)}}
|
||||
<input id="ref_selector" name="ref" type="hidden" value="{{.Reference}}">
|
||||
<div class="ui dropdown select-branch branch-selector-dropdown ellipsis-text-items {{if not .HasIssuesOrPullsWritePermission}}disabled{{end}}"
|
||||
data-no-results="{{ctx.Locale.Tr "no_results_found"}}"
|
||||
{{if and .Issue (or .IsIssueWriter .HasIssuesOrPullsWritePermission)}}data-url-update-issueref="{{$.RepoLink}}/issues/{{.Issue.Index}}/ref"{{end}}
|
||||
>
|
||||
<div class="ui button branch-dropdown-button">
|
||||
<span class="text-branch-name gt-ellipsis">{{if .Reference}}{{$.RefEndName}}{{else}}{{ctx.Locale.Tr "repo.issues.no_ref"}}{{end}}</span>
|
||||
{{if .HasIssuesOrPullsWritePermission}}{{svg "octicon-triangle-down" 14 "dropdown icon"}}{{end}}
|
||||
</div>
|
||||
<div class="menu">
|
||||
<div class="ui icon search input">
|
||||
<i class="icon">{{svg "octicon-filter" 16}}</i>
|
||||
<input name="search" placeholder="{{ctx.Locale.Tr "repo.filter_branch_and_tag"}}...">
|
||||
</div>
|
||||
<div class="branch-tag-tab">
|
||||
<a class="branch-tag-item reference column muted active" href="#" data-target="#branch-list">
|
||||
{{svg "octicon-git-branch" 16 "tw-mr-1"}} {{ctx.Locale.Tr "repo.branches"}}
|
||||
</a>
|
||||
<a class="branch-tag-item reference column muted" href="#" data-target="#tag-list">
|
||||
{{svg "octicon-tag" 16 "tw-mr-1"}} {{ctx.Locale.Tr "repo.tags"}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="branch-tag-divider"></div>
|
||||
<div id="branch-list" class="scrolling menu reference-list-menu">
|
||||
{{if or .Reference (not .Issue)}}
|
||||
<div class="item tw-text-xs" data-id="" data-name="{{ctx.Locale.Tr "repo.issues.no_ref"}}" data-id-selector="#ref_selector"><strong><a href="#">{{ctx.Locale.Tr "repo.clear_ref"}}</a></strong></div>
|
||||
{{end}}
|
||||
{{range .Branches}}
|
||||
<div class="item" data-id="refs/heads/{{.}}" data-name="{{.}}" data-id-selector="#ref_selector" title="{{.}}">{{.}}</div>
|
||||
{{else}}
|
||||
<div class="item disabled">{{ctx.Locale.Tr "no_results_found"}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div id="tag-list" class="scrolling menu reference-list-menu tw-hidden">
|
||||
{{if or .Reference (not .Issue)}}
|
||||
<div class="item tw-text-xs" data-id="" data-name="{{ctx.Locale.Tr "repo.issues.no_ref"}}" data-id-selector="#ref_selector"><strong><a href="#">{{ctx.Locale.Tr "repo.clear_ref"}}</a></strong></div>
|
||||
{{end}}
|
||||
{{range .Tags}}
|
||||
<div class="item" data-id="refs/tags/{{.}}" data-name="tags/{{.}}" data-id-selector="#ref_selector">{{.}}</div>
|
||||
{{else}}
|
||||
<div class="item disabled">{{ctx.Locale.Tr "no_results_found"}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
{{end}}
|
||||
{{/* Issue.Ref branch selector removed — the field is deprecated. The DB column is kept for commit-close logic. */}}
|
||||
|
||||
@@ -47,8 +47,6 @@
|
||||
</div>
|
||||
|
||||
<div class="issue-content-right ui segment" data-global-init="initRepoIssueSidebar">
|
||||
{{template "repo/issue/branch_selector_field" $}}{{/* TODO: RemoveIssueRef: template "repo/issue/branch_selector_field" $*/}}
|
||||
|
||||
{{if .PageIsComparePull}}
|
||||
{{template "repo/issue/sidebar/reviewer_list" $.IssuePageMetaData}}
|
||||
<div class="divider"></div>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<div class="issue-content-right ui segment" data-global-init="initRepoIssueSidebar">
|
||||
{{template "repo/issue/branch_selector_field" $}}{{/* TODO: RemoveIssueRef: template "repo/issue/branch_selector_field" $*/}}
|
||||
|
||||
{{if .Issue.IsPull}}
|
||||
{{template "repo/issue/sidebar/reviewer_list" $.IssuePageMetaData}}
|
||||
{{template "repo/issue/sidebar/wip_switch" $}}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
{{/*
|
||||
Generic multiselect combo list component.
|
||||
Provides a dropdown with search, checkable items, and a selected-items display list.
|
||||
|
||||
Parameters:
|
||||
Name - form input name (required)
|
||||
Title - display label (required)
|
||||
Items - slice of items, each must have .Value and .Label fields
|
||||
SelectedValues - comma-separated string of selected values
|
||||
Placeholder - search input placeholder (optional, defaults to "Filter...")
|
||||
EmptyText - text when nothing is selected (optional)
|
||||
Disabled - whether the control is disabled (optional)
|
||||
Icon - gear icon shown next to title (optional, defaults to "octicon-gear")
|
||||
*/}}
|
||||
<div class="combo-multiselect" data-field-name="{{.Name}}">
|
||||
<input class="combo-value" name="{{.Name}}" type="hidden" value="{{.SelectedValues}}">
|
||||
<div class="ui dropdown full-width {{if .Disabled}}disabled{{end}}">
|
||||
<a class="fixed-text muted">
|
||||
<strong>{{.Title}}</strong> {{if not .Disabled}}{{svg (or .Icon "octicon-gear")}}{{end}}
|
||||
</a>
|
||||
<div class="menu">
|
||||
{{if .Items}}
|
||||
<div class="ui icon search input">
|
||||
<i class="icon">{{svg "octicon-search" 16}}</i>
|
||||
<input type="text" placeholder="{{or .Placeholder (ctx.Locale.Tr "search.filter")}}">
|
||||
</div>
|
||||
<div class="scrolling menu">
|
||||
<a class="item clear-selection" href="#">{{ctx.Locale.Tr "repo.issues.new.clear_labels"}}</a>
|
||||
<div class="divider"></div>
|
||||
{{range .Items}}
|
||||
<a class="item" data-value="{{.Value}}">
|
||||
<span class="item-check-mark">{{svg "octicon-check" 16}}</span>
|
||||
<span class="item-label">{{.Label}}</span>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="item disabled">{{or .EmptyText (ctx.Locale.Tr "repo.issues.new.no_items")}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui list combo-multiselect-list">
|
||||
<span class="item empty-list">{{or .EmptyText "None"}}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,13 +82,7 @@
|
||||
<span class="gt-ellipsis">{{$project.Title}}</span>
|
||||
</a>
|
||||
{{end}}
|
||||
{{if .Ref}}{{/* TODO: RemoveIssueRef: see "repo/issue/branch_selector_field.tmpl" */}}
|
||||
<a class="ref flex-text-inline tw-max-w-[300px]" {{if $.RepoLink}}href="{{index $.IssueRefURLs .ID}}"{{else}}href="{{.Repo.Link}}{{index $.IssueRefURLs .ID}}"{{end}}>
|
||||
{{svg "octicon-git-branch" 14}}
|
||||
<span class="gt-ellipsis">{{index $.IssueRefEndNames .ID}}</span>
|
||||
</a>
|
||||
{{end}}
|
||||
{{$tasks := .GetTasks}}
|
||||
{{$tasks := .GetTasks}}
|
||||
{{if gt $tasks 0}}
|
||||
{{$tasksDone := .GetTasksDone}}
|
||||
<span class="checklist flex-text-inline">
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "auth.send_reset_mail"}}</button>
|
||||
</div>
|
||||
{{else if .IsResetDisable}}
|
||||
<p class="center">
|
||||
<p class="tw-text-center">
|
||||
{{if $.IsAdmin}}
|
||||
{{ctx.Locale.Tr "auth.disable_forgot_password_mail_admin"}}
|
||||
{{else}}
|
||||
@@ -28,7 +28,7 @@
|
||||
{{end}}
|
||||
</p>
|
||||
{{else if .ResendLimited}}
|
||||
<p class="center">{{ctx.Locale.Tr "auth.resent_limit_prompt"}}</p>
|
||||
<p class="tw-text-center">{{ctx.Locale.Tr "auth.resent_limit_prompt"}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="center">{{ctx.Locale.Tr "auth.invalid_code_forgot_password" (printf "%s/user/forgot_password" AppSubUrl)}}</p>
|
||||
<p class="tw-text-center">{{ctx.Locale.Tr "auth.invalid_code_forgot_password" (printf "%s/user/forgot_password" AppSubUrl)}}</p>
|
||||
{{end}}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="item">
|
||||
<!-- TODO add regenerate secret functionality */ -->
|
||||
<form class="ui form ignore-dirty" action="{{.FormActionPath}}/regenerate_secret" method="post">
|
||||
{{ctx.Locale.Tr "settings.oauth2_regenerate_secret_hint"}}
|
||||
<button class="ui mini button tw-ml-2" type="submit">{{ctx.Locale.Tr "settings.oauth2_regenerate_secret"}}</button>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
/* other variables */
|
||||
--border-radius: 4px;
|
||||
--border-radius-medium: 6px;
|
||||
--border-radius-full: 99999px; /* TODO: use calc(infinity * 1px) */
|
||||
--border-radius-full: calc(infinity * 1px);
|
||||
--opacity-disabled: 0.55;
|
||||
--height-loading: 16rem;
|
||||
--min-height-textarea: 132px; /* padding + 6 lines + border = calc(1.57142em + 6lh + 2px), but lh is not fully supported */
|
||||
@@ -535,10 +535,6 @@ strong.attention-caution, svg.attention-caution {
|
||||
color: var(--color-error-text);
|
||||
}
|
||||
|
||||
/* FIXME: this is a longstanding dirty patch since 2015, it only makes the pages more messy and shouldn't be used */
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
overflow-menu {
|
||||
border-bottom: 1px solid var(--color-secondary) !important;
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
@import "./modules/codeeditor.css";
|
||||
@import "./modules/chroma.css";
|
||||
@import "./modules/charescape.css";
|
||||
@import "./modules/combo-multiselect.css";
|
||||
|
||||
@import "./shared/flex-list.css";
|
||||
@import "./shared/milestone.css";
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/* Styles for the generic combo-multiselect component (shared/combolist.tmpl) */
|
||||
|
||||
.combo-multiselect > .ui.dropdown .item:not(.checked) .item-check-mark {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.combo-multiselect > .ui.dropdown .item .item-check-mark {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.combo-multiselect > .combo-multiselect-list {
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
.combo-multiselect > .combo-multiselect-list > .item {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
margin: 2px;
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--color-label-bg);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.combo-multiselect > .combo-multiselect-list > .item.empty-list {
|
||||
background: none;
|
||||
color: var(--color-text-light-2);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright 2026 Moko Consulting. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
import {addDelegatedEventListener, queryElemChildren, queryElems, toggleElem} from '../utils/dom.ts';
|
||||
|
||||
/**
|
||||
* Generic multiselect combo list component.
|
||||
* Works with the "shared/combolist" template to provide a reusable
|
||||
* dropdown with search, checkable items, and a selected-items display list.
|
||||
*
|
||||
* Usage: add class="combo-multiselect" to the container element.
|
||||
* The component is self-contained — no backend calls, just updates the hidden input.
|
||||
*/
|
||||
class ComboMultiselect {
|
||||
container: HTMLElement;
|
||||
elDropdown: HTMLElement;
|
||||
elList: HTMLElement;
|
||||
elComboValue: HTMLInputElement;
|
||||
|
||||
constructor(container: HTMLElement) {
|
||||
this.container = container;
|
||||
this.elDropdown = container.querySelector<HTMLElement>(':scope > .ui.dropdown')!;
|
||||
this.elList = container.querySelector<HTMLElement>(':scope > .combo-multiselect-list')!;
|
||||
this.elComboValue = container.querySelector<HTMLInputElement>(':scope > .combo-value')!;
|
||||
}
|
||||
|
||||
collectCheckedValues(): string[] {
|
||||
return Array.from(
|
||||
this.elDropdown.querySelectorAll('.menu > .item.checked'),
|
||||
(el) => el.getAttribute('data-value')!,
|
||||
);
|
||||
}
|
||||
|
||||
updateUiList() {
|
||||
const checkedValues = this.collectCheckedValues();
|
||||
const elEmptyTip = this.elList.querySelector(':scope > .item.empty-list')!;
|
||||
queryElemChildren(this.elList, '.item:not(.empty-list)', (el) => el.remove());
|
||||
|
||||
for (const value of checkedValues) {
|
||||
const el = this.elDropdown.querySelector<HTMLElement>(`.menu > .item[data-value="${CSS.escape(value)}"]`);
|
||||
if (!el) continue;
|
||||
const labelText = el.querySelector('.item-label')?.textContent || value;
|
||||
const listItem = document.createElement('span');
|
||||
listItem.classList.add('item');
|
||||
listItem.textContent = labelText;
|
||||
this.elList.append(listItem);
|
||||
}
|
||||
|
||||
toggleElem(elEmptyTip, checkedValues.length === 0);
|
||||
this.elComboValue.value = checkedValues.join(',');
|
||||
this.elComboValue.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
}
|
||||
|
||||
onItemClick(elItem: HTMLElement, e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
if (elItem.matches('.clear-selection')) {
|
||||
queryElems(this.elDropdown, '.menu > .item', (el) => el.classList.remove('checked'));
|
||||
this.updateUiList();
|
||||
return;
|
||||
}
|
||||
|
||||
elItem.classList.toggle('checked');
|
||||
this.updateUiList();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Restore checked state from initial value
|
||||
const initialValues = this.elComboValue.value ? this.elComboValue.value.split(',') : [];
|
||||
for (const value of initialValues) {
|
||||
const elItem = this.elDropdown.querySelector<HTMLElement>(`.menu > .item[data-value="${CSS.escape(value)}"]`);
|
||||
elItem?.classList.add('checked');
|
||||
}
|
||||
this.updateUiList();
|
||||
|
||||
addDelegatedEventListener(this.elDropdown, 'click', '.item', (el, e) => this.onItemClick(el, e));
|
||||
|
||||
fomanticQuery(this.elDropdown).dropdown('setting', {
|
||||
action: 'nothing',
|
||||
fullTextSearch: 'exact',
|
||||
hideDividers: 'empty',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function initComboMultiselect() {
|
||||
queryElems(document, '.combo-multiselect', (el) => {
|
||||
if (el.hasAttribute('data-combo-inited')) return;
|
||||
el.setAttribute('data-combo-inited', 'true');
|
||||
new ComboMultiselect(el).init();
|
||||
});
|
||||
}
|
||||
@@ -9,33 +9,6 @@ import {showTemporaryTooltip} from '../modules/tippy.ts';
|
||||
|
||||
const {appSubUrl} = window.config;
|
||||
|
||||
function initRepoIssueBranchSelector(elSidebar: HTMLElement) {
|
||||
// TODO: RemoveIssueRef: see "repo/issue/branch_selector_field.tmpl"
|
||||
const elSelectBranch = elSidebar.querySelector('.ui.dropdown.select-branch.branch-selector-dropdown');
|
||||
if (!elSelectBranch) return;
|
||||
const urlUpdateIssueRef = elSelectBranch.getAttribute('data-url-update-issueref');
|
||||
const elBranchMenu = elSelectBranch.querySelector('.reference-list-menu')!;
|
||||
queryElems(elBranchMenu, '.item:not(.no-select)', (el) => el.addEventListener('click', async function (e) {
|
||||
e.preventDefault();
|
||||
const selectedValue = this.getAttribute('data-id')!; // eg: "refs/heads/my-branch"
|
||||
const selectedText = this.getAttribute('data-name'); // eg: "my-branch"
|
||||
if (urlUpdateIssueRef) {
|
||||
// for existing issue, send request to update issue ref, and reload page
|
||||
try {
|
||||
await POST(urlUpdateIssueRef, {data: new URLSearchParams({ref: selectedValue})});
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
} else {
|
||||
// for new issue, only update UI&form, do not send request/reload
|
||||
const selectedHiddenSelector = this.getAttribute('data-id-selector')!;
|
||||
document.querySelector<HTMLInputElement>(selectedHiddenSelector)!.value = selectedValue;
|
||||
elSelectBranch.querySelector('.text-branch-name')!.textContent = selectedText;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function initRepoIssueDue(elSidebar: HTMLElement) {
|
||||
const form = elSidebar.querySelector<HTMLFormElement>('.issue-due-form');
|
||||
if (!form) return;
|
||||
@@ -109,7 +82,6 @@ export function initRepoPullRequestAllowMaintainerEdit(elSidebar: HTMLElement) {
|
||||
|
||||
export function initRepoIssueSidebar() {
|
||||
registerGlobalInitFunc('initRepoIssueSidebar', (elSidebar) => {
|
||||
initRepoIssueBranchSelector(elSidebar);
|
||||
initRepoIssueDue(elSidebar);
|
||||
initRepoIssueSidebarDependency(elSidebar);
|
||||
initRepoPullRequestAllowMaintainerEdit(elSidebar);
|
||||
|
||||
@@ -22,6 +22,7 @@ import {initUserExternalLogins, initUserCheckAppUrl} from './features/user-auth.
|
||||
import {initRepoPullRequestReview, initRepoIssueFilterItemLabel} from './features/repo-issue.ts';
|
||||
import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts';
|
||||
import {initRepoTopicBar} from './features/repo-home.ts';
|
||||
import {initComboMultiselect} from './features/combo-multiselect.ts';
|
||||
import {initAdminCommon} from './features/admin/common.ts';
|
||||
import {initRepoCodeView} from './features/repo-code.ts';
|
||||
import {initSshKeyFormParser} from './features/sshkey-helper.ts';
|
||||
@@ -105,6 +106,7 @@ const initPerformanceTracer = callInitFunctions([
|
||||
initTableSort,
|
||||
initRepoFileSearch,
|
||||
initCopyContent,
|
||||
initComboMultiselect,
|
||||
|
||||
initAdminCommon,
|
||||
initAdminUserListSearchForm,
|
||||
|
||||
+5
-3
@@ -52,9 +52,11 @@ export function stripTags(text: string): string {
|
||||
}
|
||||
|
||||
export function parseIssueHref(href: string): IssuePathInfo {
|
||||
// FIXME: it should use pathname and trim the appSubUrl ahead
|
||||
const path = (href || '').replace(/[#?].*$/, '');
|
||||
const [_, ownerName, repoName, pathType, indexString] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(path) || [];
|
||||
let pathname = href || '';
|
||||
try { pathname = new URL(pathname, window.location.origin).pathname } catch {}
|
||||
const appSubUrl = window.config.appSubUrl;
|
||||
if (appSubUrl && pathname.startsWith(appSubUrl)) pathname = pathname.substring(appSubUrl.length);
|
||||
const [_, ownerName, repoName, pathType, indexString] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(pathname) || [];
|
||||
return {ownerName, repoName, pathType, indexString};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user