feat(theme): add homepage customization and pinned organizations
- Add customizable homepage title and tagline via admin theme settings - Add ability for site admins to pin organizations to homepage - Add pinned organization display format option (condensed/regular) - Hide promotional text when pinned organizations are displayed - Add database migration for is_homepage_pinned column - Add custom site icon support for favicon and navbar 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -405,6 +405,7 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(328, "Add wiki index table for search", v1_26.AddWikiIndexTable),
|
||||
newMigration(329, "Add release archive columns", v1_26.AddReleaseArchiveColumns),
|
||||
newMigration(330, "Add runner capabilities column", v1_26.AddRunnerCapabilitiesColumn),
|
||||
newMigration(331, "Add is_homepage_pinned to user table", v1_26.AddIsHomepagePinnedToUser),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
16
models/migrations/v1_26/v331.go
Normal file
16
models/migrations/v1_26/v331.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// AddIsHomepagePinnedToUser adds is_homepage_pinned column to user table for organizations
|
||||
func AddIsHomepagePinnedToUser(x *xorm.Engine) error {
|
||||
type User struct {
|
||||
IsHomepagePinned bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
return x.Sync(new(User))
|
||||
}
|
||||
@@ -596,3 +596,21 @@ func getUserTeamIDsQueryBuilder(orgID, userID int64) *builder.Builder {
|
||||
"team_user.uid": userID,
|
||||
})
|
||||
}
|
||||
|
||||
// GetHomepagePinnedOrganizations returns all organizations that are pinned to the homepage
|
||||
func GetHomepagePinnedOrganizations(ctx context.Context) ([]*Organization, error) {
|
||||
orgs := make([]*Organization, 0, 10)
|
||||
return orgs, db.GetEngine(ctx).
|
||||
Where("type = ?", user_model.UserTypeOrganization).
|
||||
And("is_homepage_pinned = ?", true).
|
||||
And("visibility = ?", structs.VisibleTypePublic).
|
||||
OrderBy("name ASC").
|
||||
Find(&orgs)
|
||||
}
|
||||
|
||||
// SetHomepagePinned updates the homepage pinned status for an organization
|
||||
func (org *Organization) SetHomepagePinned(ctx context.Context, pinned bool) error {
|
||||
org.IsHomepagePinned = pinned
|
||||
_, err := db.GetEngine(ctx).ID(org.ID).Cols("is_homepage_pinned").Update(org)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -121,6 +121,8 @@ type User struct {
|
||||
// true: the user is only allowed to see organizations/repositories that they has explicit rights to.
|
||||
// (ex: in private Gitea instances user won't be allowed to see even organizations/repositories that are set as public)
|
||||
IsRestricted bool `xorm:"NOT NULL DEFAULT false"`
|
||||
// IsHomepagePinned indicates if this organization should appear on the homepage
|
||||
IsHomepagePinned bool `xorm:"NOT NULL DEFAULT false"`
|
||||
|
||||
AllowGitHook bool
|
||||
AllowImportLocal bool // Allow migrate repository by local path
|
||||
|
||||
@@ -53,9 +53,20 @@ type RepositoryStruct struct {
|
||||
GitGuideRemoteName *config.Value[string]
|
||||
}
|
||||
|
||||
type ThemeStruct struct {
|
||||
DisableRegistration *config.Value[bool]
|
||||
CustomSiteIconURL *config.Value[string]
|
||||
CustomHomeLogoURL *config.Value[string]
|
||||
CustomHomeHTML *config.Value[string]
|
||||
CustomHomeTitle *config.Value[string]
|
||||
CustomHomeTagline *config.Value[string]
|
||||
PinnedOrgDisplayFormat *config.Value[string]
|
||||
}
|
||||
|
||||
type ConfigStruct struct {
|
||||
Picture *PictureStruct
|
||||
Repository *RepositoryStruct
|
||||
Theme *ThemeStruct
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -74,6 +85,15 @@ func initDefaultConfig() {
|
||||
OpenWithEditorApps: config.ValueJSON[OpenWithEditorAppsType]("repository.open-with.editor-apps"),
|
||||
GitGuideRemoteName: config.ValueJSON[string]("repository.git-guide-remote-name").WithDefault("origin"),
|
||||
},
|
||||
Theme: &ThemeStruct{
|
||||
DisableRegistration: config.ValueJSON[bool]("theme.disable_registration").WithFileConfig(config.CfgSecKey{Sec: "service", Key: "DISABLE_REGISTRATION"}),
|
||||
CustomSiteIconURL: config.ValueJSON[string]("theme.custom_site_icon_url").WithDefault(""),
|
||||
CustomHomeLogoURL: config.ValueJSON[string]("theme.custom_home_logo_url").WithDefault(""),
|
||||
CustomHomeHTML: config.ValueJSON[string]("theme.custom_home_html").WithDefault(""),
|
||||
CustomHomeTitle: config.ValueJSON[string]("theme.custom_home_title").WithDefault(""),
|
||||
CustomHomeTagline: config.ValueJSON[string]("theme.custom_home_tagline").WithDefault(""),
|
||||
PinnedOrgDisplayFormat: config.ValueJSON[string]("theme.pinned_org_display_format").WithDefault("condensed"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ func NewFuncMap() template.FuncMap {
|
||||
"QueryEscape": queryEscape,
|
||||
"QueryBuild": QueryBuild,
|
||||
"SanitizeHTML": SanitizeHTML,
|
||||
"SafeHTML": SafeHTML,
|
||||
"URLJoin": util.URLJoin,
|
||||
"DotEscape": dotEscape,
|
||||
|
||||
@@ -172,6 +173,11 @@ func SanitizeHTML(s string) template.HTML {
|
||||
return markup.Sanitize(s)
|
||||
}
|
||||
|
||||
// SafeHTML marks a string as safe HTML (no sanitization). Use with caution - only for trusted admin content.
|
||||
func SafeHTML(s string) template.HTML {
|
||||
return template.HTML(s)
|
||||
}
|
||||
|
||||
func htmlFormat(s any, args ...any) template.HTML {
|
||||
if len(args) == 0 {
|
||||
// to prevent developers from calling "HTMLFormat $userInput" by mistake which will lead to XSS
|
||||
|
||||
@@ -338,6 +338,7 @@
|
||||
"home.collaborative_repos": "Collaborative Repositories",
|
||||
"home.my_orgs": "My Organizations",
|
||||
"home.my_mirrors": "My Mirrors",
|
||||
"home.featured_organizations": "Featured Organizations",
|
||||
"home.view_home": "View %s",
|
||||
"home.filter": "Other Filters",
|
||||
"home.filter_by_team_repositories": "Filter by team repositories",
|
||||
@@ -2798,6 +2799,9 @@
|
||||
"org.settings.confirm_delete_account": "Confirm Deletion",
|
||||
"org.settings.delete_failed": "Deleting organization failed due to an internal error",
|
||||
"org.settings.delete_successful": "Organization <b>%s</b> has been deleted successfully.",
|
||||
"org.settings.homepage_pinning": "Homepage Visibility",
|
||||
"org.settings.pin_to_homepage": "Pin this organization to the homepage",
|
||||
"org.settings.pin_to_homepage_help": "When enabled, this organization will be featured on the public homepage. Only administrators can change this setting.",
|
||||
"org.settings.hooks_desc": "Add webhooks which will be triggered for <strong>all repositories</strong> under this organization.",
|
||||
"org.settings.labels_desc": "Add labels which can be used on issues for <strong>all repositories</strong> under this organization.",
|
||||
"org.members.membership_visibility": "Membership Visibility:",
|
||||
@@ -3828,5 +3832,43 @@
|
||||
"org.stats": "Stats",
|
||||
"org.recent_activity": "Recent Activity",
|
||||
"org.profile_repo_no_permission": "You do not have permission to create repositories in this organization.",
|
||||
"org.profile_repo_create_failed": "Failed to create the profile repository."
|
||||
"org.profile_repo_create_failed": "Failed to create the profile repository.",
|
||||
"admin.config.theme_config": "Theme Configuration",
|
||||
"admin.config.disable_registration": "Disable Registration",
|
||||
"admin.config.disable_registration_desc": "When enabled, new users cannot sign up. Only administrators can create new accounts.",
|
||||
"admin.config.custom_home_logo": "Homepage Logo",
|
||||
"admin.config.custom_logo_url_placeholder": "Enter URL or upload a file below",
|
||||
"admin.config.upload_logo": "Upload Logo File",
|
||||
"admin.config.reset_logo": "Reset to Default",
|
||||
"admin.config.custom_home_html": "Custom Homepage Content",
|
||||
"admin.config.custom_home_html_placeholder": "Enter custom HTML for the homepage (shown to non-logged-in users)",
|
||||
"admin.config.custom_home_html_help": "This HTML will replace the default homepage content. Leave empty to use the default.",
|
||||
"admin.config.custom_home_title": "Homepage Title",
|
||||
"admin.config.custom_home_title_placeholder": "Leave empty to use app name",
|
||||
"admin.config.custom_home_title_help": "Custom title displayed on the homepage. Leave empty to use the default app name.",
|
||||
"admin.config.custom_home_tagline": "Homepage Tagline",
|
||||
"admin.config.custom_home_tagline_placeholder": "Leave empty to use default tagline",
|
||||
"admin.config.custom_home_tagline_help": "Custom tagline displayed below the title on the homepage. Leave empty to use the default.",
|
||||
"admin.config.pinned_org_display_format": "Pinned Organization Display Format",
|
||||
"admin.config.pinned_org_format_condensed": "Condensed (icon on left)",
|
||||
"admin.config.pinned_org_format_regular": "Regular (icon above)",
|
||||
"admin.config.pinned_org_display_format_help": "Choose how pinned organizations are displayed on the homepage.",
|
||||
"admin.config.logo_upload_success": "Homepage logo uploaded successfully",
|
||||
"admin.config.logo_url_success": "Homepage logo URL updated successfully",
|
||||
"admin.config.logo_reset_success": "Homepage logo reset to default",
|
||||
"admin.config.logo_invalid_type": "Invalid file type. Allowed: SVG, PNG, JPG, GIF",
|
||||
"admin.config.home_logo_help": "This logo will be displayed on the homepage for non-logged-in users. Recommended: SVG, PNG, JPG or GIF.",
|
||||
"admin.config.custom_site_icon": "Site Icon (Favicon & Navbar)",
|
||||
"admin.config.custom_icon_url_placeholder": "Enter icon URL or upload a file below",
|
||||
"admin.config.upload_icon": "Upload Icon File",
|
||||
"admin.config.reset_icon": "Reset to Default",
|
||||
"admin.config.current_icon": "Current Icon",
|
||||
"admin.config.icon_url": "Icon URL",
|
||||
"admin.config.icon_upload_success": "Site icon uploaded successfully",
|
||||
"admin.config.icon_url_success": "Site icon URL updated successfully",
|
||||
"admin.config.icon_reset_success": "Site icon reset to default",
|
||||
"admin.config.icon_invalid_type": "Invalid file type. Allowed: SVG, PNG, ICO",
|
||||
"admin.config.site_icon_help": "This icon will be used as the site favicon and in the navbar. Recommended: SVG or PNG, square dimensions.",
|
||||
"admin.config.current_logo": "Current Homepage Logo",
|
||||
"admin.config.logo_url": "Logo URL"
|
||||
}
|
||||
@@ -5,8 +5,11 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -235,6 +238,11 @@ func ChangeConfig(ctx *context.Context) {
|
||||
cfg.Picture.EnableFederatedAvatar.DynKey(): marshalBool,
|
||||
cfg.Repository.OpenWithEditorApps.DynKey(): marshalOpenWithApps,
|
||||
cfg.Repository.GitGuideRemoteName.DynKey(): marshalString(cfg.Repository.GitGuideRemoteName.DefaultValue()),
|
||||
cfg.Theme.DisableRegistration.DynKey(): marshalBool,
|
||||
cfg.Theme.CustomHomeHTML.DynKey(): marshalString(""),
|
||||
cfg.Theme.CustomHomeTitle.DynKey(): marshalString(""),
|
||||
cfg.Theme.CustomHomeTagline.DynKey(): marshalString(""),
|
||||
cfg.Theme.PinnedOrgDisplayFormat.DynKey(): marshalString("condensed"),
|
||||
}
|
||||
|
||||
_ = ctx.Req.ParseForm()
|
||||
@@ -272,3 +280,167 @@ loop:
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
ctx.JSONOK()
|
||||
}
|
||||
|
||||
// ChangeThemeLogo handles homepage logo upload and custom URL
|
||||
func ChangeThemeLogo(ctx *context.Context) {
|
||||
cfg := setting.Config()
|
||||
|
||||
action := ctx.FormString("action")
|
||||
if action == "reset" {
|
||||
configSettings := map[string]string{
|
||||
cfg.Theme.CustomHomeLogoURL.DynKey(): "\"\"",
|
||||
}
|
||||
if err := system_model.SetSettings(ctx, configSettings); err != nil {
|
||||
ctx.ServerError("SetSettings", err)
|
||||
return
|
||||
}
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
ctx.Flash.Success(ctx.Tr("admin.config.logo_reset_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
return
|
||||
}
|
||||
|
||||
// Check for file upload first
|
||||
file, header, err := ctx.Req.FormFile("logo_file")
|
||||
if err == nil && header != nil {
|
||||
defer file.Close()
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(header.Filename))
|
||||
allowedExts := map[string]bool{".svg": true, ".png": true, ".jpg": true, ".jpeg": true, ".gif": true}
|
||||
if !allowedExts[ext] {
|
||||
ctx.Flash.Error(ctx.Tr("admin.config.logo_invalid_type"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
return
|
||||
}
|
||||
|
||||
customDir := filepath.Join(setting.CustomPath, "public", "assets", "img")
|
||||
if err := os.MkdirAll(customDir, 0755); err != nil {
|
||||
ctx.ServerError("MkdirAll", err)
|
||||
return
|
||||
}
|
||||
|
||||
fileName := "custom-home-logo" + ext
|
||||
filePath := filepath.Join(customDir, fileName)
|
||||
destFile, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
ctx.ServerError("Create", err)
|
||||
return
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
if _, err := io.Copy(destFile, file); err != nil {
|
||||
ctx.ServerError("Copy", err)
|
||||
return
|
||||
}
|
||||
|
||||
fileURL := setting.AppSubURL + "/assets/img/" + fileName
|
||||
marshaledValue, _ := json.Marshal(fileURL)
|
||||
configSettings := map[string]string{
|
||||
cfg.Theme.CustomHomeLogoURL.DynKey(): string(marshaledValue),
|
||||
}
|
||||
if err := system_model.SetSettings(ctx, configSettings); err != nil {
|
||||
ctx.ServerError("SetSettings", err)
|
||||
return
|
||||
}
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
ctx.Flash.Success(ctx.Tr("admin.config.logo_upload_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
return
|
||||
}
|
||||
|
||||
// Check for custom URL
|
||||
customURL := ctx.FormString("custom_logo_url")
|
||||
marshaledValue, _ := json.Marshal(customURL)
|
||||
configSettings := map[string]string{
|
||||
cfg.Theme.CustomHomeLogoURL.DynKey(): string(marshaledValue),
|
||||
}
|
||||
if err := system_model.SetSettings(ctx, configSettings); err != nil {
|
||||
ctx.ServerError("SetSettings", err)
|
||||
return
|
||||
}
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
ctx.Flash.Success(ctx.Tr("admin.config.logo_url_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
}
|
||||
|
||||
// ChangeThemeIcon handles site icon (favicon + navbar) upload and custom URL
|
||||
func ChangeThemeIcon(ctx *context.Context) {
|
||||
cfg := setting.Config()
|
||||
|
||||
action := ctx.FormString("action")
|
||||
if action == "reset" {
|
||||
configSettings := map[string]string{
|
||||
cfg.Theme.CustomSiteIconURL.DynKey(): "\"\"",
|
||||
}
|
||||
if err := system_model.SetSettings(ctx, configSettings); err != nil {
|
||||
ctx.ServerError("SetSettings", err)
|
||||
return
|
||||
}
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
ctx.Flash.Success(ctx.Tr("admin.config.icon_reset_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
return
|
||||
}
|
||||
|
||||
// Check for file upload first
|
||||
file, header, err := ctx.Req.FormFile("icon_file")
|
||||
if err == nil && header != nil {
|
||||
defer file.Close()
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(header.Filename))
|
||||
allowedExts := map[string]bool{".svg": true, ".png": true, ".ico": true}
|
||||
if !allowedExts[ext] {
|
||||
ctx.Flash.Error(ctx.Tr("admin.config.icon_invalid_type"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
return
|
||||
}
|
||||
|
||||
customDir := filepath.Join(setting.CustomPath, "public", "assets", "img")
|
||||
if err := os.MkdirAll(customDir, 0755); err != nil {
|
||||
ctx.ServerError("MkdirAll", err)
|
||||
return
|
||||
}
|
||||
|
||||
fileName := "custom-site-icon" + ext
|
||||
filePath := filepath.Join(customDir, fileName)
|
||||
destFile, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
ctx.ServerError("Create", err)
|
||||
return
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
if _, err := io.Copy(destFile, file); err != nil {
|
||||
ctx.ServerError("Copy", err)
|
||||
return
|
||||
}
|
||||
|
||||
fileURL := setting.AppSubURL + "/assets/img/" + fileName
|
||||
marshaledValue, _ := json.Marshal(fileURL)
|
||||
configSettings := map[string]string{
|
||||
cfg.Theme.CustomSiteIconURL.DynKey(): string(marshaledValue),
|
||||
}
|
||||
if err := system_model.SetSettings(ctx, configSettings); err != nil {
|
||||
ctx.ServerError("SetSettings", err)
|
||||
return
|
||||
}
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
ctx.Flash.Success(ctx.Tr("admin.config.icon_upload_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
return
|
||||
}
|
||||
|
||||
// Check for custom URL
|
||||
customURL := ctx.FormString("custom_icon_url")
|
||||
marshaledValue, _ := json.Marshal(customURL)
|
||||
configSettings := map[string]string{
|
||||
cfg.Theme.CustomSiteIconURL.DynKey(): string(marshaledValue),
|
||||
}
|
||||
if err := system_model.SetSettings(ctx, configSettings); err != nil {
|
||||
ctx.ServerError("SetSettings", err)
|
||||
return
|
||||
}
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
ctx.Flash.Success(ctx.Tr("admin.config.icon_url_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/config/settings")
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
organization_model "code.gitea.io/gitea/models/organization"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
@@ -61,6 +62,14 @@ func Home(ctx *context.Context) {
|
||||
|
||||
ctx.Data["PageIsHome"] = true
|
||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
||||
|
||||
// Load pinned organizations for homepage
|
||||
pinnedOrgs, err := organization_model.GetHomepagePinnedOrganizations(ctx)
|
||||
if err != nil {
|
||||
log.Error("GetHomepagePinnedOrganizations: %v", err)
|
||||
} else {
|
||||
ctx.Data["PinnedOrganizations"] = pinnedOrgs
|
||||
}
|
||||
ctx.HTML(http.StatusOK, tplHome)
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ func Settings(ctx *context.Context) {
|
||||
ctx.Data["PageIsSettingsOptions"] = true
|
||||
ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility
|
||||
ctx.Data["RepoAdminChangeTeamAccess"] = ctx.Org.Organization.RepoAdminChangeTeamAccess
|
||||
ctx.Data["IsHomepagePinned"] = ctx.Org.Organization.IsHomepagePinned
|
||||
ctx.Data["ContextUser"] = ctx.ContextUser
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
@@ -89,6 +90,14 @@ func SettingsPost(ctx *context.Context) {
|
||||
opts.MaxRepoCreation = optional.Some(form.MaxRepoCreation)
|
||||
}
|
||||
|
||||
// Handle homepage pinning (admin only)
|
||||
if ctx.Doer.IsAdmin {
|
||||
if err := org.SetHomepagePinned(ctx, form.IsHomepagePinned); err != nil {
|
||||
ctx.ServerError("SetHomepagePinned", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := user_service.UpdateUser(ctx, org.AsUser(), opts); err != nil {
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
return
|
||||
|
||||
@@ -778,6 +778,8 @@ func registerWebRoutes(m *web.Router) {
|
||||
m.Post("/test_mail", admin.SendTestMail)
|
||||
m.Post("/test_cache", admin.TestCache)
|
||||
m.Get("/settings", admin.ConfigSettings)
|
||||
m.Post("/theme/logo", admin.ChangeThemeLogo)
|
||||
m.Post("/theme/icon", admin.ChangeThemeIcon)
|
||||
})
|
||||
|
||||
m.Group("/monitor", func() {
|
||||
|
||||
@@ -26,6 +26,7 @@ type CreateOrgForm struct {
|
||||
OrgName string `binding:"Required;Username;MaxSize(40)" locale:"org.org_name_holder"`
|
||||
Visibility structs.VisibleType
|
||||
RepoAdminChangeTeamAccess bool
|
||||
IsHomepagePinned bool
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
@@ -43,6 +44,7 @@ type UpdateOrgSettingForm struct {
|
||||
Location string `binding:"MaxSize(50)"`
|
||||
MaxRepoCreation int
|
||||
RepoAdminChangeTeamAccess bool
|
||||
IsHomepagePinned bool
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin config")}}
|
||||
|
||||
{{template "admin/config_settings/theme" .}}
|
||||
|
||||
{{template "admin/config_settings/avatars" .}}
|
||||
|
||||
{{template "admin/config_settings/repository" .}}
|
||||
|
||||
120
templates/admin/config_settings/theme.tmpl
Normal file
120
templates/admin/config_settings/theme.tmpl
Normal file
@@ -0,0 +1,120 @@
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "admin.config.theme_config"}}
|
||||
</h4>
|
||||
<div class="ui attached table segment">
|
||||
<dl class="admin-dl-horizontal">
|
||||
<dt>{{ctx.Locale.Tr "admin.config.disable_registration"}}</dt>
|
||||
<dd>
|
||||
<div class="ui toggle checkbox" data-tooltip-content="{{ctx.Locale.Tr "admin.config.disable_registration_desc"}}">
|
||||
<input type="checkbox" data-config-dyn-key="theme.disable_registration" {{if .SystemConfig.Theme.DisableRegistration.Value ctx}}checked{{end}}><label></label>
|
||||
</div>
|
||||
</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.custom_site_icon"}}</dt>
|
||||
<dd>
|
||||
<form class="ui form" method="post" action="{{AppSubUrl}}/-/admin/config/theme/icon" enctype="multipart/form-data">
|
||||
{{.CsrfTokenHtml}}
|
||||
{{if .SystemConfig.Theme.CustomSiteIconURL.Value ctx}}
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.config.current_icon"}}</label>
|
||||
<img src="{{.SystemConfig.Theme.CustomSiteIconURL.Value ctx}}" alt="Current Icon" style="max-height: 32px; max-width: 32px; background: var(--color-secondary-bg); padding: 4px; border-radius: 4px;">
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.config.icon_url"}}</label>
|
||||
<input type="text" name="custom_icon_url" value="{{.SystemConfig.Theme.CustomSiteIconURL.Value ctx}}" placeholder="{{ctx.Locale.Tr "admin.config.custom_icon_url_placeholder"}}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.config.upload_icon"}}</label>
|
||||
<input type="file" name="icon_file" accept="image/svg+xml,image/png,image/x-icon,image/vnd.microsoft.icon">
|
||||
</div>
|
||||
<div class="help">{{ctx.Locale.Tr "admin.config.site_icon_help"}}</div>
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
{{if .SystemConfig.Theme.CustomSiteIconURL.Value ctx}}
|
||||
<button class="ui red button" type="submit" name="action" value="reset">{{ctx.Locale.Tr "admin.config.reset_icon"}}</button>
|
||||
{{end}}
|
||||
</form>
|
||||
</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.custom_home_logo"}}</dt>
|
||||
<dd>
|
||||
<form class="ui form" method="post" action="{{AppSubUrl}}/-/admin/config/theme/logo" enctype="multipart/form-data">
|
||||
{{.CsrfTokenHtml}}
|
||||
{{if .SystemConfig.Theme.CustomHomeLogoURL.Value ctx}}
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.config.current_logo"}}</label>
|
||||
<img src="{{.SystemConfig.Theme.CustomHomeLogoURL.Value ctx}}" alt="Current Logo" style="max-height: 80px; max-width: 200px; background: var(--color-secondary-bg); padding: 8px; border-radius: 4px;">
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.config.logo_url"}}</label>
|
||||
<input type="text" name="custom_logo_url" value="{{.SystemConfig.Theme.CustomHomeLogoURL.Value ctx}}" placeholder="{{ctx.Locale.Tr "admin.config.custom_logo_url_placeholder"}}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.config.upload_logo"}}</label>
|
||||
<input type="file" name="logo_file" accept="image/svg+xml,image/png,image/jpeg,image/gif">
|
||||
</div>
|
||||
<div class="help">{{ctx.Locale.Tr "admin.config.home_logo_help"}}</div>
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
{{if .SystemConfig.Theme.CustomHomeLogoURL.Value ctx}}
|
||||
<button class="ui red button" type="submit" name="action" value="reset">{{ctx.Locale.Tr "admin.config.reset_logo"}}</button>
|
||||
{{end}}
|
||||
</form>
|
||||
</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.custom_home_title"}}</dt>
|
||||
<dd>
|
||||
<form class="ui form form-fetch-action" method="post" action="{{AppSubUrl}}/-/admin/config">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="key" value="theme.custom_home_title">
|
||||
<div class="field">
|
||||
<input type="text" name="value" value="{{.SystemConfig.Theme.CustomHomeTitle.Value ctx}}" placeholder="{{ctx.Locale.Tr "admin.config.custom_home_title_placeholder"}}" maxlength="100">
|
||||
</div>
|
||||
<div class="help">{{ctx.Locale.Tr "admin.config.custom_home_title_help"}}</div>
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
</form>
|
||||
</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.custom_home_tagline"}}</dt>
|
||||
<dd>
|
||||
<form class="ui form form-fetch-action" method="post" action="{{AppSubUrl}}/-/admin/config">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="key" value="theme.custom_home_tagline">
|
||||
<div class="field">
|
||||
<input type="text" name="value" value="{{.SystemConfig.Theme.CustomHomeTagline.Value ctx}}" placeholder="{{ctx.Locale.Tr "admin.config.custom_home_tagline_placeholder"}}" maxlength="255">
|
||||
</div>
|
||||
<div class="help">{{ctx.Locale.Tr "admin.config.custom_home_tagline_help"}}</div>
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
</form>
|
||||
</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.pinned_org_display_format"}}</dt>
|
||||
<dd>
|
||||
<form class="ui form form-fetch-action" method="post" action="{{AppSubUrl}}/-/admin/config">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="key" value="theme.pinned_org_display_format">
|
||||
<div class="field">
|
||||
<select class="ui dropdown" name="value">
|
||||
<option value="condensed" {{if eq (.SystemConfig.Theme.PinnedOrgDisplayFormat.Value ctx) "condensed"}}selected{{end}}>{{ctx.Locale.Tr "admin.config.pinned_org_format_condensed"}}</option>
|
||||
<option value="regular" {{if eq (.SystemConfig.Theme.PinnedOrgDisplayFormat.Value ctx) "regular"}}selected{{end}}>{{ctx.Locale.Tr "admin.config.pinned_org_format_regular"}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="help">{{ctx.Locale.Tr "admin.config.pinned_org_display_format_help"}}</div>
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
</form>
|
||||
</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.config.custom_home_html"}}</dt>
|
||||
<dd>
|
||||
<form class="ui form form-fetch-action" method="post" action="{{AppSubUrl}}/-/admin/config">
|
||||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="key" value="theme.custom_home_html">
|
||||
<div class="field">
|
||||
<textarea id="custom-home-html-editor" name="value" rows="15" placeholder="{{ctx.Locale.Tr "admin.config.custom_home_html_placeholder"}}" style="font-family: var(--fonts-monospace); font-size: 13px;">{{.SystemConfig.Theme.CustomHomeHTML.Value ctx}}</textarea>
|
||||
</div>
|
||||
<div class="help">{{ctx.Locale.Tr "admin.config.custom_home_html_help"}}</div>
|
||||
<button class="ui primary button">{{ctx.Locale.Tr "save"}}</button>
|
||||
</form>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
@@ -16,8 +16,12 @@
|
||||
<link rel="alternate" type="application/atom+xml" title="" href="{{.FeedURL}}.atom">
|
||||
<link rel="alternate" type="application/rss+xml" title="" href="{{.FeedURL}}.rss">
|
||||
{{end}}
|
||||
{{if .SystemConfig.Theme.CustomSiteIconURL.Value ctx}}
|
||||
<link rel="icon" href="{{.SystemConfig.Theme.CustomSiteIconURL.Value ctx}}">
|
||||
{{else}}
|
||||
<link rel="icon" href="{{AssetUrlPrefix}}/img/favicon.svg" type="image/svg+xml">
|
||||
<link rel="alternate icon" href="{{AssetUrlPrefix}}/img/favicon.png" type="image/png">
|
||||
{{end}}
|
||||
{{template "base/head_opengraph" .}}
|
||||
{{template "base/head_style" .}}
|
||||
{{template "base/head_script" .}}
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
<div class="navbar-left">
|
||||
<!-- the logo -->
|
||||
<a class="item" id="navbar-logo" href="{{AppSubUrl}}/" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home_title"}}{{end}}">
|
||||
{{if .SystemConfig.Theme.CustomSiteIconURL.Value ctx}}
|
||||
<img width="30" height="30" src="{{.SystemConfig.Theme.CustomSiteIconURL.Value ctx}}" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
|
||||
{{else}}
|
||||
<img width="30" height="30" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
|
||||
{{end}}
|
||||
</a>
|
||||
|
||||
<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->
|
||||
|
||||
@@ -1,16 +1,77 @@
|
||||
{{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">
|
||||
{{if .SystemConfig.Theme.CustomHomeHTML.Value ctx}}
|
||||
{{/* Custom homepage content */}}
|
||||
{{.SystemConfig.Theme.CustomHomeHTML.Value ctx | SafeHTML}}
|
||||
{{else}}
|
||||
{{/* Default homepage content */}}
|
||||
<div class="tw-mb-8 tw-px-8">
|
||||
<div class="center">
|
||||
{{if .SystemConfig.Theme.CustomHomeLogoURL.Value ctx}}
|
||||
<img class="logo" width="220" height="220" src="{{.SystemConfig.Theme.CustomHomeLogoURL.Value ctx}}" alt="{{ctx.Locale.Tr "logo"}}">
|
||||
{{else}}
|
||||
<img class="logo" width="220" height="220" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{ctx.Locale.Tr "logo"}}">
|
||||
{{end}}
|
||||
<div class="hero">
|
||||
<h1 class="ui icon header title tw-text-balance">
|
||||
{{AppName}}
|
||||
{{if .SystemConfig.Theme.CustomHomeTitle.Value ctx}}
|
||||
{{.SystemConfig.Theme.CustomHomeTitle.Value ctx}}
|
||||
{{else}}
|
||||
{{AppName}}
|
||||
{{end}}
|
||||
</h1>
|
||||
<h2 class="tw-text-balance">{{ctx.Locale.Tr "startpage.app_desc"}}</h2>
|
||||
<h2 class="tw-text-balance">
|
||||
{{if .SystemConfig.Theme.CustomHomeTagline.Value ctx}}
|
||||
{{.SystemConfig.Theme.CustomHomeTagline.Value ctx}}
|
||||
{{else}}
|
||||
{{ctx.Locale.Tr "startpage.app_desc"}}
|
||||
{{end}}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{if .PinnedOrganizations}}
|
||||
<div class="ui container tw-my-8">
|
||||
{{if eq (.SystemConfig.Theme.PinnedOrgDisplayFormat.Value ctx) "regular"}}
|
||||
{{/* Regular format: icon above, title below, description below that */}}
|
||||
<div class="ui four doubling stackable cards">
|
||||
{{range .PinnedOrganizations}}
|
||||
<a class="ui card" href="{{.HomeLink}}">
|
||||
<div class="content tw-text-center">
|
||||
<div class="tw-mb-3">
|
||||
{{ctx.AvatarUtils.Avatar . 64 "tw-rounded"}}
|
||||
</div>
|
||||
<div class="header">{{.DisplayName}}</div>
|
||||
{{if .Description}}
|
||||
<div class="meta tw-mt-2">{{.Description}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
{{/* Condensed format (default): icon on left, title/description on right */}}
|
||||
<div class="ui four doubling stackable cards">
|
||||
{{range .PinnedOrganizations}}
|
||||
<a class="ui card" href="{{.HomeLink}}">
|
||||
<div class="content">
|
||||
<div class="tw-flex tw-items-center tw-gap-3">
|
||||
{{ctx.AvatarUtils.Avatar . 48 "tw-rounded"}}
|
||||
<div class="tw-flex-1 tw-overflow-hidden">
|
||||
<div class="header tw-truncate">{{.DisplayName}}</div>
|
||||
{{if .Description}}
|
||||
<div class="meta tw-truncate">{{.Description}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
{{/* Only show promotional text when there are no pinned organizations */}}
|
||||
<div class="ui stackable middle very relaxed page grid">
|
||||
<div class="eight wide center column">
|
||||
<h1 class="hero ui icon header">
|
||||
@@ -47,5 +108,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
||||
|
||||
@@ -46,6 +46,17 @@
|
||||
<input id="max_repo_creation" name="max_repo_creation" type="number" min="-1" value="{{.Org.MaxRepoCreation}}">
|
||||
<p class="help">{{ctx.Locale.Tr "admin.users.max_repo_creation_desc"}}</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "org.settings.homepage_pinning"}}</label>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="is_homepage_pinned" {{if .IsHomepagePinned}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "org.settings.pin_to_homepage"}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help">{{ctx.Locale.Tr "org.settings.pin_to_homepage_help"}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="field">
|
||||
|
||||
Reference in New Issue
Block a user