From 3f25bd496b44f56df13f395f96d4e00ab43a23de Mon Sep 17 00:00:00 2001
From: hom3mad3 <8156337+hom3mad3@users.noreply.github.com>
Date: Thu, 6 Mar 2025 13:26:10 +0100
Subject: [PATCH] follows: refactor FollowButton.js
---
.../follows/static/follows/FollowButton.jsx | 80 +++++++++++++------
.../follows/__tests__/FollowButton.jest.jsx | 26 ++++++
.../follows/templatetags/react_follows.py | 9 ++-
changelog/_0005.md | 4 +
4 files changed, 94 insertions(+), 25 deletions(-)
create mode 100644 changelog/_0005.md
diff --git a/adhocracy4/follows/static/follows/FollowButton.jsx b/adhocracy4/follows/static/follows/FollowButton.jsx
index 5a6abaa1c..6134ecd3c 100644
--- a/adhocracy4/follows/static/follows/FollowButton.jsx
+++ b/adhocracy4/follows/static/follows/FollowButton.jsx
@@ -1,7 +1,7 @@
-import django from 'django'
import React, { useState, useEffect } from 'react'
+import ReactDOM from 'react-dom'
+import django from 'django'
import Alert from '../../../static/Alert'
-
import api from '../../../static/api'
import config from '../../../static/config'
@@ -18,27 +18,26 @@ const translated = {
following: django.gettext('Following')
}
-export const FollowButton = (props) => {
+export const FollowButton = ({
+ project,
+ authenticatedAs,
+ customClasses = '',
+ alertTarget = null
+}) => {
const [following, setFollowing] = useState(null)
const [alert, setAlert] = useState(null)
const followBtnText = following ? translated.following : translated.follow
-
const followDescriptionText = following
? translated.followingDescription
: translated.followDescription
- const followAlertText = following
- ? translated.followingAlert
- : translated.followAlert
-
useEffect(() => {
- if (props.authenticatedAs) {
+ if (authenticatedAs) {
api.follow
- .get(props.project)
+ .get(project)
.done((follow) => {
setFollowing(follow.enabled)
- setAlert(follow.alert)
})
.fail((response) => {
if (response.status === 404) {
@@ -46,45 +45,78 @@ export const FollowButton = (props) => {
}
})
}
- }, [props.project, props.authenticatedAs])
+ }, [project, authenticatedAs])
const removeAlert = () => {
setAlert(null)
}
const toggleFollow = () => {
- if (props.authenticatedAs === null) {
+ if (authenticatedAs === null) {
window.location.href = config.getLoginUrl()
return
}
- api.follow.change({ enabled: !following }, props.project).done((follow) => {
+
+ api.follow.change({ enabled: !following }, project).done((follow) => {
setFollowing(follow.enabled)
setAlert({
- type: 'success',
- message: followAlertText
+ type: follow.enabled ? 'success' : 'warning',
+ message: follow.enabled ? translated.followAlert : translated.followingAlert
})
})
}
+ const buttonClasses = following ? 'a4-btn a4-btn--following' : 'a4-btn a4-btn--follow'
+
+ const AlertPortal = () => {
+ if (!alert) return null
+
+ if (!alertTarget) {
+ console.error('AlertPortal: No alert target provided')
+ return null
+ }
+
+ const container = document.getElementById(alertTarget)
+
+ if (!container) {
+ console.error('AlertPortal: Target element with ID "' + alertTarget + '" not found in DOM')
+ return null
+ }
+
+ return ReactDOM.createPortal(
+ ,
+ container
+ )
+ }
+
return (
-
+
-
-
-
+
+ {alertTarget
+ ? (
+
+ )
+ : (
+
+
+
+ )}
)
}
+
+export default FollowButton
diff --git a/adhocracy4/follows/static/follows/__tests__/FollowButton.jest.jsx b/adhocracy4/follows/static/follows/__tests__/FollowButton.jest.jsx
index f4d3eb20f..134f9bd3c 100644
--- a/adhocracy4/follows/static/follows/__tests__/FollowButton.jest.jsx
+++ b/adhocracy4/follows/static/follows/__tests__/FollowButton.jest.jsx
@@ -66,3 +66,29 @@ test('Test FollowButton redirect', async () => {
expect(api.follow.change).not.toHaveBeenCalled()
expect(api.follow.get).not.toHaveBeenCalled()
})
+
+test('Test AlertPortal with target that does not exist', async () => {
+ api.follow.setFollowing({ enabled: false })
+ render()
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
+ const followButton = await screen.findByText('Follow')
+ expect(followButton).toBeTruthy()
+ fireEvent.click(followButton)
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
+ 'AlertPortal: Target element with ID "non-existent-id" not found in DOM'
+ )
+ consoleErrorSpy.mockRestore()
+})
+
+test('Test AlertPortal renders correctly with existing target', async () => {
+ api.follow.setFollowing({ enabled: false })
+ const alertContainer = document.createElement('div')
+ alertContainer.id = 'alert-container'
+ document.body.appendChild(alertContainer)
+ render()
+ const followButton = await screen.findByText('Follow')
+ fireEvent.click(followButton)
+ const alertElement = screen.getByText('You will be updated via email.')
+ expect(alertElement).toBeInTheDocument()
+ document.body.removeChild(alertContainer)
+})
diff --git a/adhocracy4/follows/templatetags/react_follows.py b/adhocracy4/follows/templatetags/react_follows.py
index e0d1de3c2..d5884c632 100644
--- a/adhocracy4/follows/templatetags/react_follows.py
+++ b/adhocracy4/follows/templatetags/react_follows.py
@@ -7,13 +7,20 @@
@register.simple_tag(takes_context=True)
-def react_follows(context, project):
+def react_follows(context, project, alert_target=None, custom_classes=""):
request = context["request"]
user = request.user
authenticated_as = None
if user.is_authenticated:
authenticated_as = user.username
+
attributes = {"project": project.slug, "authenticatedAs": authenticated_as}
+
+ if custom_classes:
+ attributes["customClasses"] = custom_classes
+ if alert_target:
+ attributes["alertTarget"] = alert_target
+
return format_html(
'',
attributes=json.dumps(attributes),
diff --git a/changelog/_0005.md b/changelog/_0005.md
new file mode 100644
index 000000000..fa8e303fa
--- /dev/null
+++ b/changelog/_0005.md
@@ -0,0 +1,4 @@
+### Changed
+- FollowButton.jsx: Added ability to add custom classes customize with `customClasses` parameter
+- FollowButton.jsx: Implemented alert relocation capability with `alertTarget` parameter and
+- FollowButton.jsx: Added warning style to unfollowing alerts for better visual feedback
\ No newline at end of file