mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-30 04:27:55 +01:00
Merge pull request '[UI] Fix HTMX support for profile card' (#4538) from gusted/htmx-support into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/4538 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
commit
3c8cd43fec
6 changed files with 86 additions and 41 deletions
|
@ -341,7 +341,6 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb
|
||||||
// Action response for follow/unfollow user request
|
// Action response for follow/unfollow user request
|
||||||
func Action(ctx *context.Context) {
|
func Action(ctx *context.Context) {
|
||||||
var err error
|
var err error
|
||||||
var redirectViaJSON bool
|
|
||||||
action := ctx.FormString("action")
|
action := ctx.FormString("action")
|
||||||
|
|
||||||
if ctx.ContextUser.IsOrganization() && (action == "block" || action == "unblock") {
|
if ctx.ContextUser.IsOrganization() && (action == "block" || action == "unblock") {
|
||||||
|
@ -357,10 +356,8 @@ func Action(ctx *context.Context) {
|
||||||
err = user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
err = user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
||||||
case "block":
|
case "block":
|
||||||
err = user_service.BlockUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
err = user_service.BlockUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
||||||
redirectViaJSON = true
|
|
||||||
case "unblock":
|
case "unblock":
|
||||||
err = user_model.UnblockUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
err = user_model.UnblockUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
||||||
redirectViaJSON = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -371,21 +368,15 @@ func Action(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.ContextUser.IsOrganization() {
|
if ctx.ContextUser.IsOrganization() {
|
||||||
ctx.Flash.Error(ctx.Tr("org.follow_blocked_user"))
|
ctx.Flash.Error(ctx.Tr("org.follow_blocked_user"), true)
|
||||||
} else {
|
} else {
|
||||||
ctx.Flash.Error(ctx.Tr("user.follow_blocked_user"))
|
ctx.Flash.Error(ctx.Tr("user.follow_blocked_user"), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if redirectViaJSON {
|
|
||||||
ctx.JSON(http.StatusOK, map[string]any{
|
|
||||||
"redirect": ctx.ContextUser.HomeLink(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.ContextUser.IsIndividual() {
|
if ctx.ContextUser.IsIndividual() {
|
||||||
shared_user.PrepareContextForProfileBigAvatar(ctx)
|
shared_user.PrepareContextForProfileBigAvatar(ctx)
|
||||||
|
ctx.Data["IsHTMX"] = true
|
||||||
ctx.HTML(http.StatusOK, tplProfileBigAvatar)
|
ctx.HTML(http.StatusOK, tplProfileBigAvatar)
|
||||||
return
|
return
|
||||||
} else if ctx.ContextUser.IsOrganization() {
|
} else if ctx.ContextUser.IsOrganization() {
|
||||||
|
|
|
@ -1,20 +1,23 @@
|
||||||
{{if .Flash.ErrorMsg}}
|
{{if .Flash.ErrorMsg}}
|
||||||
<div class="ui negative message flash-message flash-error">
|
<div id="flash-message" class="ui negative message flash-message flash-error" hx-swap-oob="true">
|
||||||
<p>{{.Flash.ErrorMsg | SanitizeHTML}}</p>
|
<p>{{.Flash.ErrorMsg | SanitizeHTML}}</p>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .Flash.SuccessMsg}}
|
{{if .Flash.SuccessMsg}}
|
||||||
<div class="ui positive message flash-message flash-success">
|
<div id="flash-message" class="ui positive message flash-message flash-success" hx-swap-oob="true">
|
||||||
<p>{{.Flash.SuccessMsg | SanitizeHTML}}</p>
|
<p>{{.Flash.SuccessMsg | SanitizeHTML}}</p>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .Flash.InfoMsg}}
|
{{if .Flash.InfoMsg}}
|
||||||
<div class="ui info message flash-message flash-info">
|
<div id="flash-message" class="ui info message flash-message flash-info" hx-swap-oob="true">
|
||||||
<p>{{.Flash.InfoMsg | SanitizeHTML}}</p>
|
<p>{{.Flash.InfoMsg | SanitizeHTML}}</p>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if .Flash.WarningMsg}}
|
{{if .Flash.WarningMsg}}
|
||||||
<div class="ui warning message flash-message flash-warning">
|
<div id="flash-message" class="ui warning message flash-message flash-warning" hx-swap-oob="true">
|
||||||
<p>{{.Flash.WarningMsg | SanitizeHTML}}</p>
|
<p>{{.Flash.WarningMsg | SanitizeHTML}}</p>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{if and (not .Flash.ErrorMsg) (not .Flash.SuccessMsg) (not .Flash.InfoMsg) (not .Flash.WarningMsg) (not .IsHTMX)}}
|
||||||
|
<div id="flash-message" hx-swap-oob="true"></div>
|
||||||
|
{{end}}
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
<div id="profile-avatar-card" class="ui card">
|
{{if .IsHTMX}}
|
||||||
|
{{template "base/alert" .}}
|
||||||
|
{{end}}
|
||||||
|
<div id="profile-avatar-card" class="ui card" hx-swap="morph">
|
||||||
<div id="profile-avatar" class="content tw-flex">
|
<div id="profile-avatar" class="content tw-flex">
|
||||||
{{if eq .SignedUserID .ContextUser.ID}}
|
{{if eq .SignedUserID .ContextUser.ID}}
|
||||||
<a class="image" href="{{AppSubUrl}}/user/settings" data-tooltip-content="{{ctx.Locale.Tr "user.change_avatar"}}">
|
<a class="image" href="{{AppSubUrl}}/user/settings" data-tooltip-content="{{ctx.Locale.Tr "user.change_avatar"}}">
|
||||||
|
@ -109,14 +112,13 @@
|
||||||
</button>
|
</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
</li>
|
</li>
|
||||||
<li class="block">
|
<li class="block" hx-target="#profile-avatar-card" hx-indicator="#profile-avatar-card">
|
||||||
{{if $.IsBlocked}}
|
{{if $.IsBlocked}}
|
||||||
<button class="ui basic red button link-action" data-url="{{.ContextUser.HomeLink}}?action=unblock&redirect_to={{$.Link}}">
|
<button class="ui basic red button" hx-post="{{.ContextUser.HomeLink}}?action=unblock">
|
||||||
{{svg "octicon-person"}} {{ctx.Locale.Tr "user.unblock"}}
|
{{svg "octicon-person"}} {{ctx.Locale.Tr "user.unblock"}}
|
||||||
</button>
|
</button>
|
||||||
{{else}}
|
{{else}}
|
||||||
<button type="submit" class="ui basic orange button delete-button"
|
<button type="submit" class="ui basic orange button" data-modal-id="block-user" hx-post="{{.ContextUser.HomeLink}}?action=block" hx-confirm="-">
|
||||||
data-modal-id="block-user" data-url="{{.ContextUser.HomeLink}}?action=block">
|
|
||||||
{{svg "octicon-blocked"}} {{ctx.Locale.Tr "user.block"}}
|
{{svg "octicon-blocked"}} {{ctx.Locale.Tr "user.block"}}
|
||||||
</button>
|
</button>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
41
tests/e2e/profile_actions.test.e2e.js
Normal file
41
tests/e2e/profile_actions.test.e2e.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
// @ts-check
|
||||||
|
import {test, expect} from '@playwright/test';
|
||||||
|
import {login_user, load_logged_in_context} from './utils_e2e.js';
|
||||||
|
|
||||||
|
test('Follow actions', async ({browser}, workerInfo) => {
|
||||||
|
await login_user(browser, workerInfo, 'user2');
|
||||||
|
const context = await load_logged_in_context(browser, workerInfo, 'user2');
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
await page.goto('/user1');
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
// Check if following and then unfollowing works.
|
||||||
|
// This checks that the event listeners of
|
||||||
|
// the buttons aren't dissapearing.
|
||||||
|
const followButton = page.locator('.follow');
|
||||||
|
await expect(followButton).toContainText('Follow');
|
||||||
|
await followButton.click();
|
||||||
|
await expect(followButton).toContainText('Unfollow');
|
||||||
|
await followButton.click();
|
||||||
|
await expect(followButton).toContainText('Follow');
|
||||||
|
|
||||||
|
// Simple block interaction.
|
||||||
|
await expect(page.locator('.block')).toContainText('Block');
|
||||||
|
|
||||||
|
await page.locator('.block').click();
|
||||||
|
await expect(page.locator('#block-user')).toBeVisible();
|
||||||
|
await page.locator('#block-user .ok').click();
|
||||||
|
await expect(page.locator('.block')).toContainText('Unblock');
|
||||||
|
await expect(page.locator('#block-user')).not.toBeVisible();
|
||||||
|
|
||||||
|
// Check that following the user yields in a error being shown.
|
||||||
|
await followButton.click();
|
||||||
|
const flashMessage = page.locator('#flash-message');
|
||||||
|
await expect(flashMessage).toBeVisible();
|
||||||
|
await expect(flashMessage).toContainText('You cannot follow this user because you have blocked this user or this user has blocked you.');
|
||||||
|
|
||||||
|
// Unblock interaction.
|
||||||
|
await page.locator('.block').click();
|
||||||
|
await expect(page.locator('.block')).toContainText('Block');
|
||||||
|
});
|
|
@ -34,15 +34,8 @@ func BlockUser(t *testing.T, doer, blockedUser *user_model.User) {
|
||||||
"_csrf": GetCSRF(t, session, "/"+blockedUser.Name),
|
"_csrf": GetCSRF(t, session, "/"+blockedUser.Name),
|
||||||
"action": "block",
|
"action": "block",
|
||||||
})
|
})
|
||||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
type redirect struct {
|
|
||||||
Redirect string `json:"redirect"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var respBody redirect
|
|
||||||
DecodeJSON(t, resp, &respBody)
|
|
||||||
assert.EqualValues(t, "/"+blockedUser.Name, respBody.Redirect)
|
|
||||||
assert.True(t, unittest.BeanExists(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: doer.ID}))
|
assert.True(t, unittest.BeanExists(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: doer.ID}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -303,11 +296,10 @@ func TestBlockActions(t *testing.T) {
|
||||||
"_csrf": GetCSRF(t, session, "/"+blockedUser.Name),
|
"_csrf": GetCSRF(t, session, "/"+blockedUser.Name),
|
||||||
"action": "follow",
|
"action": "follow",
|
||||||
})
|
})
|
||||||
session.MakeRequest(t, req, http.StatusOK)
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
flashCookie := session.GetCookie(forgejo_context.CookieNameFlash)
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
assert.NotNil(t, flashCookie)
|
assert.Contains(t, htmlDoc.Find("#flash-message").Text(), "You cannot follow this user because you have blocked this user or this user has blocked you.")
|
||||||
assert.EqualValues(t, "error%3DYou%2Bcannot%2Bfollow%2Bthis%2Buser%2Bbecause%2Byou%2Bhave%2Bblocked%2Bthis%2Buser%2Bor%2Bthis%2Buser%2Bhas%2Bblocked%2Byou.", flashCookie.Value)
|
|
||||||
|
|
||||||
// Assert it still doesn't exist.
|
// Assert it still doesn't exist.
|
||||||
unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: doer.ID, FollowID: blockedUser.ID})
|
unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: doer.ID, FollowID: blockedUser.ID})
|
||||||
|
@ -323,11 +315,10 @@ func TestBlockActions(t *testing.T) {
|
||||||
"_csrf": GetCSRF(t, session, "/"+doer.Name),
|
"_csrf": GetCSRF(t, session, "/"+doer.Name),
|
||||||
"action": "follow",
|
"action": "follow",
|
||||||
})
|
})
|
||||||
session.MakeRequest(t, req, http.StatusOK)
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
flashCookie := session.GetCookie(forgejo_context.CookieNameFlash)
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
assert.NotNil(t, flashCookie)
|
assert.Contains(t, htmlDoc.Find("#flash-message").Text(), "You cannot follow this user because you have blocked this user or this user has blocked you.")
|
||||||
assert.EqualValues(t, "error%3DYou%2Bcannot%2Bfollow%2Bthis%2Buser%2Bbecause%2Byou%2Bhave%2Bblocked%2Bthis%2Buser%2Bor%2Bthis%2Buser%2Bhas%2Bblocked%2Byou.", flashCookie.Value)
|
|
||||||
|
|
||||||
unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: blockedUser.ID, FollowID: doer.ID})
|
unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: blockedUser.ID, FollowID: doer.ID})
|
||||||
})
|
})
|
||||||
|
|
|
@ -295,11 +295,11 @@ async function linkAction(e) {
|
||||||
export function initGlobalLinkActions() {
|
export function initGlobalLinkActions() {
|
||||||
function showDeletePopup(e) {
|
function showDeletePopup(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const $this = $(this);
|
const $this = $(this || e.target);
|
||||||
const dataArray = $this.data();
|
const dataArray = $this.data();
|
||||||
let filter = '';
|
let filter = '';
|
||||||
if (this.getAttribute('data-modal-id')) {
|
if ($this[0].getAttribute('data-modal-id')) {
|
||||||
filter += `#${this.getAttribute('data-modal-id')}`;
|
filter += `#${$this[0].getAttribute('data-modal-id')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const $dialog = $(`.delete.modal${filter}`);
|
const $dialog = $(`.delete.modal${filter}`);
|
||||||
|
@ -317,6 +317,10 @@ export function initGlobalLinkActions() {
|
||||||
$($this.data('form')).trigger('submit');
|
$($this.data('form')).trigger('submit');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if ($this[0].getAttribute('hx-confirm')) {
|
||||||
|
e.detail.issueRequest(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const postData = new FormData();
|
const postData = new FormData();
|
||||||
for (const [key, value] of Object.entries(dataArray)) {
|
for (const [key, value] of Object.entries(dataArray)) {
|
||||||
if (key && key.startsWith('data')) {
|
if (key && key.startsWith('data')) {
|
||||||
|
@ -338,6 +342,19 @@ export function initGlobalLinkActions() {
|
||||||
|
|
||||||
// Helpers.
|
// Helpers.
|
||||||
$('.delete-button').on('click', showDeletePopup);
|
$('.delete-button').on('click', showDeletePopup);
|
||||||
|
|
||||||
|
document.addEventListener('htmx:confirm', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// htmx:confirm is triggered for every HTMX request, even those that don't
|
||||||
|
// have the `hx-confirm` attribute specified. To avoid opening modals for
|
||||||
|
// those elements, check if 'e.detail.question' is empty, which contains the
|
||||||
|
// value of the `hx-confirm` attribute.
|
||||||
|
if (!e.detail.question) {
|
||||||
|
e.detail.issueRequest(true);
|
||||||
|
} else {
|
||||||
|
showDeletePopup(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initGlobalShowModal() {
|
function initGlobalShowModal() {
|
||||||
|
|
Loading…
Reference in a new issue