feat: Pin repos to user profile or organization
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m33s
Build and Release / Lint (push) Failing after 1m54s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 2m4s
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m33s
Build and Release / Lint (push) Failing after 1m54s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 2m4s
- Add UserPinnedRepo model for pinning repos to user profiles - Add Pin dropdown in repo header with options for profile/org - Add pin/unpin routes and handlers - Update user profile to show pinned repos with nice cards - User overview tab always visible (like org overview) - Shows empty state with instructions when no pinned repos - Limit of 6 pinned repos per user - Org members can pin repos to organization 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5b0442d357
commit
1986d90df0
137
models/user/user_pinned.go
Normal file
137
models/user/user_pinned.go
Normal file
@ -0,0 +1,137 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
)
|
||||
|
||||
// UserPinnedRepo represents a pinned repository for a user's profile
|
||||
type UserPinnedRepo struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UserID int64 `xorm:"INDEX NOT NULL"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
DisplayOrder int `xorm:"DEFAULT 0"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
|
||||
Repo any `xorm:"-"` // Will be loaded by caller to avoid import cycle
|
||||
}
|
||||
|
||||
// TableName returns the table name for UserPinnedRepo
|
||||
func (p *UserPinnedRepo) TableName() string {
|
||||
return "user_pinned_repo"
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(UserPinnedRepo))
|
||||
}
|
||||
|
||||
// MaxUserPinnedRepos is the maximum number of repos a user can pin
|
||||
const MaxUserPinnedRepos = 6
|
||||
|
||||
// GetUserPinnedRepos returns all pinned repos for a user
|
||||
func GetUserPinnedRepos(ctx context.Context, userID int64) ([]*UserPinnedRepo, error) {
|
||||
pinnedRepos := make([]*UserPinnedRepo, 0, MaxUserPinnedRepos)
|
||||
err := db.GetEngine(ctx).
|
||||
Where("user_id = ?", userID).
|
||||
OrderBy("display_order ASC, id ASC").
|
||||
Find(&pinnedRepos)
|
||||
return pinnedRepos, err
|
||||
}
|
||||
|
||||
// CountUserPinnedRepos returns the count of pinned repos for a user
|
||||
func CountUserPinnedRepos(ctx context.Context, userID int64) (int64, error) {
|
||||
return db.GetEngine(ctx).Where("user_id = ?", userID).Count(new(UserPinnedRepo))
|
||||
}
|
||||
|
||||
// IsRepoPinnedByUser checks if a repo is pinned by a user
|
||||
func IsRepoPinnedByUser(ctx context.Context, userID, repoID int64) (bool, error) {
|
||||
return db.GetEngine(ctx).Where("user_id = ? AND repo_id = ?", userID, repoID).Exist(new(UserPinnedRepo))
|
||||
}
|
||||
|
||||
// PinRepoToUser pins a repo to a user's profile
|
||||
func PinRepoToUser(ctx context.Context, userID, repoID int64) error {
|
||||
// Check if already pinned
|
||||
exists, err := IsRepoPinnedByUser(ctx, userID, repoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return nil // Already pinned
|
||||
}
|
||||
|
||||
// Check max limit
|
||||
count, err := CountUserPinnedRepos(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count >= MaxUserPinnedRepos {
|
||||
return ErrUserPinnedRepoLimit{UserID: userID, Limit: MaxUserPinnedRepos}
|
||||
}
|
||||
|
||||
// Get next display order
|
||||
var maxOrder int
|
||||
_, err = db.GetEngine(ctx).
|
||||
Table("user_pinned_repo").
|
||||
Where("user_id = ?", userID).
|
||||
Select("COALESCE(MAX(display_order), 0)").
|
||||
Get(&maxOrder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pinnedRepo := &UserPinnedRepo{
|
||||
UserID: userID,
|
||||
RepoID: repoID,
|
||||
DisplayOrder: maxOrder + 1,
|
||||
}
|
||||
|
||||
_, err = db.GetEngine(ctx).Insert(pinnedRepo)
|
||||
return err
|
||||
}
|
||||
|
||||
// UnpinRepoFromUser unpins a repo from a user's profile
|
||||
func UnpinRepoFromUser(ctx context.Context, userID, repoID int64) error {
|
||||
_, err := db.GetEngine(ctx).Where("user_id = ? AND repo_id = ?", userID, repoID).Delete(new(UserPinnedRepo))
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateUserPinnedRepoOrder updates the display order of pinned repos
|
||||
func UpdateUserPinnedRepoOrder(ctx context.Context, userID int64, repoIDs []int64) error {
|
||||
for i, repoID := range repoIDs {
|
||||
_, err := db.GetEngine(ctx).
|
||||
Where("user_id = ? AND repo_id = ?", userID, repoID).
|
||||
Cols("display_order").
|
||||
Update(&UserPinnedRepo{DisplayOrder: i})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteUserPinnedReposByRepoID deletes all pins for a repo (when repo is deleted)
|
||||
func DeleteUserPinnedReposByRepoID(ctx context.Context, repoID int64) error {
|
||||
_, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Delete(new(UserPinnedRepo))
|
||||
return err
|
||||
}
|
||||
|
||||
// ErrUserPinnedRepoLimit represents an error when user has reached pin limit
|
||||
type ErrUserPinnedRepoLimit struct {
|
||||
UserID int64
|
||||
Limit int
|
||||
}
|
||||
|
||||
func (err ErrUserPinnedRepoLimit) Error() string {
|
||||
return "user has reached the maximum number of pinned repositories"
|
||||
}
|
||||
|
||||
// IsErrUserPinnedRepoLimit checks if error is ErrUserPinnedRepoLimit
|
||||
func IsErrUserPinnedRepoLimit(err error) bool {
|
||||
_, ok := err.(ErrUserPinnedRepoLimit)
|
||||
return ok
|
||||
}
|
||||
@ -3791,5 +3791,26 @@
|
||||
"org.profile_readme_empty_desc": "Create a .profile repository with a README.md to introduce your organization.",
|
||||
"org.create_profile_repo": "Create Profile Repository",
|
||||
"org.activity": "Activity",
|
||||
"org.repositories": "Repositories"
|
||||
"org.repositories": "Repositories",
|
||||
"repo.pin": "Pin",
|
||||
"repo.pin.tooltip": "Pin this repository",
|
||||
"repo.pin.pin_to_profile": "Pin to your profile",
|
||||
"repo.pin.unpin_from_profile": "Unpin from profile",
|
||||
"repo.pin.pin_to_org": "Pin to organization",
|
||||
"repo.pin.unpin_from_org": "Unpin from organization",
|
||||
"repo.pin.success_profile": "Repository pinned to your profile",
|
||||
"repo.pin.success_org": "Repository pinned to organization",
|
||||
"repo.pin.unpin_success_profile": "Repository unpinned from your profile",
|
||||
"repo.pin.unpin_success_org": "Repository unpinned from organization",
|
||||
"repo.pin.already_pinned_org": "Repository is already pinned",
|
||||
"repo.pin.error_limit": "You have reached the maximum number of pinned repositories (6)",
|
||||
"repo.pin.error_org_limit": "Organization has reached the maximum number of pinned repositories",
|
||||
"repo.pin.error_not_org": "This repository does not belong to an organization",
|
||||
"repo.pin.error_not_member": "You must be a member of the organization to pin repositories",
|
||||
"repo.pin.error_generic": "Failed to update pin status",
|
||||
"repo.pin.error_invalid_type": "Invalid pin type",
|
||||
"user.pinned_repos": "Pinned Repositories",
|
||||
"user.pinned_repos_hint": "Pin repos from the repo page",
|
||||
"user.pinned_repos_empty_title": "No pinned repositories",
|
||||
"user.pinned_repos_empty_desc": "Pin repositories to showcase your best work. Visit a repository and use the Pin dropdown."
|
||||
}
|
||||
128
routers/web/repo/pin.go
Normal file
128
routers/web/repo/pin.go
Normal file
@ -0,0 +1,128 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// Pin handles pinning a repo to user profile or organization
|
||||
func Pin(ctx *context.Context) {
|
||||
pinType := ctx.FormString("type")
|
||||
redirectTo := ctx.FormString("redirect_to")
|
||||
if redirectTo == "" {
|
||||
redirectTo = ctx.Repo.RepoLink
|
||||
}
|
||||
|
||||
switch pinType {
|
||||
case "user":
|
||||
if err := user_model.PinRepoToUser(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID); err != nil {
|
||||
if user_model.IsErrUserPinnedRepoLimit(err) {
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_limit"))
|
||||
} else {
|
||||
log.Error("PinRepoToUser failed: %v", err)
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
|
||||
}
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.pin.success_profile"))
|
||||
}
|
||||
|
||||
case "org":
|
||||
if !ctx.Repo.Repository.Owner.IsOrganization() {
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_not_org"))
|
||||
ctx.Redirect(redirectTo)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user is a member of the org
|
||||
isMember, err := organization.IsOrganizationMember(ctx, ctx.Repo.Repository.OwnerID, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
log.Error("IsOrganizationMember failed: %v", err)
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
|
||||
ctx.Redirect(redirectTo)
|
||||
return
|
||||
}
|
||||
if !isMember {
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_not_member"))
|
||||
ctx.Redirect(redirectTo)
|
||||
return
|
||||
}
|
||||
|
||||
// Use CreateOrgPinnedRepo
|
||||
pinnedRepo := &organization.OrgPinnedRepo{
|
||||
OrgID: ctx.Repo.Repository.OwnerID,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
}
|
||||
if err := organization.CreateOrgPinnedRepo(ctx, pinnedRepo); err != nil {
|
||||
if _, ok := err.(organization.ErrOrgPinnedRepoAlreadyExist); ok {
|
||||
ctx.Flash.Info(ctx.Tr("repo.pin.already_pinned_org"))
|
||||
} else {
|
||||
log.Error("CreateOrgPinnedRepo failed: %v", err)
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
|
||||
}
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.pin.success_org"))
|
||||
}
|
||||
|
||||
default:
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_invalid_type"))
|
||||
}
|
||||
|
||||
ctx.Redirect(redirectTo)
|
||||
}
|
||||
|
||||
// Unpin handles unpinning a repo from user profile or organization
|
||||
func Unpin(ctx *context.Context) {
|
||||
pinType := ctx.FormString("type")
|
||||
redirectTo := ctx.FormString("redirect_to")
|
||||
if redirectTo == "" {
|
||||
redirectTo = ctx.Repo.RepoLink
|
||||
}
|
||||
|
||||
switch pinType {
|
||||
case "user":
|
||||
if err := user_model.UnpinRepoFromUser(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID); err != nil {
|
||||
log.Error("UnpinRepoFromUser failed: %v", err)
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.pin.unpin_success_profile"))
|
||||
}
|
||||
|
||||
case "org":
|
||||
if !ctx.Repo.Repository.Owner.IsOrganization() {
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_not_org"))
|
||||
ctx.Redirect(redirectTo)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user is a member of the org
|
||||
isMember, err := organization.IsOrganizationMember(ctx, ctx.Repo.Repository.OwnerID, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
log.Error("IsOrganizationMember failed: %v", err)
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
|
||||
ctx.Redirect(redirectTo)
|
||||
return
|
||||
}
|
||||
if !isMember {
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_not_member"))
|
||||
ctx.Redirect(redirectTo)
|
||||
return
|
||||
}
|
||||
|
||||
if err := organization.DeleteOrgPinnedRepo(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID); err != nil {
|
||||
log.Error("DeleteOrgPinnedRepo failed: %v", err)
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("repo.pin.unpin_success_org"))
|
||||
}
|
||||
|
||||
default:
|
||||
ctx.Flash.Error(ctx.Tr("repo.pin.error_invalid_type"))
|
||||
}
|
||||
|
||||
ctx.Redirect(redirectTo)
|
||||
}
|
||||
@ -79,15 +79,10 @@ func userProfile(ctx *context.Context) {
|
||||
}
|
||||
|
||||
func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.Repository, profileReadme *git.Blob) {
|
||||
// if there is a profile readme, default to "overview" page, otherwise, default to "repositories" page
|
||||
// if there is not a profile readme, the overview tab should be treated as the repositories tab
|
||||
// Default to overview page for users
|
||||
tab := ctx.FormString("tab")
|
||||
if tab == "" || tab == "overview" {
|
||||
if profileReadme != nil {
|
||||
tab = "overview"
|
||||
} else {
|
||||
tab = "repositories"
|
||||
}
|
||||
if tab == "" {
|
||||
tab = "overview"
|
||||
}
|
||||
ctx.Data["TabName"] = tab
|
||||
ctx.Data["HasUserProfileReadme"] = profileReadme != nil
|
||||
@ -252,16 +247,35 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
|
||||
|
||||
total = int(count)
|
||||
case "overview":
|
||||
if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
|
||||
log.Error("failed to GetBlobContent: %v", err)
|
||||
// Load pinned repositories
|
||||
pinnedRepos, err := user_model.GetUserPinnedRepos(ctx, ctx.ContextUser.ID)
|
||||
if err != nil {
|
||||
log.Error("GetUserPinnedRepos: %v", err)
|
||||
} else {
|
||||
rctx := renderhelper.NewRenderContextRepoFile(ctx, profileDbRepo, renderhelper.RepoFileOptions{
|
||||
CurrentRefPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
|
||||
})
|
||||
if profileContent, err := markdown.RenderString(rctx, bytes); err != nil {
|
||||
log.Error("failed to RenderString: %v", err)
|
||||
// Load repo details for each pinned repo
|
||||
for _, p := range pinnedRepos {
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, p.RepoID)
|
||||
if err == nil {
|
||||
p.Repo = repo
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.Data["UserPinnedRepos"] = pinnedRepos
|
||||
ctx.Data["IsContextUserProfile"] = ctx.Doer != nil && ctx.Doer.ID == ctx.ContextUser.ID
|
||||
|
||||
// Load profile README
|
||||
if profileReadme != nil {
|
||||
if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
|
||||
log.Error("failed to GetBlobContent: %v", err)
|
||||
} else {
|
||||
ctx.Data["ProfileReadmeContent"] = profileContent
|
||||
rctx := renderhelper.NewRenderContextRepoFile(ctx, profileDbRepo, renderhelper.RepoFileOptions{
|
||||
CurrentRefPath: path.Join("branch", util.PathEscapeSegments(profileDbRepo.DefaultBranch)),
|
||||
})
|
||||
if profileContent, err := markdown.RenderString(rctx, bytes); err != nil {
|
||||
log.Error("failed to RenderString: %v", err)
|
||||
} else {
|
||||
ctx.Data["ProfileReadmeContent"] = profileContent
|
||||
}
|
||||
}
|
||||
}
|
||||
case "organizations":
|
||||
|
||||
@ -1710,6 +1710,8 @@ func registerWebRoutes(m *web.Router) {
|
||||
m.Post("/action/{action:star|unstar}", reqSignIn, starsEnabled, repo.ActionStar)
|
||||
m.Post("/action/{action:watch|unwatch}", reqSignIn, repo.ActionWatch)
|
||||
m.Post("/action/{action:accept_transfer|reject_transfer}", reqSignIn, repo.ActionTransfer)
|
||||
m.Get("/action/pin", reqSignIn, repo.Pin)
|
||||
m.Get("/action/unpin", reqSignIn, repo.Unpin)
|
||||
}, optSignIn, context.RepoAssignment)
|
||||
|
||||
common.AddOwnerRepoGitLFSRoutes(m, lfsServerEnabled, repo.CorsHandler(), optSignInFromAnyOrigin) // "/{username}/{reponame}/{lfs-paths}": git-lfs support, see also addOwnerRepoGitHTTPRouters
|
||||
|
||||
@ -19,6 +19,7 @@ import (
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
unit_model "code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
@ -415,6 +416,16 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
|
||||
|
||||
ctx.Repo.Repository = repo
|
||||
ctx.Data["RepoName"] = ctx.Repo.Repository.Name
|
||||
|
||||
// Check if repo is pinned (for pin dropdown)
|
||||
if ctx.Doer != nil {
|
||||
isPinnedToUser, _ := user_model.IsRepoPinnedByUser(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
|
||||
ctx.Data["IsRepoPinnedToUser"] = isPinnedToUser
|
||||
}
|
||||
if ctx.Repo.Repository.Owner.IsOrganization() {
|
||||
isPinnedToOrg, _ := organization.IsRepoPinned(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID)
|
||||
ctx.Data["IsRepoPinnedToOrg"] = isPinnedToOrg
|
||||
}
|
||||
ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty
|
||||
}
|
||||
|
||||
|
||||
@ -65,6 +65,7 @@
|
||||
{{if not $.DisableStars}}
|
||||
{{template "repo/star_unstar" $}}
|
||||
{{end}}
|
||||
{{template "repo/pin_unpin" $}}
|
||||
{{if and (not .IsEmpty) ($.Permission.CanRead ctx.Consts.RepoUnitTypeCode)}}
|
||||
<div class="ui labeled button
|
||||
{{if or (not $.IsSigned) (and (not $.CanSignedUserFork) (not $.UserAndOrgForks))}}
|
||||
|
||||
33
templates/repo/pin_unpin.tmpl
Normal file
33
templates/repo/pin_unpin.tmpl
Normal file
@ -0,0 +1,33 @@
|
||||
{{if $.IsSigned}}
|
||||
<div class="ui labeled button" id="pin-repo-dropdown">
|
||||
<div class="ui compact small basic button dropdown" data-tooltip-content="{{ctx.Locale.Tr "repo.pin.tooltip"}}">
|
||||
{{svg "octicon-pin" 16}}<span class="text not-mobile">{{ctx.Locale.Tr "repo.pin"}}</span>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="menu">
|
||||
{{/* Pin to user profile */}}
|
||||
{{if $.IsRepoPinnedToUser}}
|
||||
<a class="item" href="{{$.RepoLink}}/action/unpin?type=user&redirect_to={{$.Link}}">
|
||||
{{svg "octicon-pin-slash" 16}} {{ctx.Locale.Tr "repo.pin.unpin_from_profile"}}
|
||||
</a>
|
||||
{{else}}
|
||||
<a class="item" href="{{$.RepoLink}}/action/pin?type=user&redirect_to={{$.Link}}">
|
||||
{{svg "octicon-person" 16}} {{ctx.Locale.Tr "repo.pin.pin_to_profile"}}
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{/* Pin to organization (if applicable) */}}
|
||||
{{if and .Repository.Owner.IsOrganization $.IsOrganizationMember}}
|
||||
{{if $.IsRepoPinnedToOrg}}
|
||||
<a class="item" href="{{$.RepoLink}}/action/unpin?type=org&redirect_to={{$.Link}}">
|
||||
{{svg "octicon-pin-slash" 16}} {{ctx.Locale.Tr "repo.pin.unpin_from_org"}}
|
||||
</a>
|
||||
{{else}}
|
||||
<a class="item" href="{{$.RepoLink}}/action/pin?type=org&redirect_to={{$.Link}}">
|
||||
{{svg "octicon-organization" 16}} {{ctx.Locale.Tr "repo.pin.pin_to_org"}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@ -1,6 +1,6 @@
|
||||
<overflow-menu class="ui secondary pointing tabular borderless menu">
|
||||
<div class="overflow-menu-items">
|
||||
{{if and .HasUserProfileReadme .ContextUser.IsIndividual}}
|
||||
{{if .ContextUser.IsIndividual}}
|
||||
<a class="{{if eq .TabName "overview"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=overview">
|
||||
{{svg "octicon-info"}} {{ctx.Locale.Tr "user.overview"}}
|
||||
</a>
|
||||
|
||||
@ -26,7 +26,71 @@
|
||||
{{else if eq .TabName "followers"}}
|
||||
{{template "repo/user_cards" .}}
|
||||
{{else if eq .TabName "overview"}}
|
||||
{{/* Pinned Repositories Section */}}
|
||||
{{if or .UserPinnedRepos .IsContextUserProfile}}
|
||||
<div class="ui segment pinned-repos-section tw-mb-4">
|
||||
<h4 class="ui header tw-flex tw-items-center">
|
||||
{{svg "octicon-pin" 16}} {{ctx.Locale.Tr "user.pinned_repos"}}
|
||||
{{if .IsContextUserProfile}}
|
||||
<span class="tw-ml-auto text grey tw-text-sm">{{ctx.Locale.Tr "user.pinned_repos_hint"}}</span>
|
||||
{{end}}
|
||||
</h4>
|
||||
|
||||
{{if .UserPinnedRepos}}
|
||||
<div class="ui three stackable cards pinned-repos">
|
||||
{{range .UserPinnedRepos}}
|
||||
{{if .Repo}}
|
||||
<a class="ui card" href="{{.Repo.Link}}">
|
||||
<div class="content">
|
||||
<div class="header text truncate">
|
||||
{{if .Repo.IsPrivate}}{{svg "octicon-lock" 16}}{{else if .Repo.IsFork}}{{svg "octicon-repo-forked" 16}}{{else if .Repo.IsMirror}}{{svg "octicon-mirror" 16}}{{else}}{{svg "octicon-repo" 16}}{{end}}
|
||||
{{if ne .Repo.OwnerID $.ContextUser.ID}}
|
||||
<span class="text grey">{{.Repo.Owner.Name}}/</span>
|
||||
{{end}}
|
||||
{{.Repo.Name}}
|
||||
</div>
|
||||
{{if .Repo.Description}}
|
||||
<div class="description text truncate">{{.Repo.Description}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="extra content">
|
||||
{{if .Repo.PrimaryLanguage}}
|
||||
<span class="tw-mr-2">
|
||||
<span class="repo-language-color" style="background-color: {{.Repo.PrimaryLanguage.Color}}"></span>
|
||||
{{.Repo.PrimaryLanguage.Language}}
|
||||
</span>
|
||||
{{end}}
|
||||
{{if .Repo.NumStars}}
|
||||
<span class="tw-mr-2">{{svg "octicon-star" 14}} {{.Repo.NumStars}}</span>
|
||||
{{end}}
|
||||
{{if .Repo.NumForks}}
|
||||
<span>{{svg "octicon-repo-forked" 14}} {{.Repo.NumForks}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{else if .IsContextUserProfile}}
|
||||
<div class="ui placeholder segment tw-text-center">
|
||||
<div class="ui icon header">
|
||||
{{svg "octicon-pin" 32}}
|
||||
<div class="content">
|
||||
{{ctx.Locale.Tr "user.pinned_repos_empty_title"}}
|
||||
<div class="sub header">
|
||||
{{ctx.Locale.Tr "user.pinned_repos_empty_desc"}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{/* Profile README */}}
|
||||
{{if .ProfileReadmeContent}}
|
||||
<div id="readme_profile" class="render-content markup">{{.ProfileReadmeContent}}</div>
|
||||
{{end}}
|
||||
{{else if eq .TabName "organizations"}}
|
||||
{{template "repo/user_cards" .}}
|
||||
{{else}}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user