Merge pull request 'Allow users to hide all "Add more units..." hints' (#2533) from algernon/forgejo:less-is-more into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2533
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
Earl Warren 2024-03-24 05:42:37 +00:00
commit 0bfd4ca532
15 changed files with 233 additions and 24 deletions

View file

@ -50,6 +50,8 @@ var migrations = []*Migration{
NewMigration("create the forgejo_repo_flag table", forgejo_v1_22.CreateRepoFlagTable), NewMigration("create the forgejo_repo_flag table", forgejo_v1_22.CreateRepoFlagTable),
// v5 -> v6 // v5 -> v6
NewMigration("Add wiki_branch to repository", forgejo_v1_22.AddWikiBranchToRepository), NewMigration("Add wiki_branch to repository", forgejo_v1_22.AddWikiBranchToRepository),
// v6 -> v7
NewMigration("Add enable_repo_unit_hints to the user table", forgejo_v1_22.AddUserRepoUnitHintsSetting),
} }
// GetCurrentDBVersion returns the current Forgejo database version. // GetCurrentDBVersion returns the current Forgejo database version.

View file

@ -0,0 +1,17 @@
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_22 //nolint
import (
"xorm.io/xorm"
)
func AddUserRepoUnitHintsSetting(x *xorm.Engine) error {
type User struct {
ID int64
EnableRepoUnitHints bool `xorm:"NOT NULL DEFAULT true"`
}
return x.Sync(&User{})
}

View file

