-
-
Notifications
You must be signed in to change notification settings - Fork 184
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(MembersRoute): add actions dropdown to the table of organization…
… members TASK-987 TASK-990 (#5309) ### 📣 Summary In Members Table (from Account → Team/Organization → Members) add actions dropdown with one action. The action allows removing a member or leaving the organization. ### 📖 Description The availability of "Remove" action is based on the role of the logged in user. When trying to remove user (or leave organization) a confirmation prompt is being shown. ### 👀 Preview steps 1. ℹ️ have multiple different users 2. for one of the users (e.g. "joe"), use http://kf.kobo.local/admin/organizations/organization/ to add multiple users into joe's organization 3. For one of the users (e.g. "sue") set the role to "admin" 4. enable "Multi-members override" for joe's organization 5. enable feature flag `mmosEnabled` 6. navigate to `#/account/organization/members` 7. 🟢 notice that a table of organization members displays a list of users added in step 2 8. 🟢 notice that in the table "joe" (as owner) has an ability to remove all organization members (excluding themselves) including "sue" who's an admin As continuation for above steps: 1. log in as "sue" 2. navigate to `#/account/organization/members` 3. 🟢 notice that in the table "joe" (the owner) has no actions available 4. 🟢 notice that any "member" user in the table has actions available, and if you click "Remove" a confirmation prompt will appear 5. 🟢 notice that for "sue" the action available is "Leave team"/"Leave organization", and if you click it, a confirmation prompt will appear - and it will have different content than confirmation prompt for removing a member As continuation for above steps: 1. pick a "member" user and use "Remove" action 2. confirm prompt 3. 🟢 verify that user was removed successfuly ### 💭 Notes Build atop #5281
- Loading branch information
1 parent
18628b1
commit af28266
Showing
5 changed files
with
327 additions
and
71 deletions.
There are no files selected for viewing
103 changes: 103 additions & 0 deletions
103
jsapp/js/account/organization/MemberActionsDropdown.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
// Libraries | ||
import {useState} from 'react'; | ||
import cx from 'classnames'; | ||
|
||
// Partial components | ||
import KoboDropdown from 'jsapp/js/components/common/koboDropdown'; | ||
import Button from 'jsapp/js/components/common/button'; | ||
import MemberRemoveModal from './MemberRemoveModal'; | ||
|
||
// Stores, hooks and utilities | ||
import {useSession} from 'jsapp/js/stores/useSession'; | ||
import {getSimpleMMOLabel} from './organization.utils'; | ||
import envStore from 'jsapp/js/envStore'; | ||
import subscriptionStore from 'jsapp/js/account/subscriptionStore'; | ||
|
||
// Constants and types | ||
import {OrganizationUserRole} from './organizationQuery'; | ||
|
||
// Styles | ||
import styles from './memberActionsDropdown.module.scss'; | ||
|
||
interface MemberActionsDropdownProps { | ||
targetUsername: string; | ||
/** | ||
* The role of the currently logged in user, i.e. the role of the user that | ||
* wants to do the actions (not the role of the target member). | ||
*/ | ||
currentUserRole: OrganizationUserRole; | ||
} | ||
|
||
/** | ||
* A dropdown with all actions that can be taken towards an organization member. | ||
*/ | ||
export default function MemberActionsDropdown( | ||
{targetUsername, currentUserRole}: MemberActionsDropdownProps | ||
) { | ||
const session = useSession(); | ||
const [isRemoveModalVisible, setIsRemoveModalVisible] = useState(false); | ||
|
||
// Wait for session | ||
if (!session.currentLoggedAccount?.username) { | ||
return null; | ||
} | ||
|
||
// Should Not Happen™, but let's make it foolproof :) Members are not allowed | ||
// to do anything here under any circumstances. | ||
if (currentUserRole === OrganizationUserRole.member) { | ||
return null; | ||
} | ||
|
||
// If logged in user is an admin and tries to remove themselves, we need | ||
// different UI - thus we check it here. | ||
const isAdminRemovingSelf = Boolean( | ||
targetUsername === session.currentLoggedAccount?.username && | ||
currentUserRole === OrganizationUserRole.admin | ||
); | ||
|
||
// Different button label when user is removing themselves | ||
let removeButtonLabel = t('Remove'); | ||
if (isAdminRemovingSelf) { | ||
const mmoLabel = getSimpleMMOLabel( | ||
envStore.data, | ||
subscriptionStore.activeSubscriptions[0], | ||
false, | ||
false | ||
); | ||
removeButtonLabel = t('Leave ##TEAM_OR_ORGANIZATION##') | ||
.replace('##TEAM_OR_ORGANIZATION##', mmoLabel); | ||
} | ||
|
||
return ( | ||
<> | ||
{isRemoveModalVisible && | ||
<MemberRemoveModal | ||
username={targetUsername} | ||
isRemovingSelf={isAdminRemovingSelf} | ||
onConfirmDone={() => { | ||
setIsRemoveModalVisible(false); | ||
}} | ||
onCancel={() => setIsRemoveModalVisible(false)} | ||
/> | ||
} | ||
|
||
<KoboDropdown | ||
name={`member-actions-dropdown-${targetUsername}`} | ||
placement='down-right' | ||
hideOnMenuClick | ||
triggerContent={<Button type='text' size='m' startIcon='more'/>} | ||
menuContent={ | ||
<div className={styles.menuContenet}> | ||
<Button | ||
className={cx(styles.menuButton, styles.menuButtonRed)} | ||
type='text' | ||
size='m' | ||
label={removeButtonLabel} | ||
onClick={() => setIsRemoveModalVisible(true)} | ||
/> | ||
</div> | ||
} | ||
/> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
// Partial components | ||
import Button from 'jsapp/js/components/common/button'; | ||
import InlineMessage from 'jsapp/js/components/common/inlineMessage'; | ||
import KoboModal from 'jsapp/js/components/modals/koboModal'; | ||
import KoboModalHeader from 'jsapp/js/components/modals/koboModalHeader'; | ||
import KoboModalContent from 'jsapp/js/components/modals/koboModalContent'; | ||
import KoboModalFooter from 'jsapp/js/components/modals/koboModalFooter'; | ||
|
||
// Stores, hooks and utilities | ||
import {getSimpleMMOLabel} from './organization.utils'; | ||
import envStore from 'jsapp/js/envStore'; | ||
import subscriptionStore from 'jsapp/js/account/subscriptionStore'; | ||
import {useRemoveOrganizationMember} from './membersQuery'; | ||
import {notify} from 'alertifyjs'; | ||
|
||
interface MemberRemoveModalProps { | ||
username: string; | ||
isRemovingSelf: boolean; | ||
onConfirmDone: () => void; | ||
onCancel: () => void; | ||
} | ||
|
||
/** | ||
* A confirmation prompt modal for removing a user from organization. Displays | ||
* two buttons and warning message. | ||
* | ||
* Note: it's always open - if you need to hide it, just don't render it at | ||
* the parent level. | ||
*/ | ||
export default function MemberRemoveModal( | ||
{ | ||
username, | ||
isRemovingSelf, | ||
onConfirmDone, | ||
onCancel, | ||
}: MemberRemoveModalProps | ||
) { | ||
const removeMember = useRemoveOrganizationMember(); | ||
const mmoLabel = getSimpleMMOLabel( | ||
envStore.data, | ||
subscriptionStore.activeSubscriptions[0], | ||
false, | ||
false | ||
); | ||
|
||
// There are two different sets of strings - one for removing a member, and | ||
// one for leaving the organization. | ||
const REMOVE_MEMBER_TEXT = { | ||
title: t('Remove ##username## from this ##TEAM_OR_ORGANIZATION##'), | ||
description: t('Are you sure you want to remove ##username## from this ##TEAM_OR_ORGANIZATION##?'), | ||
dangerMessage: t('Removing them from this ##TEAM_OR_ORGANIZATION## also means they will immediately lose access to any projects owned by your ##TEAM_OR_ORGANIZATION##. This action cannot be undone.'), | ||
confirmButtonLabel: t('Remove member'), | ||
}; | ||
const REMOVE_SELF_TEXT = { | ||
title: t('Leave this ##TEAM_OR_ORGANIZATION##'), | ||
description: t('Are you sure you want to leave this ##TEAM_OR_ORGANIZATION##?'), | ||
dangerMessage: t('You will immediately lose access to any projects owned by this ##TEAM_OR_ORGANIZATION##. This action cannot be undone.'), | ||
confirmButtonLabel: t('Leave ##TEAM_OR_ORGANIZATION##'), | ||
}; | ||
const textToDisplay = isRemovingSelf ? REMOVE_SELF_TEXT : REMOVE_MEMBER_TEXT; | ||
// Replace placeholders with proper strings in chosen set: | ||
for (const key in textToDisplay) { | ||
const keyCast = key as keyof typeof textToDisplay; | ||
textToDisplay[keyCast] = textToDisplay[keyCast] | ||
.replaceAll('##username##', username) | ||
.replaceAll('##TEAM_OR_ORGANIZATION##', mmoLabel); | ||
} | ||
|
||
return ( | ||
<KoboModal isOpen size='medium' onRequestClose={() => onCancel()}> | ||
<KoboModalHeader>{textToDisplay.title}</KoboModalHeader> | ||
|
||
<KoboModalContent> | ||
<p>{textToDisplay.description}</p> | ||
|
||
<InlineMessage type='error' icon='alert' message={textToDisplay.dangerMessage}/> | ||
</KoboModalContent> | ||
|
||
<KoboModalFooter> | ||
<Button | ||
type='secondary' | ||
size='m' | ||
onClick={onCancel} | ||
label={t('Cancel')} | ||
/> | ||
|
||
<Button | ||
type='danger' | ||
size='m' | ||
onClick={async () => { | ||
try { | ||
removeMember.mutateAsync(username); | ||
} catch (error) { | ||
notify('Failed to remove member', 'error'); | ||
} finally { | ||
onConfirmDone(); | ||
} | ||
}} | ||
label={textToDisplay.confirmButtonLabel} | ||
isPending={removeMember.isPending} | ||
/> | ||
</KoboModalFooter> | ||
</KoboModal> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.