feat(ui): messages for empty usercards (#7947)
Show a message about list being empty, so the page doesn't look broken-ish empty. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/7947 Reviewed-by: Gusted <gusted@noreply.codeberg.org> Reviewed-by: Otto <otto@codeberg.org> Co-authored-by: 0ko <0ko@noreply.codeberg.org> Co-committed-by: 0ko <0ko@noreply.codeberg.org>
This commit is contained in:
		
					parent
					
						
							
								2b30c83a0c
							
						
					
				
			
			
				commit
				
					
						765e7bd1b6
					
				
			
		
					 6 changed files with 74 additions and 30 deletions
				
			
		| 
						 | 
				
			
			@ -4,6 +4,12 @@
 | 
			
		|||
    "home.explore_repos": "Explore repositories",
 | 
			
		||||
    "home.explore_users": "Explore users",
 | 
			
		||||
    "home.explore_orgs": "Explore organizations",
 | 
			
		||||
    "stars.list.none": "No one starred this repo.",
 | 
			
		||||
    "watch.list.none": "No one is watching this repo.",
 | 
			
		||||
    "followers.incoming.list.self.none": "No one is following your profile.",
 | 
			
		||||
    "followers.incoming.list.none": "No one is following this user.",
 | 
			
		||||
    "followers.outgoing.list.self.none": "You are not following anyone.",
 | 
			
		||||
    "followers.outgoing.list.none": "%s isn't following anyone.",
 | 
			
		||||
    "relativetime.now": "now",
 | 
			
		||||
    "relativetime.future": "in future",
 | 
			
		||||
    "relativetime.mins": {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
// Copyright 2017 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Copyright 2014 The Gogs Authors. All rights reserved.
 | 
			
		||||
// Copyright 2017 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Copyright 2023 The Forgejo Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package repo
 | 
			
		||||
| 
						 | 
				
			
			@ -1240,6 +1241,7 @@ func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOp
 | 
			
		|||
func Watchers(ctx *context.Context) {
 | 
			
		||||
	ctx.Data["Title"] = ctx.Tr("repo.watchers")
 | 
			
		||||
	ctx.Data["CardsTitle"] = ctx.Tr("repo.watchers")
 | 
			
		||||
	ctx.Data["CardsNoneMsg"] = ctx.Tr("watch.list.none")
 | 
			
		||||
	ctx.Data["PageIsWatchers"] = true
 | 
			
		||||
 | 
			
		||||
	RenderUserCards(ctx, ctx.Repo.Repository.NumWatches, func(opts db.ListOptions) ([]*user_model.User, error) {
 | 
			
		||||
| 
						 | 
				
			
			@ -1251,6 +1253,7 @@ func Watchers(ctx *context.Context) {
 | 
			
		|||
func Stars(ctx *context.Context) {
 | 
			
		||||
	ctx.Data["Title"] = ctx.Tr("repo.stargazers")
 | 
			
		||||
	ctx.Data["CardsTitle"] = ctx.Tr("repo.stargazers")
 | 
			
		||||
	ctx.Data["CardsNoneMsg"] = ctx.Tr("stars.list.none")
 | 
			
		||||
	ctx.Data["PageIsStargazers"] = true
 | 
			
		||||
	RenderUserCards(ctx, ctx.Repo.Repository.NumStars, func(opts db.ListOptions) ([]*user_model.User, error) {
 | 
			
		||||
		return repo_model.GetStargazers(ctx, ctx.Repo.Repository, opts)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,6 @@
 | 
			
		|||
// Copyright 2015 The Gogs Authors. All rights reserved.
 | 
			
		||||
// Copyright 2019 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Copyright 2023 The Forgejo Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package user
 | 
			
		||||
| 
						 | 
				
			
			@ -170,10 +171,20 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
 | 
			
		|||
		ctx.Data["Cards"] = followers
 | 
			
		||||
		total = int(numFollowers)
 | 
			
		||||
		ctx.Data["CardsTitle"] = ctx.TrN(total, "user.followers.title.one", "user.followers.title.few")
 | 
			
		||||
		if ctx.IsSigned && ctx.ContextUser.ID == ctx.Doer.ID {
 | 
			
		||||
			ctx.Data["CardsNoneMsg"] = ctx.Tr("followers.incoming.list.self.none")
 | 
			
		||||
		} else {
 | 
			
		||||
			ctx.Data["CardsNoneMsg"] = ctx.Tr("followers.incoming.list.none")
 | 
			
		||||
		}
 | 
			
		||||
	case "following":
 | 
			
		||||
		ctx.Data["Cards"] = following
 | 
			
		||||
		total = int(numFollowing)
 | 
			
		||||
		ctx.Data["CardsTitle"] = ctx.TrN(total, "user.following.title.one", "user.following.title.few")
 | 
			
		||||
		if ctx.IsSigned && ctx.ContextUser.ID == ctx.Doer.ID {
 | 
			
		||||
			ctx.Data["CardsNoneMsg"] = ctx.Tr("followers.outgoing.list.self.none")
 | 
			
		||||
		} else {
 | 
			
		||||
			ctx.Data["CardsNoneMsg"] = ctx.Tr("followers.outgoing.list.none", ctx.ContextUser.Name)
 | 
			
		||||
		}
 | 
			
		||||
	case "activity":
 | 
			
		||||
		date := ctx.FormString("date")
 | 
			
		||||
		pagingNum = setting.UI.FeedPagingNum
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,29 +4,33 @@
 | 
			
		|||
			{{.CardsTitle}}
 | 
			
		||||
		</h2>
 | 
			
		||||
	{{end}}
 | 
			
		||||
	<ul class="list">
 | 
			
		||||
		{{range .Cards}}
 | 
			
		||||
			<li class="card">
 | 
			
		||||
				<a href="{{.HomeLink}}">
 | 
			
		||||
					{{ctx.AvatarUtils.Avatar .}}
 | 
			
		||||
				</a>
 | 
			
		||||
				<div>
 | 
			
		||||
					<h3 class="name">
 | 
			
		||||
						<a href="{{.HomeLink}}">{{.DisplayName}}</a>
 | 
			
		||||
					</h3>
 | 
			
		||||
					<div class="meta">
 | 
			
		||||
						{{if .Website}}
 | 
			
		||||
							{{svg "octicon-link"}} <a href="{{.Website}}" target="_blank" rel="noopener noreferrer">{{.Website}}</a>
 | 
			
		||||
						{{else if .Location}}
 | 
			
		||||
							{{svg "octicon-location"}} {{.Location}}
 | 
			
		||||
						{{else}}
 | 
			
		||||
							{{svg "octicon-calendar"}} {{ctx.Locale.Tr "user.joined_on" (DateUtils.AbsoluteShort .CreatedUnix)}}
 | 
			
		||||
						{{end}}
 | 
			
		||||
	{{if .Cards}}
 | 
			
		||||
		<ul class="list">
 | 
			
		||||
			{{range .Cards}}
 | 
			
		||||
				<li class="card">
 | 
			
		||||
					<a href="{{.HomeLink}}">
 | 
			
		||||
						{{ctx.AvatarUtils.Avatar .}}
 | 
			
		||||
					</a>
 | 
			
		||||
					<div>
 | 
			
		||||
						<h3 class="name">
 | 
			
		||||
							<a href="{{.HomeLink}}">{{.DisplayName}}</a>
 | 
			
		||||
						</h3>
 | 
			
		||||
						<div class="meta">
 | 
			
		||||
							{{if .Website}}
 | 
			
		||||
								{{svg "octicon-link"}} <a href="{{.Website}}" target="_blank" rel="noopener noreferrer">{{.Website}}</a>
 | 
			
		||||
							{{else if .Location}}
 | 
			
		||||
								{{svg "octicon-location"}} {{.Location}}
 | 
			
		||||
							{{else}}
 | 
			
		||||
								{{svg "octicon-calendar"}} {{ctx.Locale.Tr "user.joined_on" (DateUtils.AbsoluteShort .CreatedUnix)}}
 | 
			
		||||
							{{end}}
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</li>
 | 
			
		||||
		{{end}}
 | 
			
		||||
	</ul>
 | 
			
		||||
				</li>
 | 
			
		||||
			{{end}}
 | 
			
		||||
		</ul>
 | 
			
		||||
	{{else if .CardsNoneMsg}}
 | 
			
		||||
		<div>{{.CardsNoneMsg}}</div>
 | 
			
		||||
	{{end}}
 | 
			
		||||
 | 
			
		||||
	{{template "base/paginate" .}}
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,7 +17,7 @@ import (
 | 
			
		|||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func testRepoStarringOrWatching(t *testing.T, action, listURI string) {
 | 
			
		||||
func testRepoStarringOrWatching(t *testing.T, action, listURI string, expectEmpty bool) {
 | 
			
		||||
	t.Helper()
 | 
			
		||||
 | 
			
		||||
	defer tests.PrepareTestEnv(t)()
 | 
			
		||||
| 
						 | 
				
			
			@ -50,6 +50,12 @@ func testRepoStarringOrWatching(t *testing.T, action, listURI string) {
 | 
			
		|||
	htmlDoc = NewHTMLParser(t, resp.Body)
 | 
			
		||||
	htmlDoc.AssertElement(t, ".user-cards .list .card > a[href='/user5']", true)
 | 
			
		||||
 | 
			
		||||
	if expectEmpty {
 | 
			
		||||
		// Verify which user-cards elements are present
 | 
			
		||||
		htmlDoc.AssertElement(t, ".user-cards > .list", true)
 | 
			
		||||
		htmlDoc.AssertElement(t, ".user-cards > div", false)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Unstar/unwatch the repo as user5
 | 
			
		||||
	req = NewRequestWithValues(t, "POST", fmt.Sprintf("/user2/repo1/action/%s", oppositeAction), map[string]string{
 | 
			
		||||
		"_csrf": GetCSRF(t, session, "/user2/repo1"),
 | 
			
		||||
| 
						 | 
				
			
			@ -73,15 +79,22 @@ func testRepoStarringOrWatching(t *testing.T, action, listURI string) {
 | 
			
		|||
 | 
			
		||||
	// Verify that "user5" is not among the stargazers/watchers
 | 
			
		||||
	htmlDoc = NewHTMLParser(t, resp.Body)
 | 
			
		||||
	htmlDoc.AssertElement(t, ".user-cards .list .item.ui.segment > a[href='/user5']", false)
 | 
			
		||||
	htmlDoc.AssertElement(t, ".user-cards .list .item.ui.segment > a[href='/user2']", false)
 | 
			
		||||
 | 
			
		||||
	if expectEmpty {
 | 
			
		||||
		// Verify which user-cards elements are present
 | 
			
		||||
		htmlDoc.AssertElement(t, ".user-cards > .list", false)
 | 
			
		||||
		htmlDoc.AssertElement(t, ".user-cards > div", true)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestRepoStarUnstarUI(t *testing.T) {
 | 
			
		||||
	testRepoStarringOrWatching(t, "star", "stars")
 | 
			
		||||
	testRepoStarringOrWatching(t, "star", "stars", true)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestRepoWatchUnwatchUI(t *testing.T) {
 | 
			
		||||
	testRepoStarringOrWatching(t, "watch", "watchers")
 | 
			
		||||
	testRepoStarringOrWatching(t, "watch", "watchers", false)
 | 
			
		||||
	// Empty list state is not checked because repo is watched by many users
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDisabledStars(t *testing.T) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
// Copyright 2024 The Forgejo Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
// SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
 | 
			
		||||
package integration
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -32,6 +32,7 @@ func TestUserProfileFollows(t *testing.T) {
 | 
			
		|||
	followingLink := "#profile-avatar-card a[href='/user1?tab=following']"
 | 
			
		||||
	listHeader := ".user-cards h2"
 | 
			
		||||
	listItems := ".user-cards .list"
 | 
			
		||||
	listMsg := ".user-cards > div"
 | 
			
		||||
 | 
			
		||||
	// = No follows =
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +45,8 @@ func TestUserProfileFollows(t *testing.T) {
 | 
			
		|||
	// Verify that user1 has no followers
 | 
			
		||||
	testSelectorEquals(t, page, followersLink, "0 followers")
 | 
			
		||||
	testSelectorEquals(t, page, listHeader, "Followers")
 | 
			
		||||
	testListCount(t, page, listItems, followCount)
 | 
			
		||||
	page.AssertElement(t, listItems, false)
 | 
			
		||||
	page.AssertElement(t, listMsg, true)
 | 
			
		||||
 | 
			
		||||
	// Request the profile of user1, the Following tab
 | 
			
		||||
	response = user1.MakeRequest(t, NewRequest(t, "GET", "/user1?tab=following"), http.StatusOK)
 | 
			
		||||
| 
						 | 
				
			
			@ -53,7 +55,8 @@ func TestUserProfileFollows(t *testing.T) {
 | 
			
		|||
	// Verify that user1 does not follow anyone
 | 
			
		||||
	testSelectorEquals(t, page, followingLink, "0 following")
 | 
			
		||||
	testSelectorEquals(t, page, listHeader, "Following")
 | 
			
		||||
	testListCount(t, page, listItems, followCount)
 | 
			
		||||
	page.AssertElement(t, listItems, false)
 | 
			
		||||
	page.AssertElement(t, listMsg, true)
 | 
			
		||||
 | 
			
		||||
	// Make user1 and user2 follow each other
 | 
			
		||||
	testUserFollowUser(t, user1, "user2")
 | 
			
		||||
| 
						 | 
				
			
			@ -71,6 +74,7 @@ func TestUserProfileFollows(t *testing.T) {
 | 
			
		|||
	testSelectorEquals(t, page, followersLink, "1 follower")
 | 
			
		||||
	testSelectorEquals(t, page, listHeader, "Follower")
 | 
			
		||||
	testListCount(t, page, listItems, followCount)
 | 
			
		||||
	page.AssertElement(t, listMsg, false)
 | 
			
		||||
 | 
			
		||||
	// Request the profile of user1, the Following tab
 | 
			
		||||
	response = user1.MakeRequest(t, NewRequest(t, "GET", "/user1?tab=following"), http.StatusOK)
 | 
			
		||||
| 
						 | 
				
			
			@ -80,6 +84,7 @@ func TestUserProfileFollows(t *testing.T) {
 | 
			
		|||
	testSelectorEquals(t, page, followingLink, "1 following")
 | 
			
		||||
	testSelectorEquals(t, page, listHeader, "Following")
 | 
			
		||||
	testListCount(t, page, listItems, followCount)
 | 
			
		||||
	page.AssertElement(t, listMsg, false)
 | 
			
		||||
 | 
			
		||||
	// Make user1 and user3 follow each other
 | 
			
		||||
	testUserFollowUser(t, user1, "user5")
 | 
			
		||||
| 
						 | 
				
			
			@ -97,6 +102,7 @@ func TestUserProfileFollows(t *testing.T) {
 | 
			
		|||
	testSelectorEquals(t, page, followersLink, "2 followers")
 | 
			
		||||
	testSelectorEquals(t, page, listHeader, "Followers")
 | 
			
		||||
	testListCount(t, page, listItems, followCount)
 | 
			
		||||
	page.AssertElement(t, listMsg, false)
 | 
			
		||||
 | 
			
		||||
	// Request the profile of user1, the Following tab
 | 
			
		||||
	response = user1.MakeRequest(t, NewRequest(t, "GET", "/user1?tab=following"), http.StatusOK)
 | 
			
		||||
| 
						 | 
				
			
			@ -106,6 +112,7 @@ func TestUserProfileFollows(t *testing.T) {
 | 
			
		|||
	testSelectorEquals(t, page, followingLink, "2 following")
 | 
			
		||||
	testSelectorEquals(t, page, listHeader, "Following")
 | 
			
		||||
	testListCount(t, page, listItems, followCount)
 | 
			
		||||
	page.AssertElement(t, listMsg, false)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// testUserFollowUser simply follows a user `following` by session of user `follower`
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue