fix(ui): make limits clearer in create repo form (#7402)

Resolves: #7341

Previously, the Create Repository button was only enabled if a user was able to create a repo in their own namespace. However, if they had reached the global repo limit, but were stlll able to create a repo in an org, the button would still be disabled.

In this pull request, the create repo form now:

1. Behaves like it always did previously if the user has not reached the repo limit.

2. If the User has reached the repo limit, and they are unable to create a repo in any of their orgs (or they have no orgs), the create repo form is displayed as:
![Screenshot](/attachments/9f22f43d-0036-4c48-b794-54302c0f241c)

3. If the User has reached the repo limit, and the **limit is greater than zero**, an alert appears at the top of the form, and they are only allowed to choose from the orgs that they are allowed to create repos in:
![Screenshot](/attachments/f5508e05-74fd-4858-9e95-967bd7017abd)

4. If the User has reached the repo limit, and the **limit is equal to zero**, no alert is displayed, as no user can create repos on that instance, and they are only allowed to choose from the orgs that they are allowed to create repos in:
![localhost_3000_repo_create (4).png](/attachments/e7a87da8-c19c-47e1-845e-2afc3667ab02)

Co-authored-by: 0ko <0ko@noreply.codeberg.org>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/7402
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Ryan Lerch <rlerch@redhat.com>
Co-committed-by: Ryan Lerch <rlerch@redhat.com>
This commit is contained in:
Ryan Lerch 2025-05-20 16:37:15 +02:00 committed by 0ko
parent 43fee56011
commit a0f902f635
5 changed files with 116 additions and 42 deletions

View file

@ -93,7 +93,7 @@
login_name: org3 login_name: org3
type: 1 type: 1
salt: ZogKvWdyEx salt: ZogKvWdyEx
max_repo_creation: -1 max_repo_creation: 1000
is_active: false is_active: false
is_admin: false is_admin: false
is_restricted: false is_restricted: false

View file

@ -46,6 +46,7 @@
"one": "wants to merge %[1]d commit from <code>%[2]s</code> into <code id=\"%[4]s\">%[3]s</code>", "one": "wants to merge %[1]d commit from <code>%[2]s</code> into <code id=\"%[4]s\">%[3]s</code>",
"other": "wants to merge %[1]d commits from <code>%[2]s</code> into <code id=\"%[4]s\">%[3]s</code>" "other": "wants to merge %[1]d commits from <code>%[2]s</code> into <code id=\"%[4]s\">%[3]s</code>"
}, },
"repo.form.cannot_create": "All spaces in which you can create repositories have reached the limit of repositories.",
"search.milestone_kind": "Search milestones…", "search.milestone_kind": "Search milestones…",
"incorrect_root_url": "This Forgejo instance is configured to be served on \"%s\". You are currently viewing Forgejo through a different URL, which may cause parts of the application to break. The canonical URL is controlled by Forgejo admins via the ROOT_URL setting in the app.ini.", "incorrect_root_url": "This Forgejo instance is configured to be served on \"%s\". You are currently viewing Forgejo through a different URL, which may cause parts of the application to break. The canonical URL is controlled by Forgejo admins via the ROOT_URL setting in the app.ini.",
"themes.names.forgejo-auto": "Forgejo (follow system theme)", "themes.names.forgejo-auto": "Forgejo (follow system theme)",

View file

@ -8,10 +8,11 @@
{{ctx.Locale.Tr "new_repo.title"}} {{ctx.Locale.Tr "new_repo.title"}}
</h3> </h3>
<div class="ui attached segment"> <div class="ui attached segment">
{{if or .CanCreateRepo .Orgs}}
{{template "base/alert" .}} {{template "base/alert" .}}
{{template "repo/create_helper" .}} {{template "repo/create_helper" .}}
{{if not .CanCreateRepo}} {{if and (not .CanCreateRepo) (ne .MaxCreationLimit 0)}}
<div class="ui negative message"> <div class="ui negative message">
<p>{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}</p> <p>{{ctx.Locale.TrN .MaxCreationLimit "repo.form.reach_limit_of_creation_1" "repo.form.reach_limit_of_creation_n" .MaxCreationLimit}}</p>
</div> </div>
@ -41,9 +42,14 @@
</details> </details>
</fieldset> </fieldset>
</div> </div>
<button class="ui primary button{{if not .CanCreateRepo}} disabled{{end}}"> <button class="ui primary button">
{{ctx.Locale.Tr "repo.create_repo"}} {{ctx.Locale.Tr "repo.create_repo"}}
</button> </button>
{{else}}
<div class="ui negative message">
{{ctx.Locale.Tr "repo.form.cannot_create"}}
</div>
{{end}}
</div> </div>
</form> </form>
</div> </div>