@ -146,6 +146,7 @@ type User struct {
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"` DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
Theme string `xorm:"NOT NULL DEFAULT ''"` Theme string `xorm:"NOT NULL DEFAULT ''"`
KeepActivityPrivate bool `xorm:"NOT NULL DEFAULT false"` KeepActivityPrivate bool `xorm:"NOT NULL DEFAULT false"`
EnableRepoUnitHints bool `xorm:"NOT NULL DEFAULT true"`
} }
func init() { func init() {

View file

@ -67,13 +67,14 @@ func (u User) MarshalJSON() ([]byte, error) {
// UserSettings represents user settings // UserSettings represents user settings
// swagger:model // swagger:model
type UserSettings struct { type UserSettings struct {
FullName string `json:"full_name"` FullName string `json:"full_name"`
Website string `json:"website"` Website string `json:"website"`
Description string `json:"description"` Description string `json:"description"`
Location string `json:"location"` Location string `json:"location"`
Language string `json:"language"` Language string `json:"language"`
Theme string `json:"theme"` Theme string `json:"theme"`
DiffViewStyle string `json:"diff_view_style"` DiffViewStyle string `json:"diff_view_style"`
EnableRepoUnitHints bool `json:"enable_repo_unit_hints"`
// Privacy // Privacy
HideEmail bool `json:"hide_email"` HideEmail bool `json:"hide_email"`
HideActivity bool `json:"hide_activity"` HideActivity bool `json:"hide_activity"`
@ -82,13 +83,14 @@ type UserSettings struct {
// UserSettingsOptions represents options to change user settings // UserSettingsOptions represents options to change user settings
// swagger:model // swagger:model
type UserSettingsOptions struct { type UserSettingsOptions struct {
FullName *string `json:"full_name" binding:"MaxSize(100)"` FullName *string `json:"full_name" binding:"MaxSize(100)"`
Website *string `json:"website" binding:"OmitEmpty;ValidUrl;MaxSize(255)"` Website *string `json:"website" binding:"OmitEmpty;ValidUrl;MaxSize(255)"`
Description *string `json:"description" binding:"MaxSize(255)"` Description *string `json:"description" binding:"MaxSize(255)"`
Location *string `json:"location" binding:"MaxSize(50)"` Location *string `json:"location" binding:"MaxSize(50)"`
Language *string `json:"language"` Language *string `json:"language"`
Theme *string `json:"theme"` Theme *string `json:"theme"`
DiffViewStyle *string `json:"diff_view_style"` DiffViewStyle *string `json:"diff_view_style"`
EnableRepoUnitHints *bool `json:"enable_repo_unit_hints"`
// Privacy // Privacy
HideEmail *bool `json:"hide_email"` HideEmail *bool `json:"hide_email"`
HideActivity *bool `json:"hide_activity"` HideActivity *bool `json:"hide_activity"`

View file

@ -711,6 +711,11 @@ continue = Continue
cancel = Cancel cancel = Cancel
language = Language language = Language
ui = Theme ui = Theme
hints = Hints
additional_repo_units_hint_description = Display an "Add more units..." button for repositories that do not have all available units enabled.
additional_repo_units_hint = Encourage enabling additional repository units
update_hints = Update hints
update_hints_success = Hints have been updated.
hidden_comment_types = Hidden comment types hidden_comment_types = Hidden comment types
hidden_comment_types_description = Comment types checked here will not be shown inside issue pages. Checking "Label" for example removes all "<user> added/removed <label>" comments. hidden_comment_types_description = Comment types checked here will not be shown inside issue pages. Checking "Label" for example removes all "<user> added/removed <label>" comments.
hidden_comment_types.ref_tooltip = Comments where this issue was referenced from another issue/commit/… hidden_comment_types.ref_tooltip = Comments where this issue was referenced from another issue/commit/…

View file

@ -55,6 +55,7 @@ func UpdateUserSettings(ctx *context.APIContext) {
DiffViewStyle: optional.FromPtr(form.DiffViewStyle), DiffViewStyle: optional.FromPtr(form.DiffViewStyle),
KeepEmailPrivate: optional.FromPtr(form.HideEmail), KeepEmailPrivate: optional.FromPtr(form.HideEmail),
KeepActivityPrivate: optional.FromPtr(form.HideActivity), KeepActivityPrivate: optional.FromPtr(form.HideActivity),
EnableRepoUnitHints: optional.FromPtr(form.EnableRepoUnitHints),
} }
if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil { if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
ctx.InternalServerError(err) ctx.InternalServerError(err)

View file

@ -393,6 +393,25 @@ func UpdateUserLang(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance") ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
} }
// UpdateUserHints updates a user's hints settings
func UpdateUserHints(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.UpdateHintsForm)
ctx.Data["Title"] = ctx.Tr("settings")
ctx.Data["PageIsSettingsAppearance"] = true
opts := &user_service.UpdateOptions{
EnableRepoUnitHints: optional.Some(form.EnableRepoUnitHints),
}
if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
ctx.ServerError("UpdateUser", err)
return
}
log.Trace("User settings updated: %s", ctx.Doer.Name)
ctx.Flash.Success(translation.NewLocale(ctx.Doer.Language).TrString("settings.update_hints_success"))
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
}
// UpdateUserHiddenComments update a user's shown comment types // UpdateUserHiddenComments update a user's shown comment types
func UpdateUserHiddenComments(ctx *context.Context) { func UpdateUserHiddenComments(ctx *context.Context) {
err := user_model.SetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes, forms.UserHiddenCommentTypesFromRequest(ctx).String()) err := user_model.SetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes, forms.UserHiddenCommentTypesFromRequest(ctx).String())

View file

@ -568,6 +568,7 @@ func registerRoutes(m *web.Route) {
m.Group("/appearance", func() { m.Group("/appearance", func() {
m.Get("", user_setting.Appearance) m.Get("", user_setting.Appearance)
m.Post("/language", web.Bind(forms.UpdateLanguageForm{}), user_setting.UpdateUserLang) m.Post("/language", web.Bind(forms.UpdateLanguageForm{}), user_setting.UpdateUserLang)
m.Post("/hints", web.Bind(forms.UpdateHintsForm{}), user_setting.UpdateUserHints)
m.Post("/hidden_comments", user_setting.UpdateUserHiddenComments) m.Post("/hidden_comments", user_setting.UpdateUserHiddenComments)
m.Post("/theme", web.Bind(forms.UpdateThemeForm{}), user_setting.UpdateUIThemePost) m.Post("/theme", web.Bind(forms.UpdateThemeForm{}), user_setting.UpdateUIThemePost)
}) })

View file

@ -86,15 +86,16 @@ func toUser(ctx context.Context, user *user_model.User, signed, authed bool) *ap
// User2UserSettings return UserSettings based on a user // User2UserSettings return UserSettings based on a user
func User2UserSettings(user *user_model.User) api.UserSettings { func User2UserSettings(user *user_model.User) api.UserSettings {
return api.UserSettings{ return api.UserSettings{
FullName: user.FullName, FullName: user.FullName,
Website: user.Website, Website: user.Website,
Location: user.Location, Location: user.Location,
Language: user.Language, Language: user.Language,
Description: user.Description, Description: user.Description,
Theme: user.Theme, Theme: user.Theme,
HideEmail: user.KeepEmailPrivate, HideEmail: user.KeepEmailPrivate,
HideActivity: user.KeepActivityPrivate, HideActivity: user.KeepActivityPrivate,
DiffViewStyle: user.DiffViewStyle, DiffViewStyle: user.DiffViewStyle,
EnableRepoUnitHints: user.EnableRepoUnitHints,
} }
} }

View file

@ -234,6 +234,11 @@ type UpdateLanguageForm struct {
Language string Language string
} }
// UpdateHintsForm form for updating user hint settings
type UpdateHintsForm struct {
EnableRepoUnitHints bool
}
// Validate validates the fields // Validate validates the fields
func (f *UpdateLanguageForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { func (f *UpdateLanguageForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req) ctx := context.GetValidateContext(req)

View file

@ -37,6 +37,7 @@ type UpdateOptions struct {
EmailNotificationsPreference optional.Option[string] EmailNotificationsPreference optional.Option[string]
SetLastLogin bool SetLastLogin bool
RepoAdminChangeTeamAccess optional.Option[bool] RepoAdminChangeTeamAccess optional.Option[bool]
EnableRepoUnitHints optional.Option[bool]
} }
func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) error { func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) error {
@ -83,6 +84,11 @@ func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) er
cols = append(cols, "diff_view_style") cols = append(cols, "diff_view_style")
} }
if opts.EnableRepoUnitHints.Has() {
u.EnableRepoUnitHints = opts.EnableRepoUnitHints.Value()
cols = append(cols, "enable_repo_unit_hints")
}
if opts.AllowGitHook.Has() { if opts.AllowGitHook.Has() {
u.AllowGitHook = opts.AllowGitHook.Value() u.AllowGitHook = opts.AllowGitHook.Value()

View file

@ -172,7 +172,7 @@
{{end}} {{end}}
{{if .Permission.IsAdmin}} {{if .Permission.IsAdmin}}
{{if not (.Repository.AllUnitsEnabled ctx)}} {{if and .SignedUser.EnableRepoUnitHints (not (.Repository.AllUnitsEnabled ctx))}}
<a class="{{if .PageIsRepoSettingsUnits}}active {{end}}item" href="{{.RepoLink}}/settings/units"> <a class="{{if .PageIsRepoSettingsUnits}}active {{end}}item" href="{{.RepoLink}}/settings/units">
{{svg "octicon-diff-added"}} {{ctx.Locale.Tr "repo.settings.units.add_more"}} {{svg "octicon-diff-added"}} {{ctx.Locale.Tr "repo.settings.units.add_more"}}
</a> </a>

View file

@ -23853,6 +23853,10 @@
"type": "string", "type": "string",
"x-go-name": "DiffViewStyle" "x-go-name": "DiffViewStyle"
}, },
"enable_repo_unit_hints": {
"type": "boolean",
"x-go-name": "EnableRepoUnitHints"
},
"full_name": { "full_name": {
"type": "string", "type": "string",
"x-go-name": "FullName" "x-go-name": "FullName"
@ -23897,6 +23901,10 @@
"type": "string", "type": "string",
"x-go-name": "DiffViewStyle" "x-go-name": "DiffViewStyle"
}, },
"enable_repo_unit_hints": {
"type": "boolean",
"x-go-name": "EnableRepoUnitHints"
},
"full_name": { "full_name": {
"type": "string", "type": "string",
"x-go-name": "FullName" "x-go-name": "FullName"

View file

@ -66,6 +66,25 @@
</form> </form>
</div> </div>
<!-- Hints -->
<h4 class="ui top attached header">
{{ctx.Locale.Tr "settings.hints"}}
</h4>
<div class="ui attached segment">
<form class="ui form" action="{{.Link}}/hints" method="post">
{{.CsrfTokenHtml}}
<div class="inline field">
<div class="ui checkbox" data-tooltip-content="{{ctx.Locale.Tr "settings.additional_repo_units_hint_description"}}">
<input name="enable_repo_unit_hints" type="checkbox" {{if $.SignedUser.EnableRepoUnitHints}}checked{{end}}>
<label>{{ctx.Locale.Tr "settings.additional_repo_units_hint"}}</label>
</div>
</div>
<div class="field">
<button class="ui primary button">{{ctx.Locale.Tr "settings.update_hints"}}</button>
</div>
</form>
</div>
<!-- Shown comment event types --> <!-- Shown comment event types -->
<h4 class="ui top attached header"> <h4 class="ui top attached header">
{{ctx.Locale.Tr "settings.hidden_comment_types"}} {{ctx.Locale.Tr "settings.hidden_comment_types"}}

View file

@ -4,12 +4,14 @@
package integration package integration
import ( import (
"fmt"
"net/http" "net/http"
"testing" "testing"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
@ -306,3 +308,123 @@ func TestUserLocationMapLink(t *testing.T) {
htmlDoc := NewHTMLParser(t, resp.Body) htmlDoc := NewHTMLParser(t, resp.Body)
htmlDoc.AssertElement(t, `a[href="https://example/foo/A%2Fb"]`, true) htmlDoc.AssertElement(t, `a[href="https://example/foo/A%2Fb"]`, true)
} }
func TestUserHints(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser)
// Create a known-good repo, with only one unit enabled
repo, _, f := CreateDeclarativeRepo(t, user, "", []unit_model.Type{
unit_model.TypeCode,
}, []unit_model.Type{
unit_model.TypePullRequests,
unit_model.TypeProjects,
unit_model.TypePackages,
unit_model.TypeActions,
unit_model.TypeIssues,
unit_model.TypeWiki,
}, nil)
defer f()
ensureRepoUnitHints := func(t *testing.T, hints bool) {
t.Helper()
req := NewRequestWithJSON(t, "PATCH", "/api/v1/user/settings", &api.UserSettingsOptions{
EnableRepoUnitHints: &hints,
}).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var userSettings api.UserSettings
DecodeJSON(t, resp, &userSettings)
assert.Equal(t, hints, userSettings.EnableRepoUnitHints)
}
t.Run("API", func(t *testing.T) {
t.Run("setting hints on and off", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
ensureRepoUnitHints(t, true)
ensureRepoUnitHints(t, false)
})
t.Run("retrieving settings", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
for _, v := range []bool{true, false} {
ensureRepoUnitHints(t, v)
req := NewRequest(t, "GET", "/api/v1/user/settings").AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var userSettings api.UserSettings
DecodeJSON(t, resp, &userSettings)
assert.Equal(t, v, userSettings.EnableRepoUnitHints)
}
})
})
t.Run("user settings", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
// Set a known-good state, that isn't the default
ensureRepoUnitHints(t, false)
assertHintState := func(t *testing.T, enabled bool) {
t.Helper()
req := NewRequest(t, "GET", "/user/settings/appearance")
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
_, hintChecked := htmlDoc.Find(`input[name="enable_repo_unit_hints"]`).Attr("checked")
assert.Equal(t, enabled, hintChecked)
}
t.Run("view", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
assertHintState(t, false)
})
t.Run("change", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequestWithValues(t, "POST", "/user/settings/appearance/hints", map[string]string{
"_csrf": GetCSRF(t, session, "/user/settings/appearance"),
"enable_repo_unit_hints": "true",
})
session.MakeRequest(t, req, http.StatusSeeOther)
assertHintState(t, true)
})
})
t.Run("repo view", func(t *testing.T) {
assertAddMore := func(t *testing.T, present bool) {
t.Helper()
req := NewRequest(t, "GET", repo.Link())
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
htmlDoc.AssertElement(t, fmt.Sprintf("a[href='%s/settings/units']", repo.Link()), present)
}
t.Run("hints enabled", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
ensureRepoUnitHints(t, true)
assertAddMore(t, true)
})
t.Run("hints disabled", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
ensureRepoUnitHints(t, false)
assertAddMore(t, false)
})
})
}