feat(updates): extension metadata settings, tab visibility, platform support #356
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,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"}}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user