diff --git a/lib/utils/role-utils.js b/lib/utils/role-utils.js index 0c96bb77b..57b6b1780 100644 --- a/lib/utils/role-utils.js +++ b/lib/utils/role-utils.js @@ -1,161 +1,140 @@ // @flow import _groupBy from 'lodash/fp/groupBy.js'; import _toPairs from 'lodash/fp/toPairs.js'; import * as React from 'react'; import { useSelector } from './redux-utils.js'; import { useSortedENSResolvedUsers } from '../hooks/ens-cache.js'; import { threadInfoSelector } from '../selectors/thread-selectors.js'; import type { RelativeMemberInfo, RoleInfo, ThreadInfo, } from '../types/minimally-encoded-thread-permissions-types.js'; import { configurableCommunityPermissions, type ThreadRolePermissionsBlob, type UserSurfacedPermission, } from '../types/thread-permission-types.js'; import { threadTypes } from '../types/thread-types-enum.js'; function constructRoleDeletionMessagePrompt( defaultRoleName: string, memberCount: number, ): string { let message; if (memberCount === 0) { message = 'Are you sure you want to delete this role?'; } else { const messageNoun = memberCount === 1 ? 'member' : 'members'; const messageVerb = memberCount === 1 ? 'is' : 'are'; message = `There ${messageVerb} currently ${memberCount} ${messageNoun} with ` + `this role. Deleting the role will automatically assign the ` + `${messageNoun} affected to the ${defaultRoleName} role.`; } return message; } -type RoleDeletableAndEditableStatus = { - +isDeletable: boolean, - +isEditable: boolean, -}; -function useRoleDeletableAndEditableStatus( - roleName: string, - defaultRoleID: string, - existingRoleID: string, -): RoleDeletableAndEditableStatus { - return React.useMemo(() => { - const canDelete = roleName !== 'Admins' && defaultRoleID !== existingRoleID; - const canEdit = roleName !== 'Admins'; - - return { - isDeletable: canDelete, - isEditable: canEdit, - }; - }, [roleName, defaultRoleID, existingRoleID]); -} - function useRolesFromCommunityThreadInfo( threadInfo: ThreadInfo, memberInfos: $ReadOnlyArray, ): $ReadOnlyMap { // Our in-code system has chat-specific roles, while the // user-surfaced system has roles only for communities. We retrieve roles // from the top-level community thread for accuracy, with a rare fallback // for potential issues reading memberInfos, primarily in GENESIS threads. // The special case is GENESIS threads, since per prior discussion // (see context: https://linear.app/comm/issue/ENG-4077/), we don't really // support roles for it. Also with GENESIS, the list of members are not // populated in the community root. So in this case to prevent crashing, we // should just return the role name from the current thread info. const { community } = threadInfo; const communityThreadInfo = useSelector(state => community ? threadInfoSelector(state)[community] : null, ); const topMostThreadInfo = communityThreadInfo || threadInfo; const roleMap = new Map(); if (topMostThreadInfo.type === threadTypes.GENESIS) { memberInfos.forEach(memberInfo => roleMap.set( memberInfo.id, memberInfo.role ? threadInfo.roles[memberInfo.role] : null, ), ); return roleMap; } const { members: memberInfosFromTopMostThreadInfo, roles } = topMostThreadInfo; memberInfosFromTopMostThreadInfo.forEach(memberInfo => { roleMap.set(memberInfo.id, memberInfo.role ? roles[memberInfo.role] : null); }); return roleMap; } function useMembersGroupedByRole( threadInfo: ThreadInfo, ): $ReadOnlyMap> { const { members } = threadInfo; const sortedENSResolvedMembers = useSortedENSResolvedUsers(members); const roles = useRolesFromCommunityThreadInfo( threadInfo, sortedENSResolvedMembers, ); const groupByRoleName = React.useMemo( () => _groupBy(member => roles.get(member.id)?.name)(sortedENSResolvedMembers), [sortedENSResolvedMembers, roles], ); const membersGroupedByRoleMap = React.useMemo(() => { const map: Map> = new Map(); _toPairs(groupByRoleName) .sort((a, b) => a[0].localeCompare(b[0])) .forEach(([roleName, memberInfos]) => map.set(roleName, memberInfos)); return map; }, [groupByRoleName]); return membersGroupedByRoleMap; } function toggleUserSurfacedPermission( rolePermissions: ThreadRolePermissionsBlob, userSurfacedPermission: UserSurfacedPermission, ): ThreadRolePermissionsBlob { const userSurfacedPermissionSet = Array.from( configurableCommunityPermissions[userSurfacedPermission], ); const currentRolePermissions = { ...rolePermissions }; const roleHasPermission = userSurfacedPermissionSet.every( permission => currentRolePermissions[permission], ); if (roleHasPermission) { for (const permission of userSurfacedPermissionSet) { delete currentRolePermissions[permission]; } } else { for (const permission of userSurfacedPermissionSet) { currentRolePermissions[permission] = true; } } return currentRolePermissions; } export { constructRoleDeletionMessagePrompt, - useRoleDeletableAndEditableStatus, useRolesFromCommunityThreadInfo, toggleUserSurfacedPermission, useMembersGroupedByRole, }; diff --git a/web/roles/role-actions-menu.react.js b/web/roles/role-actions-menu.react.js index 28960cc7e..ec5af849a 100644 --- a/web/roles/role-actions-menu.react.js +++ b/web/roles/role-actions-menu.react.js @@ -1,124 +1,117 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import SWMansionIcon from 'lib/components/swmansion-icon.react.js'; import { + roleIsAdminRole, roleIsDefaultRole, useRoleUserSurfacedPermissions, } from 'lib/shared/thread-utils.js'; import type { RoleInfo, ThreadInfo, } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import { values } from 'lib/utils/objects.js'; -import { useRoleDeletableAndEditableStatus } from 'lib/utils/role-utils.js'; import CreateRolesModal from './create-roles-modal.react.js'; import DeleteRoleModal from './delete-role-modal.react.js'; import css from './role-actions-menu.css'; import MenuItem from '../components/menu-item.react.js'; import Menu from '../components/menu.react.js'; const menuIcon = ; type RoleActionsMenuProps = { +threadInfo: ThreadInfo, +roleName: string, }; function RoleActionsMenu(props: RoleActionsMenuProps): React.Node { const { threadInfo, roleName } = props; const { pushModal } = useModalContext(); const defaultRoleID = Object.keys(threadInfo.roles).find(roleID => roleIsDefaultRole(threadInfo.roles[roleID]), ); invariant(defaultRoleID, 'default role should exist'); const existingRole = values(threadInfo.roles).find( (role: RoleInfo) => role.name === roleName, ); invariant(existingRole, 'existing role should exist'); - const roleOptions = useRoleDeletableAndEditableStatus( - roleName, - defaultRoleID, - existingRole.id, - ); - const roleNamesToUserSurfacedPermissions = useRoleUserSurfacedPermissions(threadInfo); const openEditRoleModal = React.useCallback( () => pushModal( , ), [ existingRole.id, pushModal, roleName, roleNamesToUserSurfacedPermissions, threadInfo, ], ); const openDeleteRoleModal = React.useCallback(() => { pushModal( , ); }, [pushModal, threadInfo, defaultRoleID, existingRole.id]); const menuItems = React.useMemo(() => { const availableOptions = []; - const { isDeletable, isEditable } = roleOptions; - if (isEditable) { + if (!roleIsAdminRole(existingRole)) { availableOptions.push( , ); } - if (isDeletable) { + if (!roleIsAdminRole(existingRole) && !roleIsDefaultRole(existingRole)) { availableOptions.push( , ); } return availableOptions; - }, [roleOptions, openDeleteRoleModal, openEditRoleModal]); + }, [existingRole, openEditRoleModal, openDeleteRoleModal]); return (
{menuItems}
); } export default RoleActionsMenu;