View file

@ -2,17 +2,27 @@
{{ctx.Locale.Tr "repo.owner"}} {{ctx.Locale.Tr "repo.owner"}}
<div class="ui selection required dropdown" aria-labelledby="repo_owner_label"> <div class="ui selection required dropdown" aria-labelledby="repo_owner_label">
{{/* uid id is used by the repo-template code */}} {{/* uid id is used by the repo-template code */}}
{{if .CanCreateRepo}}
<input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required> <input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required>
<span class="text truncated-item-container" title="{{.ContextUser.Name}}"> <span class="text truncated-item-container" title="{{.ContextUser.Name}}">
{{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}} {{ctx.AvatarUtils.Avatar .ContextUser 28 "mini"}}
<span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span> <span class="truncated-item-name">{{.ContextUser.ShortName 40}}</span>
</span> </span>
{{else if .Orgs}}
<input type="hidden" id="uid" name="uid" value="{{(index .Orgs 0).ID}}" required>
<span class="text truncated-item-container" title="{{(index .Orgs 0).Name}}">
{{ctx.AvatarUtils.Avatar (index .Orgs 0) 28 "mini"}}
<span class="truncated-item-name">{{(index .Orgs 0).ShortName 40}}</span>
</span>
{{end}}
{{svg "octicon-triangle-down" 14 "dropdown icon"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu"> <div class="menu">
{{if .CanCreateRepo}}
<div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}"> <div class="item truncated-item-container" data-value="{{.SignedUser.ID}}" title="{{.SignedUser.Name}}">
{{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}} {{ctx.AvatarUtils.Avatar .SignedUser 28 "mini"}}
<span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span> <span class="truncated-item-name">{{.SignedUser.ShortName 40}}</span>
</div> </div>
{{end}}
{{range .Orgs}} {{range .Orgs}}
<div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}"> <div class="item truncated-item-container" data-value="{{.ID}}" title="{{.Name}}">
{{ctx.AvatarUtils.Avatar . 28 "mini"}} {{ctx.AvatarUtils.Avatar . 28 "mini"}}

View file

@ -126,6 +126,63 @@ func TestRepoCreateForm(t *testing.T) {
assertRepoCreateForm(t, htmlDoc, user, "") assertRepoCreateForm(t, htmlDoc, user, "")
} }
func TestRepoCreateFormRepoLimit(t *testing.T) {
defer tests.PrepareTestEnv(t)()
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
userName := "user2"
session := loginUser(t, userName)
locale := translation.NewLocale("en-US")
cannotCreateTr := locale.Tr("repo.form.cannot_create")
// Test the case where a user has hit the global max creation limit, but can still create
// a repo in an organization. Because the limit is greater than 0 we also show an alert
// to tell the user they have hit the limit.
t.Run("Limit above zero", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
maxCreationLimit := 1
creationLimitTr := locale.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
defer test.MockVariableValue(&setting.Repository.MaxCreationLimit, maxCreationLimit)()
resp := session.MakeRequest(t, NewRequest(t, "GET", "/repo/create"), http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
assertRepoCreateForm(t, htmlDoc, org, "")
alert := htmlDoc.doc.Find("div.ui.negative.message").Text()
assert.Contains(t, alert, creationLimitTr)
})
// Test the case where a user has hit the global max creation limit, but can still create
// a repo in an organization. Because the limit is 0 we DO NOT show the alert.
t.Run("Limit is zero", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
maxCreationLimit := 0
defer test.MockVariableValue(&setting.Repository.MaxCreationLimit, maxCreationLimit)()
resp := session.MakeRequest(t, NewRequest(t, "GET", "/repo/create"), http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
assertRepoCreateForm(t, htmlDoc, org, "")
htmlDoc.AssertElement(t, "div.ui.negative.message", false)
})
// Test the case where a user has hit the global max creation limit, and also cannot create
// a repo in any of their orgs. The form isnt shown, and we deisplay an alert telling the user
// they can't create a repo.
t.Run("Global limit", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
maxCreationLimit := 0
defer test.MockVariableValue(&setting.Repository.MaxCreationLimit, maxCreationLimit)()
session := loginUser(t, "user8")
resp := session.MakeRequest(t, NewRequest(t, "GET", "/repo/create"), http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
alert := htmlDoc.doc.Find("div.ui.negative.message").Text()
assert.Contains(t, alert, cannotCreateTr)
})
}
func TestRepoGenerate(t *testing.T) { func TestRepoGenerate(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
userName := "user1" userName := "user1"