Skip to content

Commit c6ca8e7

Browse files
committed
feature: custom repo buttons
which is similar to Sponsor in github, and user can use it to do other things they like also :) limit the buttons number to 3, because too many buttons will break header ui. Signed-off-by: a1012112796 <[email protected]>
1 parent a3c4c57 commit c6ca8e7

File tree

11 files changed

+302
-1
lines changed

11 files changed

+302
-1
lines changed

models/migrations/migrations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,8 @@ var migrations = []Migration{
309309
NewMigration("Add LFS columns to Mirror", addLFSMirrorColumns),
310310
// v179 -> v180
311311
NewMigration("Convert avatar url to text", convertAvatarURLToText),
312+
// v180 -> v181
313+
NewMigration("add custom_repo_buttons_config column for repository table", addCustomRepoButtonsConfigRepositoryColumn),
312314
}
313315

314316
// GetCurrentDBVersion returns the current db version

models/migrations/v180.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright 2021 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migrations
6+
7+
import (
8+
"fmt"
9+
10+
"xorm.io/xorm"
11+
)
12+
13+
func addCustomRepoButtonsConfigRepositoryColumn(x *xorm.Engine) error {
14+
type Repository struct {
15+
CustomRepoButtonsConfig string `xorm:"TEXT"`
16+
}
17+
18+
if err := x.Sync2(new(Repository)); err != nil {
19+
return fmt.Errorf("sync2: %v", err)
20+
}
21+
return nil
22+
}

models/repo.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
api "code.gitea.io/gitea/modules/structs"
3535
"code.gitea.io/gitea/modules/timeutil"
3636
"code.gitea.io/gitea/modules/util"
37+
"gopkg.in/yaml.v3"
3738

3839
"xorm.io/builder"
3940
)
@@ -246,6 +247,9 @@ type Repository struct {
246247
// Avatar: ID(10-20)-md5(32) - must fit into 64 symbols
247248
Avatar string `xorm:"VARCHAR(64)"`
248249

250+
CustomRepoButtonsConfig string `xorm:"TEXT"`
251+
CustomRepoButtons []CustomRepoButton `xorm:"-"`
252+
249253
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
250254
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
251255
}
@@ -2117,3 +2121,86 @@ func IterateRepository(f func(repo *Repository) error) error {
21172121
}
21182122
}
21192123
}
2124+
2125+
// CustomRepoButtonType type of custom repo button
2126+
type CustomRepoButtonType string
2127+
2128+
const (
2129+
// CustomRepoButtonTypeLink a single link (default)
2130+
CustomRepoButtonTypeLink CustomRepoButtonType = "link"
2131+
// CustomRepoButtonTypeContent some content with markdown format
2132+
CustomRepoButtonTypeContent = "content"
2133+
// CustomRepoButtonExample examle config
2134+
CustomRepoButtonExample string = `-
2135+
title: Sponsor
2136+
type: link
2137+
link: http://www.example.com
2138+
2139+
-
2140+
title: Sponsor 2
2141+
type: content
2142+
content: "## test content \n - [xx](http://www.example.com)"
2143+
`
2144+
)
2145+
2146+
// CustomRepoButton a config of CustomRepoButton
2147+
type CustomRepoButton struct {
2148+
Title string `yaml:"title"` // max length: 20
2149+
Typ CustomRepoButtonType `yaml:"type"`
2150+
Link string `yaml:"link"`
2151+
Content string `yaml:"content"`
2152+
RenderedContent string `yaml:"-"`
2153+
}
2154+
2155+
// IsLink check if it's a link button
2156+
func (b CustomRepoButton) IsLink() bool {
2157+
return b.Typ != CustomRepoButtonTypeContent
2158+
}
2159+
2160+
// LoadCustomRepoButton by config
2161+
func (repo *Repository) LoadCustomRepoButton() error {
2162+
if repo.CustomRepoButtons != nil {
2163+
return nil
2164+
}
2165+
2166+
repo.CustomRepoButtons = make([]CustomRepoButton, 0, 3)
2167+
err := yaml.Unmarshal([]byte(repo.CustomRepoButtonsConfig), &repo.CustomRepoButtons)
2168+
if err != nil {
2169+
return err
2170+
}
2171+
2172+
return nil
2173+
}
2174+
2175+
// CustomRepoButtonConfigVaild format check
2176+
func CustomRepoButtonConfigVaild(cfg string) (bool, error) {
2177+
btns := make([]CustomRepoButton, 0, 3)
2178+
2179+
err := yaml.Unmarshal([]byte(cfg), &btns)
2180+
if err != nil {
2181+
return false, err
2182+
}
2183+
2184+
// max button nums: 3
2185+
if len(btns) > 3 {
2186+
return false, nil
2187+
}
2188+
2189+
for _, btn := range btns {
2190+
if len(btn.Title) > 20 {
2191+
return false, nil
2192+
}
2193+
if btn.Typ != CustomRepoButtonTypeContent && len(btn.Link) == 0 {
2194+
return false, nil
2195+
}
2196+
}
2197+
2198+
return true, nil
2199+
}
2200+
2201+
// SetCustomRepoButtons sets custom button config
2202+
func (repo *Repository) SetCustomRepoButtons(cfg string) (err error) {
2203+
repo.CustomRepoButtonsConfig = cfg
2204+
_, err = x.Where("id = ?", repo.ID).Cols("custom_repo_buttons_config").NoAutoTime().Update(repo)
2205+
return
2206+
}

