Skip to content

Commit 009715a

Browse files
committed
follows: refactor FollowButton.js
1 parent 531fd17 commit 009715a

File tree

5 files changed

+95
-26
lines changed

5 files changed

+95
-26
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import django from 'django'
21
import React, { useState, useEffect } from 'react'
2+
import ReactDOM from 'react-dom'
3+
import django from 'django'
34
import Alert from '../../../static/Alert'
4-
55
import api from '../../../static/api'
66
import config from '../../../static/config'
77

@@ -18,73 +18,105 @@ const translated = {
1818
following: django.gettext('Following')
1919
}
2020

21-
export const FollowButton = (props) => {
21+
export const FollowButton = ({
22+
project,
23+
authenticatedAs,
24+
customClasses = '',
25+
alertTarget = null
26+
}) => {
2227
const [following, setFollowing] = useState(null)
2328
const [alert, setAlert] = useState(null)
2429

2530
const followBtnText = following ? translated.following : translated.follow
26-
2731
const followDescriptionText = following
2832
? translated.followingDescription
2933
: translated.followDescription
3034

31-
const followAlertText = following
32-
? translated.followingAlert
33-
: translated.followAlert
34-
3535
useEffect(() => {
36-
if (props.authenticatedAs) {
36+
if (authenticatedAs) {
3737
api.follow
38-
.get(props.project)
38+
.get(project)
3939
.done((follow) => {
4040
setFollowing(follow.enabled)
41-
setAlert(follow.alert)
4241
})
4342
.fail((response) => {
4443
if (response.status === 404) {
4544
setFollowing(false)
4645
}
4746
})
4847
}
49-
}, [props.project, props.authenticatedAs])
48+
}, [project, authenticatedAs])
5049

5150
const removeAlert = () => {
5251
setAlert(null)
5352
}
5453

5554
const toggleFollow = () => {
56-
if (props.authenticatedAs === null) {
55+
if (authenticatedAs === null) {
5756
window.location.href = config.getLoginUrl()
5857
return
5958
}
60-
api.follow.change({ enabled: !following }, props.project).done((follow) => {
59+
60+
api.follow.change({ enabled: !following }, project).done((follow) => {
6161
setFollowing(follow.enabled)
6262
setAlert({
63-
type: 'success',
64-
message: followAlertText
63+
type: follow.enabled ? 'success' : 'warning',
64+
message: follow.enabled ? translated.followAlert : translated.followingAlert
6565
})
6666
})
6767
}
6868

69+
const buttonClasses = following ? 'a4-btn a4-btn--following' : 'a4-btn a4-btn--follow'
70+
71+
const AlertPortal = () => {
72+
if (!alert) return null
73+
74+
if (!alertTarget) {
75+
console.error('AlertPortal: No alert target provided')
76+
return null
77+
}
78+
79+
const container = document.getElementById(alertTarget)
80+
81+
if (!container) {
82+
console.error('AlertPortal: Target element with ID ' + alertTarget + ' not found in DOM')
83+
return null
84+
}
85+
86+
return ReactDOM.createPortal(
87+
<Alert onClick={removeAlert} {...alert} />,
88+
container
89+
)
90+
}
91+
6992
return (
70-
<span className="a4-follow">
93+
<span className={'a4-follow ' + customClasses}>
7194
<button
72-
className={
73-
following ? 'a4-btn a4-btn--following' : 'a4-btn a4-btn--follow'
74-
}
95+
className={buttonClasses}
7596
type="button"
7697
onClick={toggleFollow}
7798
aria-describedby="follow-description"
78-
disabled={following === null && props.authenticatedAs !== null}
99+
aria-pressed={following}
100+
disabled={following === null && authenticatedAs !== null}
79101
>
80102
<span className="a4-follow__btn--content">{followBtnText}</span>
103+
81104
<span className="a4-sr-only" id="follow-description">
82105
{followDescriptionText}
83106
</span>
84107
</button>
85-
<span className="a4-follow__notification">
86-
<Alert onClick={removeAlert} {...alert} />
87-
</span>
108+
109+
{alertTarget
110+
? (
111+
<AlertPortal />
112+
)
113+
: (
114+
<span className="a4-follow__notification">
115+
<Alert onClick={removeAlert} {...alert} />
116+
</span>
117+
)}
88118
</span>
89119
)
90120
}
121+
122+
export default FollowButton

adhocracy4/follows/static/follows/__tests__/FollowButton.jest.jsx

+26
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,29 @@ test('Test FollowButton redirect', async () => {
6666
expect(api.follow.change).not.toHaveBeenCalled()
6767
expect(api.follow.get).not.toHaveBeenCalled()
6868
})
69+
70+
test('Test AlertPortal with target that does not exist', async () => {
71+
api.follow.setFollowing({ enabled: false })
72+
render(<FollowButton authenticatedAs project="test" alertTarget="non-existent-id" />)
73+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
74+
const followButton = await screen.findByText('Follow')
75+
expect(followButton).toBeTruthy()
76+
fireEvent.click(followButton)
77+
expect(consoleErrorSpy).toHaveBeenCalledWith(
78+
'AlertPortal: Target element with ID "non-existent-id" not found in DOM'
79+
)
80+
consoleErrorSpy.mockRestore()
81+
})
82+
83+
test('Test AlertPortal renders correctly with existing target', async () => {
84+
api.follow.setFollowing({ enabled: false })
85+
const alertContainer = document.createElement('div')
86+
alertContainer.id = 'alert-container'
87+
document.body.appendChild(alertContainer)
88+
render(<FollowButton authenticatedAs project="test" alertTarget="alert-container" />)
89+
const followButton = await screen.findByText('Follow')
90+
fireEvent.click(followButton)
91+
const alertElement = screen.getByText('You will be updated via email.')
92+
expect(alertElement).toBeInTheDocument()
93+
document.body.removeChild(alertContainer)
94+
})

adhocracy4/follows/static/follows/react_follows.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ module.exports.renderFollow = function (el) {
77
const props = JSON.parse(el.getAttribute('data-attributes'))
88
const root = createRoot(el)
99
root.render(<FollowButton {...props} />)
10-
}
10+
}

adhocracy4/follows/templatetags/react_follows.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,20 @@
77

88

99
@register.simple_tag(takes_context=True)
10-
def react_follows(context, project):
10+
def react_follows(context, project, alert_target=None, custom_classes=""):
1111
request = context["request"]
1212
user = request.user
1313
authenticated_as = None
1414
if user.is_authenticated:
1515
authenticated_as = user.username
16+
1617
attributes = {"project": project.slug, "authenticatedAs": authenticated_as}
18+
19+
if custom_classes:
20+
attributes["customClasses"] = custom_classes
21+
if alert_target:
22+
attributes["alertTarget"] = alert_target
23+
1724
return format_html(
1825
'<span data-a4-widget="follows" data-attributes="{attributes}"></span>',
1926
attributes=json.dumps(attributes),

changelog/_0005.md

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
### Changed
2+
- FollowButton.jsx: Added ability to add custom classes customize with `customClasses` parameter
3+
- FollowButton.jsx: Implemented alert relocation capability with `alertTarget` parameter and
4+
- FollowButton.jsx: Added warning style to unfollowing alerts for better visual feedback

0 commit comments

Comments
 (0)