Compare commits

..

20 Commits

Author SHA1 Message Date
Jonathan Miller e998c494b2 fix: resolve tech-debt batch 7 — dead routes, stale FIXMEs, feed revision
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
PR RC Release / Build RC Release (pull_request) Successful in 20s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
- chore: remove stale mustNotBeArchived FIXME (CanEnableEditor no longer exists)
- fix(routes): remove dead /cherry-pick/{sha} route — replaced by /_cherrypick/
- fix(feed): use full ref name instead of ShortName for file feed revision

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 13:46:06 -05:00
Jonathan Miller eafd5320e3 fix(build): remove unused imports causing compile errors
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 13:41:35 -05:00
Jonathan Miller 1ed8e463df chore: remove stale TODO comments (#328, #332)
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 9s
Universal: PR Check / Branch Policy (pull_request) Successful in 9s
Universal: PR Check / Validate PR (pull_request) Failing after 17s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 34s
- Remove TODO from OAuth2 regenerate secret template — the
  functionality was already fully implemented
- Remove stub TODO test comments for TestMerge, TestNewPullRequest,
  TestAddTestPullRequestTask — stale for years
- Remove stale GetProjectsMode TODO — method is needed by templates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 13:38:18 -05:00
Jonathan Miller cd0a803341 fix(issues): deprecate Issue.Ref branch selector UI (#307)
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 24s
Remove the branch/tag selector from the issue sidebar and new issue
form. The Issue.Ref field was added 8 years ago and provides minimal
value — it only saves a branch name and optionally restricts which
branch's commits can auto-close the issue.

Removed:
- Branch selector template (branch_selector_field.tmpl)
- Sidebar and new-form includes
- Ref badge from issue lists
- POST /{index}/ref web route and UpdateIssueRef handler
- GetRefEndNamesAndURLs calls from list renderers
- JS handler for branch selector dropdown

Preserved:
- DB column (Issue.Ref) — still used by commit-close logic
- API response still includes ref for backward compatibility

Closes #307

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 13:31:11 -05:00
Jonathan Miller 278645113b fix(cron): add missing translation for cleanup_expired_license_keys
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 13:14:45 -05:00
Jonathan Miller 7e312077e7 fix(docker): disable openssh s6 service in Dockerfile
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 9s
Universal: PR Check / Branch Policy (pull_request) Successful in 8s
Universal: PR Check / Validate PR (pull_request) Failing after 18s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 43s
Bake the noop script directly into the image layer so openssh never
starts. We use external SSH (port 2222) via host networking, not the
container's sshd.

Fixes: #372

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 12:13:21 -05:00
Jonathan Miller 40130d1793 docs: update CHANGELOG for v05.15.00 dev cycle
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
PR RC Release / Build RC Release (pull_request) Successful in 20s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 11:55:21 -05:00
Jonathan Miller 79724b5bc9 feat(ui): add generic combo-multiselect component
Add a reusable multiselect dropdown component inspired by the issue
sidebar label picker, but decoupled from issue-specific logic.

Components:
- templates/shared/combolist.tmpl — generic template accepting Items,
  Name, Title, SelectedValues parameters
- web_src/js/features/combo-multiselect.ts — lightweight JS init that
  handles check/uncheck, search, and hidden input updates
- web_src/css/modules/combo-multiselect.css — check-mark visibility
  and selected-items list styling

Usage in any template:
  {{template "shared/combolist" dict
    "Name" "channels"
    "Title" "Update Channels"
    "Items" .AvailableChannels
    "SelectedValues" .SelectedChannelIDs
  }}

Items must have .Value and .Label fields.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 11:41:08 -05:00
Jonathan Miller 88e210bffb fix(build): use slices.Collect for maps.Values (Go 1.23+ compat)
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 6s
PR RC Release / Build RC Release (pull_request) Failing after 25s
In Go 1.23+, maps.Values returns iter.Seq not a slice.
Use slices.Collect() to materialize the iterator.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 11:38:00 -05:00
jmiller f73f628403 Merge pull request 'fix: tech-debt batch 5 — CSS cleanup' (#361) from fix/tech-debt-batch-5 into dev
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Failing after 21s
2026-05-31 16:19:35 +00:00
Jonathan Miller c07443aa87 fix: resolve tech-debt batch 5 — CSS cleanup
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
- fix(css): use calc(infinity * 1px) for --border-radius-full instead
  of magic 99999px value (supported in all modern browsers)
- fix(css): remove legacy .center class from 2015, replace 6 usages
  with tw-text-center utility class

Refs #318

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 11:19:01 -05:00
jmiller c1c7556916 Merge pull request 'fix: tech-debt batch 4 — parseIssueHref, job limit, stale TODOs' (#360) from fix/tech-debt-batch-4 into dev 2026-05-31 16:13:34 +00:00
Jonathan Miller 889f64009b fix: resolve tech-debt batch 4 — parseIssueHref, job limit, stale TODOs
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
- fix(ts): parseIssueHref now uses URL pathname and trims appSubUrl
  for correct issue link parsing with sub-path deployments
- fix(actions): enforce MaxJobNumPerRun (256) limit when creating jobs,
  rejecting workflows that exceed the GitHub-compatible limit
- chore: remove stale TODO comment on OAuth redirect route

Refs #325, #334

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 11:12:44 -05:00
Jonathan Miller 27c4d176f4 fix(licenses): remove duplicate DeleteLicenseKey declaration
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 26s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 11:06:41 -05:00
jmiller 53bcb3cdf9 Merge pull request 'fix: tech-debt batch 3 — remove deprecated functions, use stdlib' (#357) from fix/tech-debt-batch-3 into dev 2026-05-31 16:05:15 +00:00
Jonathan Miller b9b3026122 fix: resolve tech-debt batch 3 — remove deprecated functions, use stdlib
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 3s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
PR RC Release / Build RC Release (pull_request) Successful in 4s
Universal: PR Check / Validate PR (pull_request) Failing after 12s
Branch Cleanup / Delete merged branch (pull_request) Successful in 3s
- refactor(go): replace ValuesRepository with maps.Values (Go 1.21+)
- refactor(go): remove CanEnableEditor wrapper, use CanContentChange directly
- chore: remove stale TODO comments about project column route naming

Refs #311, #317

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 11:03:42 -05:00
Jonathan Miller d30e7d7a5a feat(updates): extension metadata settings for update feed generation
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Branch Cleanup / Delete merged branch (pull_request) Has been skipped
Universal: PR Check / Validate PR (pull_request) Failing after 7s
PR RC Release / Build RC Release (pull_request) Failing after 27s
- Add configurable fields: element name, display name, description,
  extension type, maintainer, maintainer URL, info URL, target version,
  PHP minimum
- Add platform dropdown: joomla, dolibarr, wordpress, prestashop,
  drupal, composer, both
- Update Joomla XML generator to use metadata from config (falls back
  to repo-derived values when not set)
- Add GetEffectiveConfig() for resolving repo → org → nil config chain
- Add locale keys for all new settings fields

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 10:58:45 -05:00
Jonathan Miller e0c69d792c fix(licenses): only show licenses tab when licensing is enabled
Uses LicensingEnabled (org config) instead of EnableLicenses/IsRepoAdmin.
Tab is now hidden when the org hasn't enabled the licensing system.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 10:53:06 -05:00
jmiller d4bb976544 Merge pull request 'fix: tech-debt batch 2 — dropdown validation, editor cleanup, rename' (#350) from fix/tech-debt-batch-2 into dev 2026-05-31 15:52:26 +00:00
Jonathan Miller d74ae92e5b fix: resolve tech-debt batch 2 — dropdown validation, editor cleanup, rename
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
- fix(templates): add required validation to issue dropdown fields
- refactor(ts): remove redundant `handled` field from MarkdownHandleIndentionResult
- refactor(go): rename HasOrgOrUserVisible to IsOwnerVisibleToDoer for clarity

Refs #311, #334

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 10:50:33 -05:00
59 changed files with 465 additions and 246 deletions
+45
View File
@@ -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
+3
View File
@@ -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
-1
View File
@@ -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
-6
View File
@@ -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())
-6
View File
@@ -178,12 +178,6 @@ func TouchHeartbeat(ctx context.Context, keyID int64) error {
return err
}
// DeleteLicenseKey deletes a license key by ID.
func DeleteLicenseKey(ctx context.Context, id int64) error {
_, err := db.GetEngine(ctx).ID(id).Delete(new(LicenseKey))
return err
}
// ValidateLicenseKey validates a raw key string against the database.
// Returns the key record and its associated package, or an error.
// The domain parameter is optional — when provided, it is checked against
+24 -1
View File
@@ -24,9 +24,19 @@ type UpdateStreamConfig struct {
OwnerID int64 `xorm:"INDEX NOT NULL"` // org or user
RepoID int64 `xorm:"INDEX NOT NULL DEFAULT 0"` // 0 = org-level default
StreamMode string `xorm:"NOT NULL DEFAULT 'joomla'"` // joomla, custom
Platform string `xorm:"NOT NULL DEFAULT 'joomla'"` // joomla, dolibarr, both
Platform string `xorm:"NOT NULL DEFAULT 'joomla'"` // joomla, dolibarr, both, wordpress, prestashop, drupal
LicensingEnabled bool `xorm:"NOT NULL DEFAULT false"` // master toggle for licensing system
RequireKey bool `xorm:"NOT NULL DEFAULT false"` // require license key for update feed
// Extension metadata — used in update feed generation.
ExtensionName string `xorm:"TEXT"` // element identifier (e.g. pkg_mokowaas, com_mokowaas)
DisplayName string `xorm:"TEXT"` // human-readable name (e.g. "Package - MokoWaaS")
Description string `xorm:"TEXT"` // short description for update feeds
ExtensionType string `xorm:"VARCHAR(50)"` // component, module, plugin, package, template, library
Maintainer string `xorm:"TEXT"` // maintainer/author name
MaintainerURL string `xorm:"TEXT"` // maintainer website
InfoURL string `xorm:"TEXT"` // extension info/product page URL
TargetVersion string `xorm:"TEXT"` // target platform version regex (e.g. "(5|6)\..*")
PHPMinimum string `xorm:"VARCHAR(20)"` // minimum PHP version (e.g. "8.1")
// CustomStreams is a JSON array of stream definitions.
// Each entry: {"name":"lts","suffix":"-lts","description":"Long-term support"}
CustomStreams string `xorm:"TEXT"`
@@ -121,6 +131,19 @@ func GetEffectiveStreams(ctx context.Context, ownerID, repoID int64) []StreamDef
return DefaultJoomlaStreams()
}
// GetEffectiveConfig returns the full config for a repo: repo override → org default.
func GetEffectiveConfig(ctx context.Context, ownerID, repoID int64) *UpdateStreamConfig {
repoCfg, err := GetRepoConfig(ctx, repoID)
if err == nil && repoCfg != nil {
return repoCfg
}
orgCfg, err := GetOrgConfig(ctx, ownerID)
if err == nil && orgCfg != nil {
return orgCfg
}
return nil
}
// SaveConfig creates or updates an update stream config.
func SaveConfig(ctx context.Context, cfg *UpdateStreamConfig) error {
existing := new(UpdateStreamConfig)
+3 -3
View File
@@ -417,8 +417,8 @@ func GetUsersWhoCanCreateOrgRepo(ctx context.Context, orgID int64) (map[int64]*u
And("team_user.org_id = ?", orgID).Find(&users)
}
// HasOrgOrUserVisible tells if the given user can see the given org or user
func HasOrgOrUserVisible(ctx context.Context, orgOrUser, user *user_model.User) bool {
// IsOwnerVisibleToDoer tells if the given user can see the given org or user
func IsOwnerVisibleToDoer(ctx context.Context, orgOrUser, user *user_model.User) bool {
// If user is nil, it's an anonymous user/request.
// The Ghost user is handled like an anonymous user.
if user == nil || user.IsGhost() {
@@ -446,7 +446,7 @@ func HasOrgsVisible(ctx context.Context, orgs []*Organization, user *user_model.
}
for _, org := range orgs {
if HasOrgOrUserVisible(ctx, org.AsUser(), user) {
if IsOwnerVisibleToDoer(ctx, org.AsUser(), user) {
return true
}
}
+11 -11
View File
@@ -378,18 +378,18 @@ func TestHasOrgVisibleTypePublic(t *testing.T) {
assert.NoError(t, organization.CreateOrganization(t.Context(), org, owner))
org = unittest.AssertExistsAndLoadBean(t,
&organization.Organization{Name: org.Name, Type: user_model.UserTypeOrganization})
test1 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), owner)
test2 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), org3)
test3 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), nil)
test1 := organization.IsOwnerVisibleToDoer(t.Context(), org.AsUser(), owner)
test2 := organization.IsOwnerVisibleToDoer(t.Context(), org.AsUser(), org3)
test3 := organization.IsOwnerVisibleToDoer(t.Context(), org.AsUser(), nil)
assert.True(t, test1) // owner of org
assert.True(t, test2) // user not a part of org
assert.True(t, test3) // logged out user
restrictedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29, IsRestricted: true})
require.True(t, restrictedUser.IsRestricted)
assert.True(t, organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), restrictedUser))
assert.True(t, organization.IsOwnerVisibleToDoer(t.Context(), org.AsUser(), restrictedUser))
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
assert.False(t, organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), restrictedUser))
assert.False(t, organization.IsOwnerVisibleToDoer(t.Context(), org.AsUser(), restrictedUser))
}
func TestHasOrgVisibleTypeLimited(t *testing.T) {
@@ -407,9 +407,9 @@ func TestHasOrgVisibleTypeLimited(t *testing.T) {
assert.NoError(t, organization.CreateOrganization(t.Context(), org, owner))
org = unittest.AssertExistsAndLoadBean(t,
&organization.Organization{Name: org.Name, Type: user_model.UserTypeOrganization})
test1 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), owner)
test2 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), org3)
test3 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), nil)
test1 := organization.IsOwnerVisibleToDoer(t.Context(), org.AsUser(), owner)
test2 := organization.IsOwnerVisibleToDoer(t.Context(), org.AsUser(), org3)
test3 := organization.IsOwnerVisibleToDoer(t.Context(), org.AsUser(), nil)
assert.True(t, test1) // owner of org
assert.True(t, test2) // user not a part of org
assert.False(t, test3) // logged out user
@@ -430,9 +430,9 @@ func TestHasOrgVisibleTypePrivate(t *testing.T) {
assert.NoError(t, organization.CreateOrganization(t.Context(), org, owner))
org = unittest.AssertExistsAndLoadBean(t,
&organization.Organization{Name: org.Name, Type: user_model.UserTypeOrganization})
test1 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), owner)
test2 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), org3)
test3 := organization.HasOrgOrUserVisible(t.Context(), org.AsUser(), nil)
test1 := organization.IsOwnerVisibleToDoer(t.Context(), org.AsUser(), owner)
test2 := organization.IsOwnerVisibleToDoer(t.Context(), org.AsUser(), org3)
test3 := organization.IsOwnerVisibleToDoer(t.Context(), org.AsUser(), nil)
assert.True(t, test1) // owner of org
assert.False(t, test2) // user not a part of org
assert.False(t, test3) // logged out user
+1 -2
View File
@@ -427,8 +427,7 @@ func GetIndividualUserRepoPermission(ctx context.Context, repo *repo_model.Repos
// Prevent strangers from checking out public repo of private organization/users
// Allow user if they are a collaborator of a repo within a private user or a private organization but not a member of the organization itself
// TODO: rename it to "IsOwnerVisibleToDoer"
if !organization.HasOrgOrUserVisible(ctx, repo.Owner, user) && !isCollaborator {
if !organization.IsOwnerVisibleToDoer(ctx, repo.Owner, user) && !isCollaborator {
perm.AccessMode = perm_model.AccessModeNone
return perm, nil
}
+1 -7
View File
@@ -634,13 +634,7 @@ func (repo *Repository) AllowsPulls(ctx context.Context) bool {
return repo.CanEnablePulls() && repo.UnitEnabled(ctx, unit.TypePullRequests)
}
// CanEnableEditor returns true if repository meets the requirements of web editor.
// FIXME: most CanEnableEditor calls should be replaced with CanContentChange
// And all other like CanCreateBranch / CanEnablePulls should also be updated
func (repo *Repository) CanEnableEditor() bool {
return repo.CanContentChange()
}
// CanContentChange returns true if repository content can be modified (not a mirror and not archived).
func (repo *Repository) CanContentChange() bool {
return !repo.IsMirror && !repo.IsArchived
}
+5 -7
View File
@@ -12,6 +12,9 @@ import (
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/perm"
"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"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
@@ -44,18 +47,13 @@ func (repos RepositoryList) Swap(i, j int) {
}
// ValuesRepository converts a repository map to a list
// FIXME: Remove in favor of maps.values when MIN_GO_VERSION >= 1.18
func ValuesRepository(m map[int64]*Repository) []*Repository {
values := make([]*Repository, 0, len(m))
for _, v := range m {
values = append(values, v)
}
return values
return slices.Collect(maps.Values(m))
}
// RepositoryListOfMap make list from values of map
func RepositoryListOfMap(repoMap map[int64]*Repository) RepositoryList {
return RepositoryList(ValuesRepository(repoMap))
return RepositoryList(slices.Collect(maps.Values(repoMap)))
}
func (repos RepositoryList) LoadUnits(ctx context.Context) error {
+1 -2
View File
@@ -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
}
+16
View File
@@ -2837,6 +2837,21 @@
"org.settings.enable_licensing_help": "Show the Licenses page in the org menu and enable license key management. Individual repos can also enable licensing independently.",
"org.settings.require_key": "Require license key for all update feeds",
"org.settings.require_key_help": "Update feeds return empty results unless a valid key is provided. Joomla clients will see a Download Key field. Individual repos can override this.",
"org.settings.extension_metadata": "Extension Metadata",
"org.settings.extension_metadata_desc": "Configure how this extension appears in update feeds. These fields are used when generating updates.xml, JSON feeds, and package metadata.",
"org.settings.update_platform": "Update Feed Format",
"org.settings.extension_name": "Element Name",
"org.settings.extension_name_help": "The unique extension identifier as registered in the CMS (e.g. pkg_mokowaas, com_akeebabackup).",
"org.settings.display_name": "Display Name",
"org.settings.display_name_help": "Human-readable name shown in the CMS update manager.",
"org.settings.extension_type": "Extension Type",
"org.settings.target_version": "Target Platform Version",
"org.settings.target_version_help": "Regex pattern for compatible CMS versions (e.g. (5|6)\\..*). Leave empty for all versions.",
"org.settings.maintainer": "Maintainer",
"org.settings.maintainer_url": "Maintainer URL",
"org.settings.info_url": "Info/Product URL",
"org.settings.info_url_help": "Link to the extension's product or documentation page.",
"org.settings.php_minimum": "Minimum PHP Version",
"org.settings.update_streams_heading": "Update Streams",
"org.settings.update_streams_desc": "Configure the default update streams for all repositories. Release tags are matched to streams by their suffix. Repos can override with per-repo settings.",
"org.settings.stream_mode": "Stream Mode",
@@ -3060,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",
+1 -2
View File
@@ -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()))
@@ -740,7 +739,7 @@ func mustNotBeArchived(ctx *context.APIContext) {
}
func mustEnableEditor(ctx *context.APIContext) {
if !ctx.Repo.Repository.CanEnableEditor() {
if !ctx.Repo.Repository.CanContentChange() {
ctx.APIError(http.StatusLocked, fmt.Errorf("%s is not allowed to edit", ctx.Repo.Repository.FullName()))
return
}
+4 -4
View File
@@ -145,8 +145,8 @@ func GetUserOrgsPermissions(ctx *context.APIContext) {
op := api.OrganizationPermissions{}
if !organization.HasOrgOrUserVisible(ctx, o, ctx.Doer) {
ctx.APIErrorNotFound("HasOrgOrUserVisible", nil)
if !organization.IsOwnerVisibleToDoer(ctx, o, ctx.Doer) {
ctx.APIErrorNotFound("IsOwnerVisibleToDoer", nil)
return
}
@@ -311,8 +311,8 @@ func Get(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"
if !organization.HasOrgOrUserVisible(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) {
ctx.APIErrorNotFound("HasOrgOrUserVisible", nil)
if !organization.IsOwnerVisibleToDoer(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) {
ctx.APIErrorNotFound("IsOwnerVisibleToDoer", nil)
return
}
+1 -1
View File
@@ -93,7 +93,7 @@ func prepareDoerCreateRepoInOrg(ctx *context.APIContext, orgName string) *organi
return nil
}
if !organization.HasOrgOrUserVisible(ctx, org.AsUser(), ctx.Doer) {
if !organization.IsOwnerVisibleToDoer(ctx, org.AsUser(), ctx.Doer) {
ctx.APIErrorNotFound()
return nil
}
+1 -1
View File
@@ -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,
})
+10
View File
@@ -40,9 +40,19 @@ func SettingsUpdateStreamsPost(ctx *context.Context) {
OwnerID: orgID,
RepoID: 0,
StreamMode: ctx.FormString("stream_mode"),
Platform: ctx.FormString("platform"),
CustomStreams: ctx.FormString("custom_streams"),
LicensingEnabled: ctx.FormString("licensing_enabled") == "on",
RequireKey: ctx.FormString("require_key") == "on",
ExtensionName: ctx.FormString("extension_name"),
DisplayName: ctx.FormString("display_name"),
Description: ctx.FormString("feed_description"),
ExtensionType: ctx.FormString("extension_type"),
Maintainer: ctx.FormString("maintainer"),
MaintainerURL: ctx.FormString("maintainer_url"),
InfoURL: ctx.FormString("info_url"),
TargetVersion: ctx.FormString("target_version"),
PHPMinimum: ctx.FormString("php_minimum"),
}
if cfg.StreamMode == "" {
+1 -1
View File
@@ -67,7 +67,7 @@ func prepareEditorPageFormOptions(ctx *context.Context, editorAction string) *co
return nil
}
if commitFormOptions.WillSubmitToFork && !commitFormOptions.TargetRepo.CanEnableEditor() {
if commitFormOptions.WillSubmitToFork && !commitFormOptions.TargetRepo.CanContentChange() {
ctx.Data["NotFoundPrompt"] = ctx.Locale.Tr("repo.editor.fork_not_editable")
ctx.NotFound(nil)
}
-22
View File
@@ -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) {
-2
View File
@@ -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 {
-7
View File
@@ -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
-3
View File
@@ -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 {
+1 -1
View File
@@ -50,7 +50,7 @@ func MustBeNotEmpty(ctx *context.Context) {
// MustBeEditable check that repo can be edited
func MustBeEditable(ctx *context.Context) {
if !ctx.Repo.Repository.CanEnableEditor() {
if !ctx.Repo.Repository.CanContentChange() {
ctx.NotFound(nil)
return
}
+1 -1
View File
@@ -255,7 +255,7 @@ func prepareFileView(ctx *context.Context, entry *git.TreeEntry) {
func prepareFileViewEditorButtons(ctx *context.Context) bool {
// archived or mirror repository, the buttons should not be shown
if !ctx.Repo.Repository.CanEnableEditor() {
if !ctx.Repo.Repository.CanContentChange() {
return true
}
+1 -1
View File
@@ -218,7 +218,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil
ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale)
}
if !fInfo.isLFSFile() && ctx.Repo.Repository.CanEnableEditor() {
if !fInfo.isLFSFile() && ctx.Repo.Repository.CanContentChange() {
ctx.Data["CanEditReadmeFile"] = true
}
}
-3
View File
@@ -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
-2
View File
@@ -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 {
-6
View File
@@ -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)
@@ -1138,7 +1137,6 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Post("/edit", web.Bind(forms.CreateProjectForm{}), org.EditProjectPost)
m.Post("/{action:open|close}", org.ChangeProjectStatus)
// TODO: improper name. Others are "delete project", "edit project", but this one is "move columns"
m.Post("/move", project.MoveColumns)
m.Post("/columns/new", web.Bind(forms.EditProjectColumnForm{}), org.AddColumnToProjectPost)
m.Group("/{columnID}", func() {
@@ -1354,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() {
@@ -1568,7 +1565,6 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Post("/edit", web.Bind(forms.CreateProjectForm{}), repo.EditProjectPost)
m.Post("/{action:open|close}", repo.ChangeProjectStatus)
// TODO: improper name. Others are "delete project", "edit project", but this one is "move columns"
m.Post("/move", project.MoveColumns)
m.Post("/columns/new", web.Bind(forms.EditProjectColumnForm{}), repo.AddColumnToProjectPost)
m.Group("/{columnID}", func() {
@@ -1752,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)
+4
View File
@@ -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)
+1 -1
View File
@@ -149,7 +149,7 @@ func determineAccessMode(ctx *Base, pkgOwner, doer *user_model.User) (perm.Acces
}
}
}
if accessMode == perm.AccessModeNone && organization.HasOrgOrUserVisible(ctx, pkgOwner, doer) {
if accessMode == perm.AccessModeNone && organization.IsOwnerVisibleToDoer(ctx, pkgOwner, doer) {
// 2. If user is unauthorized or no org member, check if org is visible
accessMode = perm.AccessModeRead
}
+1 -1
View File
@@ -190,7 +190,7 @@ func PrepareCommitFormOptions(ctx *Context, doer *user_model.User, targetRepo *r
return nil, err
}
canCommitToBranch := !submitToForkedRepo /* same repo */ && targetRepo.CanEnableEditor() && canPushWithProtection
canCommitToBranch := !submitToForkedRepo /* same repo */ && targetRepo.CanContentChange() && canPushWithProtection
if protectionRequireSigned {
canCommitToBranch = canCommitToBranch && willSign
}
+56 -11
View File
@@ -125,8 +125,8 @@ func NormalizeChannel(ch string) string {
}
// GenerateJoomlaXML builds a Joomla-compatible updates.xml from repository releases.
// It returns the raw XML bytes. The element, maintainer, and target platform
// are derived from the repo name and owner.
// It returns the raw XML bytes. Extension metadata is read from the update stream config;
// falls back to repo name/owner when not configured.
// allowedChannels optionally restricts output to specific channels (nil = all).
func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, requireKey bool, allowedChannels ...string) ([]byte, error) {
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
@@ -149,7 +149,41 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
}
repoLink := fmt.Sprintf("%s/%s/%s", baseURL, repo.Owner.Name, repo.Name)
// Load extension metadata from config (falls back to repo-derived values).
cfg := licenses.GetEffectiveConfig(ctx, repo.OwnerID, repo.ID)
element := strings.ToLower(repo.Name)
if cfg != nil && cfg.ExtensionName != "" {
element = cfg.ExtensionName
}
displayName := fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name)
if cfg != nil && cfg.DisplayName != "" {
displayName = cfg.DisplayName
}
extType := "component"
if cfg != nil && cfg.ExtensionType != "" {
extType = cfg.ExtensionType
}
maintainer := repo.Owner.Name
if cfg != nil && cfg.Maintainer != "" {
maintainer = cfg.Maintainer
}
maintainerURL := fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name)
if cfg != nil && cfg.MaintainerURL != "" {
maintainerURL = cfg.MaintainerURL
}
targetVersion := ".*"
if cfg != nil && cfg.TargetVersion != "" {
targetVersion = cfg.TargetVersion
}
phpMinimum := ""
if cfg != nil && cfg.PHPMinimum != "" {
phpMinimum = cfg.PHPMinimum
}
feedDescription := ""
if cfg != nil && cfg.Description != "" {
feedDescription = cfg.Description
}
// Resolve effective streams (repo override → org default → Joomla default).
streams := licenses.GetEffectiveStreams(ctx, repo.OwnerID, repo.ID)
@@ -215,30 +249,41 @@ func GenerateJoomlaXML(ctx context.Context, repo *repo_model.Repository, require
version = version + suffix
}
desc := feedDescription
if desc == "" {
desc = fmt.Sprintf("%s %s build.", displayName, ch)
}
infoURL := fmt.Sprintf("%s/releases/tag/%s", repoLink, rel.TagName)
if cfg != nil && cfg.InfoURL != "" {
infoURL = cfg.InfoURL
}
u := xmlUpdate{
Name: fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name),
Description: fmt.Sprintf("%s - %s %s build.", repo.Owner.Name, repo.Name, ch),
Name: displayName,
Description: desc,
Element: element,
Type: "component",
Type: extType,
Client: "site",
Version: version,
CreationDate: time.Unix(int64(rel.CreatedUnix), 0).Format("2006-01-02"),
InfoURL: xmlInfoURL{
Title: fmt.Sprintf("%s - %s", repo.Owner.Name, repo.Name),
URL: fmt.Sprintf("%s/releases/tag/%s", repoLink, rel.TagName),
Title: displayName,
URL: infoURL,
},
Downloads: xmlDownloads{
DownloadURL: []xmlDownloadURL{
{Type: "full", Format: "zip", URL: downloadURL},
},
},
Tags: xmlTags{Tag: ch},
Tags: xmlTags{Tag: ch},
ChangelogURL: fmt.Sprintf("%s/raw/branch/%s/CHANGELOG.md", repoLink, repo.DefaultBranch),
Maintainer: repo.Owner.Name,
MaintainerURL: fmt.Sprintf("%s/%s", baseURL, repo.Owner.Name),
Maintainer: maintainer,
MaintainerURL: maintainerURL,
PHPMinimum: phpMinimum,
TargetPlatform: xmlTargetPlat{
Name: "joomla",
Version: ".*",
Version: targetVersion,
},
}
+1 -1
View File
@@ -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
View File
@@ -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">
+1 -1
View File
@@ -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}}">
+80 -1
View File
@@ -29,7 +29,86 @@
<div class="ui divider"></div>
{{/* Section 2: Update Streams */}}
{{/* Section 2: Extension Metadata */}}
<h5>{{svg "octicon-package" 14}} {{ctx.Locale.Tr "org.settings.extension_metadata"}}</h5>
<p>{{ctx.Locale.Tr "org.settings.extension_metadata_desc"}}</p>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.update_platform"}}</label>
<select name="platform" class="ui dropdown">
<option value="joomla" {{if or (eq .StreamConfig.Platform "") (eq .StreamConfig.Platform "joomla")}}selected{{end}}>Joomla</option>
<option value="dolibarr" {{if eq .StreamConfig.Platform "dolibarr"}}selected{{end}}>Dolibarr</option>
<option value="wordpress" {{if eq .StreamConfig.Platform "wordpress"}}selected{{end}}>WordPress</option>
<option value="prestashop" {{if eq .StreamConfig.Platform "prestashop"}}selected{{end}}>PrestaShop</option>
<option value="drupal" {{if eq .StreamConfig.Platform "drupal"}}selected{{end}}>Drupal</option>
<option value="composer" {{if eq .StreamConfig.Platform "composer"}}selected{{end}}>Composer (PHP)</option>
<option value="both" {{if eq .StreamConfig.Platform "both"}}selected{{end}}>Joomla + Dolibarr</option>
</select>
</div>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.extension_name"}}</label>
<input name="extension_name" value="{{.StreamConfig.ExtensionName}}" placeholder="pkg_mokowaas">
<p class="help">{{ctx.Locale.Tr "org.settings.extension_name_help"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.display_name"}}</label>
<input name="display_name" value="{{.StreamConfig.DisplayName}}" placeholder="Package - MokoWaaS">
<p class="help">{{ctx.Locale.Tr "org.settings.display_name_help"}}</p>
</div>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.licenses.description"}}</label>
<input name="feed_description" value="{{.StreamConfig.Description}}" placeholder="MokoWaaS stable build.">
</div>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.extension_type"}}</label>
<select name="extension_type" class="ui dropdown">
<option value="package" {{if eq .StreamConfig.ExtensionType "package"}}selected{{end}}>Package</option>
<option value="component" {{if or (eq .StreamConfig.ExtensionType "") (eq .StreamConfig.ExtensionType "component")}}selected{{end}}>Component</option>
<option value="module" {{if eq .StreamConfig.ExtensionType "module"}}selected{{end}}>Module</option>
<option value="plugin" {{if eq .StreamConfig.ExtensionType "plugin"}}selected{{end}}>Plugin</option>
<option value="template" {{if eq .StreamConfig.ExtensionType "template"}}selected{{end}}>Template</option>
<option value="library" {{if eq .StreamConfig.ExtensionType "library"}}selected{{end}}>Library</option>
</select>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.target_version"}}</label>
<input name="target_version" value="{{.StreamConfig.TargetVersion}}" placeholder="(5|6)\..*">
<p class="help">{{ctx.Locale.Tr "org.settings.target_version_help"}}</p>
</div>
</div>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.maintainer"}}</label>
<input name="maintainer" value="{{.StreamConfig.Maintainer}}" placeholder="Moko Consulting">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.maintainer_url"}}</label>
<input name="maintainer_url" value="{{.StreamConfig.MaintainerURL}}" placeholder="https://mokoconsulting.tech">
</div>
</div>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.info_url"}}</label>
<input name="info_url" value="{{.StreamConfig.InfoURL}}" placeholder="https://mokoconsulting.tech/products/mokowaas">
<p class="help">{{ctx.Locale.Tr "org.settings.info_url_help"}}</p>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.php_minimum"}}</label>
<input name="php_minimum" value="{{.StreamConfig.PHPMinimum}}" placeholder="8.1">
</div>
</div>
<div class="ui divider"></div>
{{/* Section 3: Update Streams */}}
<h5>{{svg "octicon-rss" 14}} {{ctx.Locale.Tr "org.settings.update_streams_heading"}}</h5>
<p>{{ctx.Locale.Tr "org.settings.update_streams_desc"}}</p>
+1 -1
View File
@@ -148,7 +148,7 @@
<a class="item" rel="nofollow" href="{{$.BeforeSourcePath}}/{{PathEscapeSegments .Name}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a>
{{else}}
<a class="item" rel="nofollow" href="{{$.SourcePath}}/{{PathEscapeSegments .Name}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a>
{{if and $.Repository.CanEnableEditor $.CanEditFile}}
{{if and $.Repository.CanContentChange $.CanEditFile}}
<a class="item" rel="nofollow" href="{{$.HeadRepoLink}}/_edit/{{PathEscapeSegments $.HeadBranchName}}/{{PathEscapeSegments $file.Name}}?return_uri={{print $.BackToLink "#diff-" $file.NameHash | QueryEscape}}">{{ctx.Locale.Tr "repo.editor.edit_this_file"}}</a>
{{end}}
{{end}}
+1 -1
View File
@@ -128,7 +128,7 @@
</a>
{{end}}
{{if or .EnableLicenses .IsRepoAdmin}}
{{if .LicensingEnabled}}
<a href="{{.RepoLink}}/licenses" class="{{if .IsLicensesPage}}active {{end}}item">
{{svg "octicon-key"}} {{ctx.Locale.Tr "repo.licenses"}}
{{if .NumLicensePackages}}
@@ -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. */}}
+1 -2
View File
@@ -1,8 +1,7 @@
<div class="field {{if not .item.VisibleOnForm}}tw-hidden{{end}}">
{{template "repo/issue/fields/header" .}}
{{/* FIXME: required validation */}}
<div class="ui fluid selection dropdown {{if .item.Attributes.multiple}}multiple clearable{{end}}">
<input type="hidden" name="form-field-{{.item.ID}}" value="{{.item.Attributes.default}}">
<input type="hidden" name="form-field-{{.item.ID}}" value="{{.item.Attributes.default}}" {{if .item.Validations.required}}required{{end}}>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
{{if not .item.Validations.required}}
{{svg "octicon-x" 14 "remove icon"}}
-2
View File
@@ -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" $}}
+1 -1
View File
@@ -70,7 +70,7 @@
{{$addFilePath = ""}}
{{end}}
{{end}}
<button class="ui dropdown basic compact jump button repo-add-file" {{if not .Repository.CanEnableEditor}}disabled{{end}}>
<button class="ui dropdown basic compact jump button repo-add-file" {{if not .Repository.CanContentChange}}disabled{{end}}>
{{ctx.Locale.Tr "repo.editor.add_file"}}
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
+1 -1
View File
@@ -69,7 +69,7 @@
{{svg "octicon-rss"}}
</a>
{{end}}
{{if .Repository.CanEnableEditor}}
{{if .Repository.CanContentChange}}
{{if .CanEditFile}}
<a class="btn-octicon" data-tooltip-content="{{.EditFileTooltip}}" href="{{.RepoLink}}/_edit/{{PathEscapeSegments .BranchName}}/{{PathEscapeSegments .TreePath}}">{{svg "octicon-pencil"}}</a>
{{else}}
+46
View File
@@ -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>
+1 -7
View File
@@ -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">
+2 -2
View File
@@ -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>
+1 -1
View File
@@ -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>
+1 -5
View File
@@ -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;
+1
View File
@@ -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";
+27
View File
@@ -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);
}
+93
View File
@@ -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();
});
}
@@ -29,12 +29,11 @@ test('markdownHandleIndention', () => {
input = input.replaceAll('|', '');
const ret = markdownHandleIndention({value: input, selStart: inputPos, selEnd: inputPos});
if (expected === null) {
expect(ret).toEqual({handled: false});
expect(ret).toEqual({});
} else {
const expectedPos = expected.indexOf('|');
expected = expected.replaceAll('|', '');
expect(ret).toEqual({
handled: true,
valueSelection: {value: expected, selStart: expectedPos, selEnd: expectedPos},
});
}
+3 -4
View File
@@ -73,7 +73,6 @@ function handleIndentSelection(textarea: HTMLTextAreaElement, e: KeyboardEvent)
}
type MarkdownHandleIndentionResult = {
handled: boolean;
valueSelection?: TextareaValueSelection;
};
@@ -135,7 +134,7 @@ function recalculateLengthBeforeLine(linesBuf: TextLinesBuffer) {
}
export function markdownHandleIndention(tvs: TextareaValueSelection): MarkdownHandleIndentionResult {
const unhandled: MarkdownHandleIndentionResult = {handled: false};
const unhandled: MarkdownHandleIndentionResult = {};
if (tvs.selEnd !== tvs.selStart) return unhandled; // do not process when there is a selection
const linesBuf = textareaSplitLines(tvs.value, tvs.selStart);
@@ -182,13 +181,13 @@ export function markdownHandleIndention(tvs: TextareaValueSelection): MarkdownHa
markdownReformatListNumbers(linesBuf, indention);
const newPos = linesBuf.lengthBeforePosLine + linesBuf.inlinePos;
return {handled: true, valueSelection: {value: linesBuf.lines.join('\n'), selStart: newPos, selEnd: newPos}};
return {valueSelection: {value: linesBuf.lines.join('\n'), selStart: newPos, selEnd: newPos}};
}
function handleNewline(textarea: HTMLTextAreaElement, e: KeyboardEvent) {
if (e.isComposing) return;
const ret = markdownHandleIndention({value: textarea.value, selStart: textarea.selectionStart, selEnd: textarea.selectionEnd});
if (!ret.handled || !ret.valueSelection) return; // FIXME: the "handled" seems redundant, only valueSelection is enough (null for unhandled)
if (!ret.valueSelection) return;
e.preventDefault();
textarea.value = ret.valueSelection.value;
textarea.setSelectionRange(ret.valueSelection.selStart, ret.valueSelection.selEnd);
-28
View File
@@ -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);
+2
View File
@@ -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
View File
@@ -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};
}