Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e998c494b2 | |||
| eafd5320e3 | |||
| 1ed8e463df | |||
| cd0a803341 | |||
| 278645113b | |||
| 7e312077e7 | |||
| 40130d1793 | |||
| 79724b5bc9 | |||
| 88e210bffb | |||
| f73f628403 | |||
| c07443aa87 | |||
| c1c7556916 | |||
| 889f64009b | |||
| 27c4d176f4 | |||
| 53bcb3cdf9 | |||
| b9b3026122 | |||
| d30e7d7a5a | |||
| e0c69d792c | |||
| d4bb976544 | |||
| d74ae92e5b |
@@ -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())
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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}}">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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,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"}}
|
||||
|
||||
@@ -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" $}}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
@@ -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},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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