feat(explore): add organization grouping on explore page
Adds optional group_header field to organizations for categorizing them on the explore page (e.g., "Enterprise", "Community", "Partners"). Includes database migration, organization settings form field, and grouped display template. Groups are sorted alphabetically with ungrouped organizations shown last. Users can toggle grouping view with show_groups parameter.
This commit is contained in:
@@ -408,6 +408,7 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(331, "Add is_homepage_pinned to user table", v1_26.AddIsHomepagePinnedToUser),
|
||||
newMigration(332, "Add display_title and license_type to repository", v1_26.AddDisplayTitleAndLicenseTypeToRepository),
|
||||
newMigration(333, "Add group_header to repository", v1_26.AddGroupHeaderToRepository),
|
||||
newMigration(334, "Add group_header to user for organization grouping", v1_26.AddGroupHeaderToUser),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
17
models/migrations/v1_26/v334.go
Normal file
17
models/migrations/v1_26/v334.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright 2026 The Gitea Authors and MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// AddGroupHeaderToUser adds group_header column to the user table for organization grouping
|
||||
func AddGroupHeaderToUser(x *xorm.Engine) error {
|
||||
type User struct {
|
||||
GroupHeader string `xorm:"VARCHAR(255)"`
|
||||
}
|
||||
|
||||
return x.Sync(new(User))
|
||||
}
|
||||
@@ -147,6 +147,7 @@ type User struct {
|
||||
NumMembers int
|
||||
Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"`
|
||||
RepoAdminChangeTeamAccess bool `xorm:"NOT NULL DEFAULT false"`
|
||||
GroupHeader string `xorm:"VARCHAR(255)"`
|
||||
|
||||
// Preferences
|
||||
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
|
||||
|
||||
@@ -367,6 +367,7 @@
|
||||
"explore.code_last_indexed_at": "Last indexed %s",
|
||||
"explore.relevant_repositories_tooltip": "Repositories that are forks or that have no topic, no icon, and no description are hidden.",
|
||||
"explore.relevant_repositories": "Only relevant repositories are being shown, <a href=\"%s\">show unfiltered results</a>.",
|
||||
"explore.orgs.show_groups": "Show Groups",
|
||||
"auth.create_new_account": "Register Account",
|
||||
"auth.already_have_account": "Already have an account?",
|
||||
"auth.sign_in_now": "Sign in now!",
|
||||
@@ -2766,6 +2767,9 @@
|
||||
"org.settings.email": "Contact Email Address",
|
||||
"org.settings.website": "Website",
|
||||
"org.settings.location": "Location",
|
||||
"org.settings.group_header": "Group Header",
|
||||
"org.settings.group_header_placeholder": "e.g., Enterprise, Community, Partners",
|
||||
"org.settings.group_header_help": "Optional header for grouping this organization on the explore page",
|
||||
"org.settings.permission": "Permissions",
|
||||
"org.settings.repoadminchangeteam": "Repository admin can add and remove access for teams",
|
||||
"org.settings.visibility": "Visibility",
|
||||
|
||||
@@ -26,6 +26,10 @@ func Organizations(ctx *context.Context) {
|
||||
ctx.Data["PageIsExploreOrganizations"] = true
|
||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
||||
|
||||
// Set up grouping before RenderUserSearch (grouping logic is in RenderUserSearch)
|
||||
showGrouping := ctx.FormString("show_groups") != "0" // default to true
|
||||
ctx.Data["ShowGrouping"] = showGrouping
|
||||
|
||||
visibleTypes := []structs.VisibleType{structs.VisibleTypePublic}
|
||||
if ctx.Doer != nil {
|
||||
visibleTypes = append(visibleTypes, structs.VisibleTypeLimited, structs.VisibleTypePrivate)
|
||||
|
||||
@@ -6,6 +6,7 @@ package explore
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
"code.gitcaddy.com/server/v3/models/db"
|
||||
user_model "code.gitcaddy.com/server/v3/models/user"
|
||||
@@ -119,6 +120,36 @@ func RenderUserSearch(ctx *context.Context, opts user_model.SearchUserOptions, t
|
||||
ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail
|
||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
||||
|
||||
// Group organizations by GroupHeader if grouping is enabled (for org explore page)
|
||||
if ctx.Data["PageIsExploreOrganizations"] == true && ctx.Data["ShowGrouping"] == true {
|
||||
groupedOrgs := make(map[string][]*user_model.User)
|
||||
var headers []string
|
||||
headerSeen := make(map[string]bool)
|
||||
|
||||
for _, user := range users {
|
||||
header := user.GroupHeader
|
||||
if !headerSeen[header] {
|
||||
headerSeen[header] = true
|
||||
headers = append(headers, header)
|
||||
}
|
||||
groupedOrgs[header] = append(groupedOrgs[header], user)
|
||||
}
|
||||
|
||||
// Sort headers alphabetically, empty string last
|
||||
sort.Slice(headers, func(i, j int) bool {
|
||||
if headers[i] == "" {
|
||||
return false
|
||||
}
|
||||
if headers[j] == "" {
|
||||
return true
|
||||
}
|
||||
return headers[i] < headers[j]
|
||||
})
|
||||
|
||||
ctx.Data["GroupedOrgs"] = groupedOrgs
|
||||
ctx.Data["OrgHeaders"] = headers
|
||||
}
|
||||
|
||||
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
@@ -84,6 +84,7 @@ func SettingsPost(ctx *context.Context) {
|
||||
Description: optional.Some(form.Description),
|
||||
Website: optional.Some(form.Website),
|
||||
Location: optional.Some(form.Location),
|
||||
GroupHeader: optional.Some(form.GroupHeader),
|
||||
RepoAdminChangeTeamAccess: optional.Some(form.RepoAdminChangeTeamAccess),
|
||||
}
|
||||
if ctx.Doer.IsAdmin {
|
||||
|
||||
@@ -42,6 +42,7 @@ type UpdateOrgSettingForm struct {
|
||||
Description string `binding:"MaxSize(255)"`
|
||||
Website string `binding:"ValidUrl;MaxSize(255)"`
|
||||
Location string `binding:"MaxSize(50)"`
|
||||
GroupHeader string `binding:"MaxSize(255)"`
|
||||
MaxRepoCreation int
|
||||
RepoAdminChangeTeamAccess bool
|
||||
IsHomepagePinned bool
|
||||
|
||||
@@ -41,6 +41,7 @@ type UpdateOptions struct {
|
||||
Website optional.Option[string]
|
||||
Location optional.Option[string]
|
||||
Description optional.Option[string]
|
||||
GroupHeader optional.Option[string]
|
||||
AllowGitHook optional.Option[bool]
|
||||
AllowImportLocal optional.Option[bool]
|
||||
MaxRepoCreation optional.Option[int]
|
||||
@@ -88,6 +89,11 @@ func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) er
|
||||
|
||||
cols = append(cols, "description")
|
||||
}
|
||||
if opts.GroupHeader.Has() {
|
||||
u.GroupHeader = opts.GroupHeader.Value()
|
||||
|
||||
cols = append(cols, "group_header")
|
||||
}
|
||||
if opts.Language.Has() {
|
||||
u.Language = opts.Language.Value()
|
||||
|
||||
|
||||
61
templates/explore/org_list_grouped.tmpl
Normal file
61
templates/explore/org_list_grouped.tmpl
Normal file
@@ -0,0 +1,61 @@
|
||||
{{if and (eq (.SystemConfig.Theme.ExploreOrgDisplayFormat.Value ctx) "tiles")}}
|
||||
{{/* Tile Cards View for Organizations - Grouped */}}
|
||||
{{range .OrgHeaders}}
|
||||
{{if .}}
|
||||
<h4 class="ui dividing header tw-mt-4">{{.}}</h4>
|
||||
{{end}}
|
||||
{{$orgs := index $.GroupedOrgs .}}
|
||||
<div class="ui four doubling stackable cards">
|
||||
{{range $orgs}}
|
||||
<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>
|
||||
{{end}}
|
||||
{{else}}
|
||||
{{/* Default List View - Grouped */}}
|
||||
{{range .OrgHeaders}}
|
||||
{{if .}}
|
||||
<h4 class="ui dividing header tw-mt-4">{{.}}</h4>
|
||||
{{end}}
|
||||
{{$orgs := index $.GroupedOrgs .}}
|
||||
<div class="flex-list">
|
||||
{{range $orgs}}
|
||||
<div class="flex-item tw-items-center">
|
||||
<div class="flex-item-leading">
|
||||
{{ctx.AvatarUtils.Avatar . 48}}
|
||||
</div>
|
||||
<div class="flex-item-main">
|
||||
<div class="flex-item-title">
|
||||
<a class="text muted" href="{{.HomeLink}}">{{.DisplayName}}</a>
|
||||
{{if .Visibility.IsPrivate}}
|
||||
<span class="ui basic tiny label">{{ctx.Locale.Tr "repo.desc.private"}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="flex-item-body">
|
||||
{{if .Location}}
|
||||
<span class="flex-text-inline">{{svg "octicon-location"}}{{.Location}}</span>
|
||||
{{end}}
|
||||
{{if and .Email (or (and $.ShowUserEmail $.IsSigned (not .KeepEmailPrivate)) $.PageIsAdminUsers)}}
|
||||
<span class="flex-text-inline">
|
||||
{{svg "octicon-mail"}}
|
||||
<a href="mailto:{{.Email}}">{{.Email}}</a>
|
||||
</span>
|
||||
{{end}}
|
||||
<span class="flex-text-inline">{{svg "octicon-calendar"}}{{ctx.Locale.Tr "user.joined_on" (DateUtils.AbsoluteShort .CreatedUnix)}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -3,7 +3,20 @@
|
||||
{{template "explore/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "explore/search" .}}
|
||||
{{if .PageIsExploreOrganizations}}
|
||||
<div class="tw-flex tw-items-center tw-justify-end tw-mb-4">
|
||||
<label class="tw-flex tw-items-center tw-gap-2 tw-cursor-pointer">
|
||||
<input type="checkbox" id="show-grouping" {{if .ShowGrouping}}checked{{end}} onchange="window.location.href='?show_groups=' + (this.checked ? '1' : '0') + '&q={{.Keyword}}&sort={{.SortType}}'">
|
||||
<span>{{ctx.Locale.Tr "explore.orgs.show_groups"}}</span>
|
||||
</label>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if and .PageIsExploreOrganizations .ShowGrouping .OrgHeaders}}
|
||||
{{/* Grouped View for Organizations */}}
|
||||
{{template "explore/org_list_grouped" .}}
|
||||
{{else}}
|
||||
{{template "explore/user_list" .}}
|
||||
{{end}}
|
||||
{{template "base/paginate" .}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,11 @@
|
||||
<label for="location">{{ctx.Locale.Tr "org.settings.location"}}</label>
|
||||
<input id="location" name="location" value="{{.Org.Location}}" maxlength="50">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="group_header">{{ctx.Locale.Tr "org.settings.group_header"}}</label>
|
||||
<input id="group_header" name="group_header" value="{{.Org.GroupHeader}}" maxlength="255" placeholder="{{ctx.Locale.Tr "org.settings.group_header_placeholder"}}">
|
||||
<p class="help">{{ctx.Locale.Tr "org.settings.group_header_help"}}</p>
|
||||
</div>
|
||||
|
||||
<div class="field" id="permission_box">
|
||||
<label>{{ctx.Locale.Tr "org.settings.permission"}}</label>
|
||||
|
||||
Reference in New Issue
Block a user