diff --git a/jsapp/js/account/organization/MemberActionsDropdown.tsx b/jsapp/js/account/organization/MemberActionsDropdown.tsx new file mode 100644 index 0000000000..b88bb33fe1 --- /dev/null +++ b/jsapp/js/account/organization/MemberActionsDropdown.tsx @@ -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 && + { + setIsRemoveModalVisible(false); + }} + onCancel={() => setIsRemoveModalVisible(false)} + /> + } + + } + menuContent={ +
+
+ } + /> + + ); +} diff --git a/jsapp/js/account/organization/MemberRemoveModal.tsx b/jsapp/js/account/organization/MemberRemoveModal.tsx new file mode 100644 index 0000000000..822c7b3e31 --- /dev/null +++ b/jsapp/js/account/organization/MemberRemoveModal.tsx @@ -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 ( + onCancel()}> + {textToDisplay.title} + + +

{textToDisplay.description}

+ + +
+ + +