models/repo_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,96 @@ func TestRepoGetReviewerTeams(t *testing.T) {
221221
assert.NoError(t, err)
222222
assert.Equal(t, 2, len(teams))
223223
}
224+
225+
func TestRepo_LoadCustomRepoButton(t *testing.T) {
226+
assert.NoError(t, PrepareTestDatabase())
227+
228+
repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
229+
230+
repo1.CustomRepoButtonsConfig = CustomRepoButtonExample
231+
232+
assert.NoError(t, repo1.LoadCustomRepoButton())
233+
}
234+
235+
func TestCustomRepoButtonConfigVaild(t *testing.T) {
236+
tests := []struct {
237+
name string
238+
cfg string
239+
want bool
240+
wantErr bool
241+
}{
242+
// empty
243+
{
244+
name: "empty",
245+
cfg: "",
246+
want: true,
247+
wantErr: false,
248+
},
249+
// right config
250+
{
251+
name: "right config",
252+
cfg: CustomRepoButtonExample,
253+
want: true,
254+
wantErr: false,
255+
},
256+
// missing link
257+
{
258+
name: "missing link",
259+
cfg: `-
260+
title: Sponsor
261+
type: link
262+
`,
263+
want: false,
264+
wantErr: false,
265+
},
266+
// too many buttons
267+
{
268+
name: "too many buttons",
269+
cfg: `-
270+
title: Sponsor
271+
type: link
272+
link: http://www.example.com
273+
274+
-
275+
title: Sponsor
276+
type: link
277+
link: http://www.example.com
278+
279+
-
280+
title: Sponsor
281+
type: link
282+
link: http://www.example.com
283+
284+
-
285+
title: Sponsor
286+
type: link
287+
link: http://www.example.com
288+
`,
289+
want: false,
290+
wantErr: false,
291+
},
292+
// too long title
293+
{
294+
name: "too long title",
295+
cfg: `-
296+
title: Sponsor-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
297+
type: link
298+
link: http://www.example.com
299+
`,
300+
want: false,
301+
wantErr: false,
302+
},
303+
}
304+
for _, tt := range tests {
305+
t.Run(tt.name, func(t *testing.T) {
306+
got, err := CustomRepoButtonConfigVaild(tt.cfg)
307+
if (err != nil) != tt.wantErr {
308+
t.Errorf("CustomRepoButtonConfigVaild() error = %v, wantErr %v", err, tt.wantErr)
309+
return
310+
}
311+
if got != tt.want {
312+
t.Errorf("CustomRepoButtonConfigVaild() = %v, want %v", got, tt.want)
313+
}
314+
})
315+
}
316+
}

modules/context/repo.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,19 @@ func repoAssignment(ctx *Context, repo *models.Repository) {
370370
ctx.Repo.Repository = repo
371371
ctx.Data["RepoName"] = ctx.Repo.Repository.Name
372372
ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty
373+
374+
// load custom repo buttons
375+
if err := ctx.Repo.Repository.LoadCustomRepoButton(); err != nil {
376+
ctx.ServerError("LoadCustomRepoButton", err)
377+
return
378+
}
379+
380+
for index, btn := range repo.CustomRepoButtons {
381+
if !btn.IsLink() {
382+
repo.CustomRepoButtons[index].RenderedContent = string(markdown.Render([]byte(btn.Content), ctx.Repo.RepoLink,
383+
ctx.Repo.Repository.ComposeMetas()))
384+
}
385+
}
373386
}
374387

375388
// RepoIDAssignment returns a handler which assigns the repo to the context.

options/locale/locale_en-US.ini

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1859,6 +1859,12 @@ settings.lfs_pointers.exists=Exists in store
18591859
settings.lfs_pointers.accessible=Accessible to User
18601860
settings.lfs_pointers.associateAccessible=Associate accessible %d OIDs
18611861

1862+
custom_repo_buttons_cfg_desc = configuration
1863+
settings.custom_repo_buttons = Custom repo buttons
1864+
settings.custom_repo_buttons.wrong_setting = Wrong custom repo buttons config
1865+
settings.custom_repo_buttons.error = An error occurred while trying to set custom repo buttons for the repo. See the log for more details.
1866+
settings.custom_repo_buttons.success = custom repo buttons was successfully seted.
1867+
18621868
diff.browse_source = Browse Source
18631869
diff.parent = parent
18641870
diff.commit = commit

routers/repo/setting.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ func Settings(ctx *context.Context) {
5353
ctx.Data["SigningKeyAvailable"] = len(signing) > 0
5454
ctx.Data["SigningSettings"] = setting.Repository.Signing
5555

56+
ctx.Data["CustomRepoButtonExample"] = models.CustomRepoButtonExample
57+
5658
ctx.HTML(http.StatusOK, tplSettingsOptions)
5759
}
5860

