From c6ca8e7fb4fb39e5ac82b82ba68c5313a3e54d4d Mon Sep 17 00:00:00 2001 From: a1012112796 <1012112796@qq.com> Date: Sun, 18 Apr 2021 15:21:27 +0800 Subject: [PATCH 1/4] 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 <1012112796@qq.com> --- models/migrations/migrations.go | 2 + models/migrations/v180.go | 22 +++++++ models/repo.go | 87 ++++++++++++++++++++++++++ models/repo_test.go | 93 ++++++++++++++++++++++++++++ modules/context/repo.go | 13 ++++ options/locale/locale_en-US.ini | 6 ++ routers/repo/setting.go | 18 +++++- services/forms/repo_form.go | 3 + templates/repo/header.tmpl | 25 ++++++++ templates/repo/settings/options.tmpl | 18 ++++++ web_src/js/index.js | 16 +++++ 11 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 models/migrations/v180.go diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index c54c383fb810d..746c9ee00321d 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -309,6 +309,8 @@ var migrations = []Migration{ NewMigration("Add LFS columns to Mirror", addLFSMirrorColumns), // v179 -> v180 NewMigration("Convert avatar url to text", convertAvatarURLToText), + // v180 -> v181 + NewMigration("add custom_repo_buttons_config column for repository table", addCustomRepoButtonsConfigRepositoryColumn), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v180.go b/models/migrations/v180.go new file mode 100644 index 0000000000000..c01291af969f1 --- /dev/null +++ b/models/migrations/v180.go @@ -0,0 +1,22 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "fmt" + + "xorm.io/xorm" +) + +func addCustomRepoButtonsConfigRepositoryColumn(x *xorm.Engine) error { + type Repository struct { + CustomRepoButtonsConfig string `xorm:"TEXT"` + } + + if err := x.Sync2(new(Repository)); err != nil { + return fmt.Errorf("sync2: %v", err) + } + return nil +} diff --git a/models/repo.go b/models/repo.go index bdb84ee00da55..145c2852b8580 100644 --- a/models/repo.go +++ b/models/repo.go @@ -34,6 +34,7 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" + "gopkg.in/yaml.v3" "xorm.io/builder" ) @@ -246,6 +247,9 @@ type Repository struct { // Avatar: ID(10-20)-md5(32) - must fit into 64 symbols Avatar string `xorm:"VARCHAR(64)"` + CustomRepoButtonsConfig string `xorm:"TEXT"` + CustomRepoButtons []CustomRepoButton `xorm:"-"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` } @@ -2117,3 +2121,86 @@ func IterateRepository(f func(repo *Repository) error) error { } } } + +// CustomRepoButtonType type of custom repo button +type CustomRepoButtonType string + +const ( + // CustomRepoButtonTypeLink a single link (default) + CustomRepoButtonTypeLink CustomRepoButtonType = "link" + // CustomRepoButtonTypeContent some content with markdown format + CustomRepoButtonTypeContent = "content" + // CustomRepoButtonExample examle config + CustomRepoButtonExample string = `- + title: Sponsor + type: link + link: http://www.example.com + +- + title: Sponsor 2 + type: content + content: "## test content \n - [xx](http://www.example.com)" +` +) + +// CustomRepoButton a config of CustomRepoButton +type CustomRepoButton struct { + Title string `yaml:"title"` // max length: 20 + Typ CustomRepoButtonType `yaml:"type"` + Link string `yaml:"link"` + Content string `yaml:"content"` + RenderedContent string `yaml:"-"` +} + +// IsLink check if it's a link button +func (b CustomRepoButton) IsLink() bool { + return b.Typ != CustomRepoButtonTypeContent +} + +// LoadCustomRepoButton by config +func (repo *Repository) LoadCustomRepoButton() error { + if repo.CustomRepoButtons != nil { + return nil + } + + repo.CustomRepoButtons = make([]CustomRepoButton, 0, 3) + err := yaml.Unmarshal([]byte(repo.CustomRepoButtonsConfig), &repo.CustomRepoButtons) + if err != nil { + return err + } + + return nil +} + +// CustomRepoButtonConfigVaild format check +func CustomRepoButtonConfigVaild(cfg string) (bool, error) { + btns := make([]CustomRepoButton, 0, 3) + + err := yaml.Unmarshal([]byte(cfg), &btns) + if err != nil { + return false, err + } + + // max button nums: 3 + if len(btns) > 3 { + return false, nil + } + + for _, btn := range btns { + if len(btn.Title) > 20 { + return false, nil + } + if btn.Typ != CustomRepoButtonTypeContent && len(btn.Link) == 0 { + return false, nil + } + } + + return true, nil +} + +// SetCustomRepoButtons sets custom button config +func (repo *Repository) SetCustomRepoButtons(cfg string) (err error) { + repo.CustomRepoButtonsConfig = cfg + _, err = x.Where("id = ?", repo.ID).Cols("custom_repo_buttons_config").NoAutoTime().Update(repo) + return +} diff --git a/models/repo_test.go b/models/repo_test.go index 10ba2c99f8973..edf2eb3937f57 100644 --- a/models/repo_test.go +++ b/models/repo_test.go @@ -221,3 +221,96 @@ func TestRepoGetReviewerTeams(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 2, len(teams)) } + +func TestRepo_LoadCustomRepoButton(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + + repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) + + repo1.CustomRepoButtonsConfig = CustomRepoButtonExample + + assert.NoError(t, repo1.LoadCustomRepoButton()) +} + +func TestCustomRepoButtonConfigVaild(t *testing.T) { + tests := []struct { + name string + cfg string + want bool + wantErr bool + }{ + // empty + { + name: "empty", + cfg: "", + want: true, + wantErr: false, + }, + // right config + { + name: "right config", + cfg: CustomRepoButtonExample, + want: true, + wantErr: false, + }, + // missing link + { + name: "missing link", + cfg: `- + title: Sponsor + type: link +`, + want: false, + wantErr: false, + }, + // too many buttons + { + name: "too many buttons", + cfg: `- + title: Sponsor + type: link + link: http://www.example.com + +- + title: Sponsor + type: link + link: http://www.example.com + +- + title: Sponsor + type: link + link: http://www.example.com + +- + title: Sponsor + type: link + link: http://www.example.com +`, + want: false, + wantErr: false, + }, + // too long title + { + name: "too long title", + cfg: `- + title: Sponsor-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + type: link + link: http://www.example.com +`, + want: false, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := CustomRepoButtonConfigVaild(tt.cfg) + if (err != nil) != tt.wantErr { + t.Errorf("CustomRepoButtonConfigVaild() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("CustomRepoButtonConfigVaild() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/modules/context/repo.go b/modules/context/repo.go index 5ce31e9e3504b..a044202fd69fd 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -370,6 +370,19 @@ func repoAssignment(ctx *Context, repo *models.Repository) { ctx.Repo.Repository = repo ctx.Data["RepoName"] = ctx.Repo.Repository.Name ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty + + // load custom repo buttons + if err := ctx.Repo.Repository.LoadCustomRepoButton(); err != nil { + ctx.ServerError("LoadCustomRepoButton", err) + return + } + + for index, btn := range repo.CustomRepoButtons { + if !btn.IsLink() { + repo.CustomRepoButtons[index].RenderedContent = string(markdown.Render([]byte(btn.Content), ctx.Repo.RepoLink, + ctx.Repo.Repository.ComposeMetas())) + } + } } // RepoIDAssignment returns a handler which assigns the repo to the context. diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 1a8d253749cf5..3c393f97e178c 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1859,6 +1859,12 @@ settings.lfs_pointers.exists=Exists in store settings.lfs_pointers.accessible=Accessible to User settings.lfs_pointers.associateAccessible=Associate accessible %d OIDs +custom_repo_buttons_cfg_desc = configuration +settings.custom_repo_buttons = Custom repo buttons +settings.custom_repo_buttons.wrong_setting = Wrong custom repo buttons config +settings.custom_repo_buttons.error = An error occurred while trying to set custom repo buttons for the repo. See the log for more details. +settings.custom_repo_buttons.success = custom repo buttons was successfully seted. + diff.browse_source = Browse Source diff.parent = parent diff.commit = commit diff --git a/routers/repo/setting.go b/routers/repo/setting.go index 533adcbdf6ba5..d89e2c14aff4d 100644 --- a/routers/repo/setting.go +++ b/routers/repo/setting.go @@ -53,6 +53,8 @@ func Settings(ctx *context.Context) { ctx.Data["SigningKeyAvailable"] = len(signing) > 0 ctx.Data["SigningSettings"] = setting.Repository.Signing + ctx.Data["CustomRepoButtonExample"] = models.CustomRepoButtonExample + ctx.HTML(http.StatusOK, tplSettingsOptions) } @@ -612,7 +614,21 @@ func SettingsPost(ctx *context.Context) { log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name) ctx.Redirect(ctx.Repo.RepoLink + "/settings") - + case "custom_repo_buttons": + if ok, _ := models.CustomRepoButtonConfigVaild(form.CustomRepoButtonsCfg); !ok { + ctx.Flash.Error(ctx.Tr("repo.settings.custom_repo_buttons.wrong_setting")) + ctx.Data["Err_CustomRepoButtons"] = true + ctx.Redirect(ctx.Repo.RepoLink + "/settings") + return + } + if err := repo.SetCustomRepoButtons(form.CustomRepoButtonsCfg); err != nil { + log.Error("repo.SetCustomRepoButtons: %s", err) + ctx.Flash.Error(ctx.Tr("repo.settings.custom_repo_buttons.error")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings") + return + } + ctx.Flash.Success(ctx.Tr("repo.settings.custom_repo_buttons.success")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings") default: ctx.NotFound("", nil) } diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index 55d1f6e3bc386..8d6ba21860ab3 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -156,6 +156,9 @@ type RepoSettingForm struct { // Admin settings EnableHealthCheck bool + + // custom repo buttons + CustomRepoButtonsCfg string } // Validate validates the fields diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index ebd0333e8ca5e..4c93ff6937036 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -42,6 +42,21 @@ {{if not .IsBeingCreated}}
+ {{range .CustomRepoButtons}} +
+ {{if .IsLink}} + + {{.Title}} + + {{else}} + + {{.Title}} + + {{end}} +
+ {{end}} {{if $.RepoTransfer}}
{{$.CsrfTokenHtml}} @@ -98,6 +113,16 @@ {{end}}
+ + + {{end}}
{{if not .Repository.IsBeingCreated}} diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index 012bff317da57..a771ed7cee91e 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -473,6 +473,24 @@
{{end}} +

+ {{.i18n.Tr "repo.settings.custom_repo_buttons"}} +

+
+ + {{.CsrfTokenHtml}} + +
+ + +
+
+
+ +
+ +
+ {{if .Permission.IsOwner}}

{{.i18n.Tr "repo.settings.danger_zone"}} diff --git a/web_src/js/index.js b/web_src/js/index.js index 1716df9e7fa64..dc41f95fd37e6 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -1279,6 +1279,22 @@ async function initRepository() { $('.language-stats-details, .repository-menu').slideToggle(); }); } + + // custom repo buttons + (function() { + if ($('.repo-buttons').length === 0) { + return; + } + + const $detailModal = $('#detail-modal'); + + $('.show-repo-button-content').on('click', function () { + $detailModal.find('.content .render-content').html($(this).data('content')); + $detailModal.find('.sub.header').text($(this).data('title')); + $detailModal.modal('show'); + return false; + }); + })(); } function initPullRequestMergeInstruction() { From bad0c3b2a0cdfd027f9ab995cc26ea52300adf9f Mon Sep 17 00:00:00 2001 From: a1012112796 <1012112796@qq.com> Date: Sun, 18 Apr 2021 15:49:57 +0800 Subject: [PATCH 2/4] fix version --- models/repo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/repo.go b/models/repo.go index 145c2852b8580..aaa8dc038bbe4 100644 --- a/models/repo.go +++ b/models/repo.go @@ -34,7 +34,7 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" - "gopkg.in/yaml.v3" + "gopkg.in/yaml.v2" "xorm.io/builder" ) From 8f10b46dfbb8aeb3b8ed911d04ff1c19710bb250 Mon Sep 17 00:00:00 2001 From: a1012112796 <1012112796@qq.com> Date: Tue, 20 Apr 2021 09:45:56 +0800 Subject: [PATCH 3/4] simplify --- modules/context/repo.go | 14 +++++++++++--- templates/repo/header.tmpl | 22 +++++++++++----------- web_src/js/index.js | 6 +----- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/modules/context/repo.go b/modules/context/repo.go index a044202fd69fd..70a0b89eabcc5 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" @@ -378,9 +379,16 @@ func repoAssignment(ctx *Context, repo *models.Repository) { } for index, btn := range repo.CustomRepoButtons { - if !btn.IsLink() { - repo.CustomRepoButtons[index].RenderedContent = string(markdown.Render([]byte(btn.Content), ctx.Repo.RepoLink, - ctx.Repo.Repository.ComposeMetas())) + if btn.IsLink() { + continue + } + + if repo.CustomRepoButtons[index].RenderedContent, err = markdown.RenderString(&markup.RenderContext{ + URLPrefix: ctx.Repo.Repository.Link(), + Metas: ctx.Repo.Repository.ComposeMetas(), + }, btn.Content); err != nil { + ctx.ServerError("LoadCustomRepoButton", err) + return } } } diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index 4c93ff6937036..4e7653d5cad5e 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -42,6 +42,7 @@ {{if not .IsBeingCreated}}
+ {{ $index := 0}} {{range .CustomRepoButtons}}
{{if .IsLink}} @@ -50,12 +51,20 @@ {{else}} + data-index="{{$index}}"> {{.Title}} + {{end}}
+ {{ $index = Add $index 1}} {{end}} {{if $.RepoTransfer}}
@@ -114,15 +123,6 @@
- - {{end}}
{{if not .Repository.IsBeingCreated}} diff --git a/web_src/js/index.js b/web_src/js/index.js index dc41f95fd37e6..ec6a4c2deb1f5 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -1286,12 +1286,8 @@ async function initRepository() { return; } - const $detailModal = $('#detail-modal'); - $('.show-repo-button-content').on('click', function () { - $detailModal.find('.content .render-content').html($(this).data('content')); - $detailModal.find('.sub.header').text($(this).data('title')); - $detailModal.modal('show'); + $(`#detail-modal-${$(this).data('index')}`).modal('show'); return false; }); })(); From b0125ae41888d556dd66300abf937625811e5b7a Mon Sep 17 00:00:00 2001 From: a1012112796 <1012112796@qq.com> Date: Sun, 25 Apr 2021 11:54:05 +0800 Subject: [PATCH 4/4] fix a small nit --- models/repo.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/models/repo.go b/models/repo.go index 99ed5cbd65c2b..eca49b975e0e9 100644 --- a/models/repo.go +++ b/models/repo.go @@ -2149,7 +2149,7 @@ const ( // CustomRepoButton a config of CustomRepoButton type CustomRepoButton struct { Title string `yaml:"title"` // max length: 20 - Typ CustomRepoButtonType `yaml:"type"` + Type CustomRepoButtonType `yaml:"type"` Link string `yaml:"link"` Content string `yaml:"content"` RenderedContent string `yaml:"-"` @@ -2157,7 +2157,7 @@ type CustomRepoButton struct { // IsLink check if it's a link button func (b CustomRepoButton) IsLink() bool { - return b.Typ != CustomRepoButtonTypeContent + return b.Type != CustomRepoButtonTypeContent } // LoadCustomRepoButton by config @@ -2193,7 +2193,7 @@ func CustomRepoButtonConfigVaild(cfg string) (bool, error) { if len(btn.Title) > 20 { return false, nil } - if btn.Typ != CustomRepoButtonTypeContent && len(btn.Link) == 0 { + if btn.Type != CustomRepoButtonTypeContent && len(btn.Link) == 0 { return false, nil } }