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