@@ -612,7 +614,21 @@ func SettingsPost(ctx *context.Context) {
612614

613615
log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name)
614616
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
615-
617+
case "custom_repo_buttons":
618+
if ok, _ := models.CustomRepoButtonConfigVaild(form.CustomRepoButtonsCfg); !ok {
619+
ctx.Flash.Error(ctx.Tr("repo.settings.custom_repo_buttons.wrong_setting"))
620+
ctx.Data["Err_CustomRepoButtons"] = true
621+
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
622+
return
623+
}
624+
if err := repo.SetCustomRepoButtons(form.CustomRepoButtonsCfg); err != nil {
625+
log.Error("repo.SetCustomRepoButtons: %s", err)
626+
ctx.Flash.Error(ctx.Tr("repo.settings.custom_repo_buttons.error"))
627+
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
628+
return
629+
}
630+
ctx.Flash.Success(ctx.Tr("repo.settings.custom_repo_buttons.success"))
631+
ctx.Redirect(ctx.Repo.RepoLink + "/settings")
616632
default:
617633
ctx.NotFound("", nil)
618634
}

services/forms/repo_form.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,9 @@ type RepoSettingForm struct {
156156

157157
// Admin settings
158158
EnableHealthCheck bool
159+
160+
// custom repo buttons
161+
CustomRepoButtonsCfg string
159162
}
160163

161164
// Validate validates the fields

templates/repo/header.tmpl

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,21 @@
4242
</div>
4343
{{if not .IsBeingCreated}}
4444
<div class="repo-buttons">
45+
{{range .CustomRepoButtons}}
46+
<div class="ui labeled button">
47+
{{if .IsLink}}
48+
<a class="ui basic label" href="{{.Link}}" target="_blank" rel="noopener noreferrer">
49+
{{.Title}}
50+
</a>
51+
{{else}}
52+
<a class="ui basic label show-repo-button-content"
53+
data-title="{{.Title}}"
54+
data-content="{{.RenderedContent}}">
55+
{{.Title}}
56+
</a>
57+
{{end}}
58+
</div>
59+
{{end}}
4560
{{if $.RepoTransfer}}
4661
<form method="post" action="{{$.RepoLink}}/action/accept_transfer?redirect_to={{$.RepoLink}}">
4762
{{$.CsrfTokenHtml}}
@@ -98,6 +113,16 @@
98113
{{end}}
99114
</div><!-- end grid -->
100115
</div><!-- end container -->
116+
117+
<div class="ui modal custom-repo-buttons" id="detail-modal">
118+
{{svg "octicon-x" 16 "close inside"}}
119+
<div class="content">
120+
<div class="sub header"></div>
121+
<div class="ui divider"></div>
122+
<div class="render-content markdown"></div>
123+
</div>
124+
</div>
125+
101126
{{end}}
102127
<div class="ui tabs container">
103128
{{if not .Repository.IsBeingCreated}}

templates/repo/settings/options.tmpl

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,24 @@
473473
</div>
474474
{{end}}
475475

476+
<h4 class="ui top attached header">
477+
{{.i18n.Tr "repo.settings.custom_repo_buttons"}}
478+
</h4>
479+
<div class="ui attached segment">
480+
<form class="ui form" method="post">
481+
{{.CsrfTokenHtml}}
482+
<input type="hidden" name="action" value="custom_repo_buttons">
483+
<div class="field {{if .Err_CustomRepoButtons}}error{{end}}">
484+
<label for="custom_repo_buttons_cfg">{{$.i18n.Tr "repo.custom_repo_buttons_cfg_desc"}}</label>
485+
<textarea id="custom_repo_buttons_cfg" name="custom_repo_buttons_cfg" rows="4" placeholder="{{$.CustomRepoButtonExample}}">{{.Repository.CustomRepoButtonsConfig}}</textarea>
486+
</div>
487+
<div class="ui divider"></div>
488+
<div class="field">
489+
<button class="ui green button">{{$.i18n.Tr "repo.settings.update_settings"}}</button>
490+
</div>
491+
</form>
492+
</div>
493+
476494
{{if .Permission.IsOwner}}
477495
<h4 class="ui top attached error header">
478496
{{.i18n.Tr "repo.settings.danger_zone"}}

web_src/js/index.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,6 +1279,22 @@ async function initRepository() {
12791279
$('.language-stats-details, .repository-menu').slideToggle();
12801280
});
12811281
}
1282+
1283+
// custom repo buttons
1284+
(function() {
1285+
if ($('.repo-buttons').length === 0) {
1286+
return;
1287+
}
1288+
1289+
const $detailModal = $('#detail-modal');
1290+
1291+
$('.show-repo-button-content').on('click', function () {
1292+
$detailModal.find('.content .render-content').html($(this).data('content'));
1293+
$detailModal.find('.sub.header').text($(this).data('title'));
1294+
$detailModal.modal('show');
1295+
return false;
1296+
});
1297+
})();
12821298
}
12831299

12841300
function initPullRequestMergeInstruction() {

0 commit comments

Comments
 (0)