Skip to content

Commit b02ac99

Browse files
authored
feat: transfer tasks and alerts when removing a team member (#6531)
* feat: transfer tasks and alerts when removing user * chore: clean up types * chore: add test ids * chore: remove unnecessary redirect * chore: remove unused import * chore: change testid name * chore: update user test to use remove user overlay
1 parent 94d637e commit b02ac99

File tree

8 files changed

+228
-80
lines changed

8 files changed

+228
-80
lines changed

cypress/e2e/cloud/users.test.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,21 @@ describe('Users Page', () => {
5959
cy.getByTestIDSubStr('user-list-item').should('have.length', 2)
6060

6161
cy.getByTestID(`user-list-item user@influxdata.com`).within(() => {
62-
cy.getByTestID('delete-user--button').trigger('mouseover')
63-
// TODO figure out how to have cypress handle hover events
64-
// cy.getByTestID('delete-user--button').should('be.visible')
65-
cy.getByTestID('delete-user--button').click()
62+
// This won't be deemed visible due to Cypress's (lack of) hover handling.
63+
// Just check for existence. https://docs.cypress.io/api/commands/hover
64+
cy.getByTestID('delete-user--button').should('exist').click()
6665
})
6766

68-
cy.getByTestID('delete-user--confirm-button').should('be.visible')
69-
cy.getByTestID('delete-user--confirm-button').click()
67+
cy.getByTestID('remove-member-overlay--container').within(() => {
68+
cy.getByTestID('dropdown--button').should('be.visible').click()
69+
cy.getByTestID('dropdown-menu')
70+
.should('be.visible')
71+
.and('contain', 'josh@influxdata.com')
72+
.click()
73+
cy.getByTestID('remove-member-form--submit').should('be.visible').click()
74+
})
7075

71-
cy.getByTestID('notification-success--dismiss').click()
76+
cy.getByTestID('notification-success--dismiss').should('be.visible').click()
7277

7378
cy.getByTestIDSubStr('user-list-item').should('have.length', 1)
7479
})

src/organizations/components/OrgProfileTab/LeaveOrg.tsx

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,41 @@
11
// Libraries
22
import React, {FC, useContext} from 'react'
3-
import {useSelector} from 'react-redux'
3+
import {useDispatch, useSelector} from 'react-redux'
44

55
// Components
6-
import {
7-
ButtonShape,
8-
ComponentColor,
9-
ConfirmationButton,
10-
FlexBox,
11-
IconFont,
12-
} from '@influxdata/clockface'
6+
import {Button, ComponentColor, FlexBox, IconFont} from '@influxdata/clockface'
137

148
// Selector
159
import {selectCurrentOrg, selectUser} from 'src/identity/selectors'
1610

1711
// Providers
1812
import {UsersContext} from 'src/users/context/users'
1913

20-
// Constants
21-
import {CLOUD_URL} from 'src/shared/constants'
14+
// Overlay
15+
import {dismissOverlay, showOverlay} from 'src/overlays/actions/overlays'
16+
17+
// Types
18+
import {RemoveMemberOverlayParams} from 'src/users/components/RemoveMemberOverlay'
2219

2320
// Styles
2421
import 'src/organizations/components/OrgProfileTab/style.scss'
2522

2623
export const LeaveOrgButton: FC = () => {
2724
const org = useSelector(selectCurrentOrg)
25+
const {users} = useContext(UsersContext)
2826
const currentUserId = useSelector(selectUser)?.id
2927
const {removeUser} = useContext(UsersContext)
28+
const userToRemove = users.find(user => user.id === currentUserId)
29+
const dispatch = useDispatch()
3030

3131
const handleRemoveUser = () => {
32-
removeUser(currentUserId)
33-
window.location.href = CLOUD_URL
32+
const params: RemoveMemberOverlayParams = {
33+
removeUser,
34+
users,
35+
userToRemove,
36+
}
37+
38+
dispatch(showOverlay('remove-member', params, () => dismissOverlay()))
3439
}
3540

3641
return (
@@ -40,18 +45,15 @@ export const LeaveOrgButton: FC = () => {
4045
<p className="org-profile-tab--heading org-profile-tab--deleteHeading">
4146
Leave the <b>{org.name}</b> organization.
4247
</p>
43-
<ConfirmationButton
48+
<Button
4449
className="org-profile-tab--leaveOrgButton"
4550
color={ComponentColor.Default}
46-
confirmationButtonColor={ComponentColor.Danger}
47-
confirmationButtonText="Leave Organization"
48-
confirmationLabel="This action will remove yourself from accessing this organization"
4951
icon={IconFont.Logout}
50-
onConfirm={handleRemoveUser}
51-
shape={ButtonShape.Square}
52+
id={currentUserId}
53+
onClick={handleRemoveUser}
5254
testID="delete-user"
5355
text="Leave Organization"
54-
titleText="Leave Organization"
56+
titleText="Remove member access"
5557
/>
5658
</FlexBox.Child>
5759
</>

src/overlays/components/OverlayController.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,11 @@ import ConfirmationOverlay from 'src/support/components/ConfirmationOverlay'
4848
import {CreateOrganizationOverlay} from 'src/identity/components/GlobalHeader/GlobalHeaderDropdown/CreateOrganization/CreateOrganizationOverlay'
4949
import {MarketoAccountUpgradeOverlay} from 'src/identity/components/MarketoAccountUpgradeOverlay'
5050
import {SuspendPaidOrgOverlay} from 'src/organizations/components/OrgProfileTab/SuspendPaidOrgOverlay'
51+
import {ReplaceCertificateOverlay} from 'src/writeData/subscriptions/components/CertificateInput'
52+
import {RemoveMemberOverlay} from 'src/users/components/RemoveMemberOverlay'
5153

5254
// Actions
5355
import {dismissOverlay} from 'src/overlays/actions/overlays'
54-
import {ReplaceCertificateOverlay} from 'src/writeData/subscriptions/components/CertificateInput'
5556

5657
export interface OverlayContextType {
5758
onClose: () => void
@@ -189,6 +190,9 @@ export const OverlayController: FunctionComponent = () => {
189190
case 'suspend-org-in-paid-account':
190191
activeOverlay.current = <SuspendPaidOrgOverlay />
191192
break
193+
case 'remove-member':
194+
activeOverlay.current = <RemoveMemberOverlay />
195+
break
192196
default:
193197
activeOverlay.current = null
194198
break

src/overlays/reducers/overlays.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export type OverlayID =
4040
| 'create-organization'
4141
| 'marketo-upgrade-account-overlay'
4242
| 'suspend-org-in-paid-account'
43+
| 'remove-member'
4344

4445
export interface OverlayState {
4546
id: OverlayID | null
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Libraries
2+
import React, {FC, useContext, useState} from 'react'
3+
import {useSelector} from 'react-redux'
4+
5+
import {
6+
Alert,
7+
AlignItems,
8+
Button,
9+
ComponentColor,
10+
ComponentStatus,
11+
Dropdown,
12+
FlexBox,
13+
FlexDirection,
14+
Form,
15+
IconFont,
16+
Overlay,
17+
RemoteDataState,
18+
SpinnerContainer,
19+
TechnoSpinner,
20+
} from '@influxdata/clockface'
21+
22+
// Contexts
23+
import {OverlayContext} from 'src/overlays/components/OverlayController'
24+
25+
// Constants
26+
import {CLOUD_URL} from 'src/shared/constants'
27+
28+
// Selectors
29+
import {selectUser} from 'src/identity/selectors'
30+
31+
// Types
32+
import {User} from 'src/client/unityRoutes'
33+
import {UsersContextType} from 'src/users/context/users'
34+
35+
export interface RemoveMemberOverlayParams {
36+
removeUser: UsersContextType['removeUser']
37+
users: User[]
38+
userToRemove: User
39+
}
40+
41+
export const RemoveMemberOverlay: FC = () => {
42+
const {onClose, params} = useContext(OverlayContext)
43+
const {removeUser, users, userToRemove} = params
44+
const otherUsers = users.filter((user: User) => user.id !== userToRemove.id)
45+
const [newTasksAlertsUser, setNewTasksAlertsUser] = useState(otherUsers[0])
46+
47+
const currentUserId = useSelector(selectUser).id
48+
const removingSelf = userToRemove.id === currentUserId
49+
50+
const handleSelectTasksAlertsUser = (user: User) => {
51+
setNewTasksAlertsUser(user)
52+
}
53+
54+
const handleRemoveAndTransfer = () => {
55+
removeUser(userToRemove.id, newTasksAlertsUser.id)
56+
57+
if (removingSelf) {
58+
window.location.href = CLOUD_URL
59+
} else {
60+
onClose()
61+
}
62+
}
63+
64+
const usersLoadedStatus = Boolean(users.length)
65+
? RemoteDataState.Done
66+
: RemoteDataState.Loading
67+
68+
return (
69+
<Overlay.Container
70+
className="remove-member-overlay"
71+
maxWidth={650}
72+
testID="remove-member-overlay--container"
73+
>
74+
<Overlay.Header
75+
onDismiss={onClose}
76+
testID="remove-member-overlay--header"
77+
title={`Remove ${userToRemove.email}`}
78+
/>
79+
<Overlay.Body>
80+
<Alert
81+
className="remove-member-overlay--warning-message"
82+
color={ComponentColor.Danger}
83+
icon={IconFont.AlertTriangle}
84+
>
85+
When you remove {removingSelf ? 'yourself' : 'a member'} from an
86+
organization, you need to transfer tasks and alerts to another member
87+
so that scripts are not interrupted.
88+
</Alert>
89+
<br />
90+
<SpinnerContainer
91+
loading={usersLoadedStatus}
92+
spinnerComponent={<TechnoSpinner />}
93+
>
94+
<FlexBox
95+
alignItems={AlignItems.FlexStart}
96+
className="remove-member-overlay--form"
97+
direction={FlexDirection.Row}
98+
>
99+
<Form.Element
100+
label={`Transfer ${userToRemove.email}'s tasks and alerts to:`}
101+
required={true}
102+
>
103+
<Dropdown
104+
testID="remove-member--transfer-dropdown"
105+
button={(active, onClick) => (
106+
<Dropdown.Button active={active} onClick={onClick}>
107+
<b>{newTasksAlertsUser.email}</b>
108+
</Dropdown.Button>
109+
)}
110+
menu={onCollapse => (
111+
<Dropdown.Menu onCollapse={onCollapse}>
112+
{otherUsers.map((user: User) => (
113+
<Dropdown.Item
114+
key={user.id}
115+
id={user.id}
116+
selected={user.id === newTasksAlertsUser.id}
117+
value={user}
118+
onClick={handleSelectTasksAlertsUser}
119+
>
120+
{user.email}
121+
</Dropdown.Item>
122+
))}
123+
</Dropdown.Menu>
124+
)}
125+
/>
126+
</Form.Element>
127+
</FlexBox>
128+
</SpinnerContainer>
129+
</Overlay.Body>
130+
<Overlay.Footer>
131+
<Button
132+
color={ComponentColor.Default}
133+
onClick={onClose}
134+
testID="remove-member-form--cancel"
135+
text="Cancel"
136+
/>
137+
<Button
138+
color={ComponentColor.Danger}
139+
onClick={handleRemoveAndTransfer}
140+
status={ComponentStatus.Default}
141+
testID="remove-member-form--submit"
142+
text="Remove and Transfer"
143+
/>
144+
</Overlay.Footer>
145+
</Overlay.Container>
146+
)
147+
}

src/users/components/UserList.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,43 @@
11
// Libraries
22
import React, {FC, useContext, useState} from 'react'
3+
import {useDispatch} from 'react-redux'
34

45
// Components
56
import {Columns, Grid, IndexList} from '@influxdata/clockface'
67
import {UsersContext} from 'src/users/context/users'
78
import {UserListItem} from 'src/users/components/UserListItem'
89
import {InviteListItem} from 'src/users/components/InviteListItem'
910

11+
// Overlays
12+
import {dismissOverlay, showOverlay} from 'src/overlays/actions/overlays'
13+
14+
// Types
15+
import {RemoveMemberOverlayParams} from 'src/users/components/RemoveMemberOverlay'
16+
1017
// Utils
1118
import {SearchWidget} from 'src/shared/components/search_widget/SearchWidget'
1219
import {filter} from 'src/users/utils/filter'
1320

1421
export const UserList: FC = () => {
15-
const {users, invites} = useContext(UsersContext)
16-
22+
const {removeUser, users, invites} = useContext(UsersContext)
23+
const dispatch = useDispatch()
1724
const [searchTerm, setSearchTerm] = useState('')
18-
1925
const filteredUsers = filter(
2026
users,
2127
['email', 'firstName', 'lastName', 'role'],
2228
searchTerm
2329
)
24-
2530
const filteredInvites = filter(invites, ['email', 'role'], searchTerm)
2631

2732
const isDeletable = users.length > 1
2833

34+
const handleRemoveUser = (evt: React.MouseEvent<HTMLElement, MouseEvent>) => {
35+
const userToRemove = users.find(user => user.id === evt.currentTarget.id)
36+
const params: RemoveMemberOverlayParams = {removeUser, users, userToRemove}
37+
38+
dispatch(showOverlay('remove-member', params, () => dismissOverlay()))
39+
}
40+
2941
return (
3042
<Grid>
3143
<Grid.Row>
@@ -51,6 +63,7 @@ export const UserList: FC = () => {
5163
))}
5264
{filteredUsers.map(user => (
5365
<UserListItem
66+
handleRemoveUser={handleRemoveUser}
5467
key={`user-${user.id}`}
5568
user={user}
5669
isDeletable={isDeletable}

0 commit comments

Comments
 (0)