Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

follows: refactor FollowButton.js #1780

Merged
merged 1 commit into from
Mar 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 56 additions & 24 deletions adhocracy4/follows/static/follows/FollowButton.jsx
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -18,73 +18,105 @@ 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) {
setFollowing(false)
}
})
}
}, [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(
<Alert onClick={removeAlert} {...alert} />,
container
)
}

return (
<span className="a4-follow">
<span className={'a4-follow ' + customClasses}>
<button
className={
following ? 'a4-btn a4-btn--following' : 'a4-btn a4-btn--follow'
}
className={buttonClasses}
type="button"
onClick={toggleFollow}
aria-describedby="follow-description"
disabled={following === null && props.authenticatedAs !== null}
aria-pressed={following}
disabled={following === null && authenticatedAs !== null}
>
<span className="a4-follow__btn--content">{followBtnText}</span>

<span className="a4-sr-only" id="follow-description">
{followDescriptionText}
</span>
</button>
<span className="a4-follow__notification">
<Alert onClick={removeAlert} {...alert} />
</span>

{alertTarget
? (
<AlertPortal />
)
: (
<span className="a4-follow__notification">
<Alert onClick={removeAlert} {...alert} />
</span>
)}
</span>
)
}

export default FollowButton
26 changes: 26 additions & 0 deletions adhocracy4/follows/static/follows/__tests__/FollowButton.jest.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<FollowButton authenticatedAs project="test" alertTarget="non-existent-id" />)
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(<FollowButton authenticatedAs project="test" alertTarget="alert-container" />)
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)
})
9 changes: 8 additions & 1 deletion adhocracy4/follows/templatetags/react_follows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
'<span data-a4-widget="follows" data-attributes="{attributes}"></span>',
attributes=json.dumps(attributes),
Expand Down
4 changes: 4 additions & 0 deletions changelog/_0005.md
Original file line number Diff line number Diff line change
@@ -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
Loading