From f351cf9f5a894fe019f331cc0ec6f012a0779c42 Mon Sep 17 00:00:00 2001 From: Bartosz Jarocki Date: Fri, 8 Mar 2024 14:14:15 +0100 Subject: [PATCH] feat: managing teams (#9285) Co-authored-by: Marcus Wermuth Co-authored-by: Jordan Husney --- .vscode/launch.json | 4 +- .../client/components/DeleteTeamDialog.tsx | 74 +++++ .../client/components/OrgAdminActionMenu.tsx | 93 +++++++ .../components/OrgBilling/Organization.tsx | 22 +- .../components/OrgMembers/OrgMembers.tsx | 5 + .../OrgTeamMembers/OrgTeamMemberMenu.tsx | 71 +++++ .../OrgTeamMembers/OrgTeamMembers.tsx | 104 +++++++ .../OrgTeamMembers/OrgTeamMembersMenu.tsx | 26 ++ .../OrgTeamMembers/OrgTeamMembersRoot.tsx | 25 ++ .../OrgTeamMembers/OrgTeamMembersRow.tsx | 117 ++++++++ .../components/OrgTeams/OrgTeams.tsx | 57 ++-- .../components/OrgTeams/OrgTeamsRow.tsx | 51 ++-- .../components/OrgUserRow/OrgMemberRow.tsx | 258 ++++++++++++------ .../containers/OrgMembers/OrgMembersRoot.tsx | 1 + .../client/mutations/ArchiveTeamMutation.ts | 8 + packages/client/package.json | 2 + .../client/ui/AlertDialog/AlertDialog.tsx | 5 + .../ui/AlertDialog/AlertDialogAction.tsx | 18 ++ .../ui/AlertDialog/AlertDialogCancel.tsx | 18 ++ .../ui/AlertDialog/AlertDialogContent.tsx | 23 ++ .../ui/AlertDialog/AlertDialogDescription.tsx | 15 + .../ui/AlertDialog/AlertDialogFooter.tsx | 10 + .../ui/AlertDialog/AlertDialogHeader.tsx | 7 + .../ui/AlertDialog/AlertDialogOverlay.tsx | 18 ++ .../ui/AlertDialog/AlertDialogPortal.tsx | 3 + .../ui/AlertDialog/AlertDialogTitle.tsx | 15 + .../ui/AlertDialog/AlertDialogTrigger.tsx | 3 + packages/client/ui/Avatar/Avatar.tsx | 16 ++ packages/client/ui/Avatar/AvatarFallback.tsx | 19 ++ packages/client/ui/Avatar/AvatarImage.tsx | 16 ++ packages/client/ui/Button/Button.tsx | 21 +- packages/server/graphql/types/Organization.ts | 17 +- packages/server/utils/authorization.ts | 25 +- yarn.lock | 56 ++++ 34 files changed, 1058 insertions(+), 165 deletions(-) create mode 100644 packages/client/components/DeleteTeamDialog.tsx create mode 100644 packages/client/components/OrgAdminActionMenu.tsx create mode 100644 packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMemberMenu.tsx create mode 100644 packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembers.tsx create mode 100644 packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersMenu.tsx create mode 100644 packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersRoot.tsx create mode 100644 packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersRow.tsx create mode 100644 packages/client/ui/AlertDialog/AlertDialog.tsx create mode 100644 packages/client/ui/AlertDialog/AlertDialogAction.tsx create mode 100644 packages/client/ui/AlertDialog/AlertDialogCancel.tsx create mode 100644 packages/client/ui/AlertDialog/AlertDialogContent.tsx create mode 100644 packages/client/ui/AlertDialog/AlertDialogDescription.tsx create mode 100644 packages/client/ui/AlertDialog/AlertDialogFooter.tsx create mode 100644 packages/client/ui/AlertDialog/AlertDialogHeader.tsx create mode 100644 packages/client/ui/AlertDialog/AlertDialogOverlay.tsx create mode 100644 packages/client/ui/AlertDialog/AlertDialogPortal.tsx create mode 100644 packages/client/ui/AlertDialog/AlertDialogTitle.tsx create mode 100644 packages/client/ui/AlertDialog/AlertDialogTrigger.tsx create mode 100644 packages/client/ui/Avatar/Avatar.tsx create mode 100644 packages/client/ui/Avatar/AvatarFallback.tsx create mode 100644 packages/client/ui/Avatar/AvatarImage.tsx diff --git a/.vscode/launch.json b/.vscode/launch.json index 2d3b6382f4e..a7fe78cf299 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -30,7 +30,7 @@ { "name": "Launch Chrome", "request": "launch", - "type": "pwa-chrome", + "type": "chrome", "url": "http://localhost:3000", "webRoot": "${workspaceFolder}" }, @@ -38,7 +38,7 @@ "type": "node", "request": "launch", "name": "Debug Executor", - "program": "scripts/runExecutor.js", + "program": "scripts/runExecutor.js" }, { "type": "node", diff --git a/packages/client/components/DeleteTeamDialog.tsx b/packages/client/components/DeleteTeamDialog.tsx new file mode 100644 index 00000000000..d6bb993bbde --- /dev/null +++ b/packages/client/components/DeleteTeamDialog.tsx @@ -0,0 +1,74 @@ +import React, {useState} from 'react' +import FlatPrimaryButton from './FlatPrimaryButton' +import {Input} from '../ui/Input/Input' +import {Dialog} from '../ui/Dialog/Dialog' +import {DialogContent} from '../ui/Dialog/DialogContent' +import {DialogTitle} from '../ui/Dialog/DialogTitle' +import {DialogActions} from '../ui/Dialog/DialogActions' +import useMutationProps from '../hooks/useMutationProps' +import SecondaryButton from './SecondaryButton' +import ArchiveTeamMutation from '../mutations/ArchiveTeamMutation' +import useAtmosphere from '../hooks/useAtmosphere' +import useRouter from '../hooks/useRouter' + +interface Props { + isOpen: boolean + onClose: () => void + onDeleteTeam: (teamId: string) => void + teamId: string + teamName: string + teamOrgId: string +} + +const DeleteTeamDialog = (props: Props) => { + const atmosphere = useAtmosphere() + const {history} = useRouter() + const {isOpen, onClose, teamId, teamName, teamOrgId, onDeleteTeam} = props + + const {submitting, onCompleted, onError, error, submitMutation} = useMutationProps() + + const [typedTeamName, setTypedTeamName] = useState(false) + + const handleDeleteTeam = () => { + if (submitting) return + submitMutation() + ArchiveTeamMutation(atmosphere, {teamId}, {history, onError, onCompleted}) + onDeleteTeam(teamId) + history.push(`/me/organizations/${teamOrgId}/teams`) + } + + return ( + + + Delete Team + +
+ + { + e.preventDefault() + if (e.target.value === teamName) setTypedTeamName(true) + else setTypedTeamName(false) + }} + placeholder={teamName} + /> + {error && ( +
{error.message}
+ )} +
+ + + + I understand the consequences, delete this team + + Cancel + +
+
+ ) +} + +export default DeleteTeamDialog diff --git a/packages/client/components/OrgAdminActionMenu.tsx b/packages/client/components/OrgAdminActionMenu.tsx new file mode 100644 index 00000000000..19799e08abb --- /dev/null +++ b/packages/client/components/OrgAdminActionMenu.tsx @@ -0,0 +1,93 @@ +import graphql from 'babel-plugin-relay/macro' +import React from 'react' +import {useFragment} from 'react-relay' +import useAtmosphere from '~/hooks/useAtmosphere' +import {MenuProps} from '../hooks/useMenu' +import SetOrgUserRoleMutation from '../mutations/SetOrgUserRoleMutation' +import useMutationProps from '../hooks/useMutationProps' +import {OrgAdminActionMenu_organization$key} from '../__generated__/OrgAdminActionMenu_organization.graphql' +import {OrgAdminActionMenu_organizationUser$key} from '../__generated__/OrgAdminActionMenu_organizationUser.graphql' +import Menu from './Menu' +import MenuItem from './MenuItem' + +interface Props { + menuProps: MenuProps + isViewerLastOrgAdmin: boolean + organizationUser: OrgAdminActionMenu_organizationUser$key + organization: OrgAdminActionMenu_organization$key + toggleLeave: () => void + toggleRemove: () => void +} + +const OrgAdminActionMenu = (props: Props) => { + const { + menuProps, + isViewerLastOrgAdmin, + organizationUser: organizationUserRef, + organization: organizationRef, + toggleLeave, + toggleRemove + } = props + const organization = useFragment( + graphql` + fragment OrgAdminActionMenu_organization on Organization { + id + } + `, + organizationRef + ) + const organizationUser = useFragment( + graphql` + fragment OrgAdminActionMenu_organizationUser on OrganizationUser { + role + user { + id + } + } + `, + organizationUserRef + ) + const atmosphere = useAtmosphere() + const {onError, onCompleted, submitting, submitMutation} = useMutationProps() + const {id: orgId} = organization + const {viewerId} = atmosphere + const {role, user} = organizationUser + const {id: userId} = user + + const setRole = + (role: 'ORG_ADMIN' | 'BILLING_LEADER' | null = null) => + () => { + if (submitting) return + submitMutation() + const variables = {orgId, userId, role} + SetOrgUserRoleMutation(atmosphere, variables, {onError, onCompleted}) + } + + const isOrgAdmin = role === 'ORG_ADMIN' + const isBillingLeader = role === 'BILLING_LEADER' + const isSelf = viewerId === userId + const canRemoveSelf = isSelf && !isViewerLastOrgAdmin + const roleName = role === 'ORG_ADMIN' ? 'Org Admin' : 'Billing Leader' + + return ( + <> + + {!isOrgAdmin && } + {!isOrgAdmin && !isBillingLeader && ( + + )} + {isOrgAdmin && !isSelf && ( + + )} + {((role && !isSelf) || canRemoveSelf) && ( + + )} + {canRemoveSelf && } + {!isSelf && } + {isSelf && !canRemoveSelf && } + + + ) +} + +export default OrgAdminActionMenu diff --git a/packages/client/modules/userDashboard/components/OrgBilling/Organization.tsx b/packages/client/modules/userDashboard/components/OrgBilling/Organization.tsx index 36ea78f60e3..ceab6eb6e09 100644 --- a/packages/client/modules/userDashboard/components/OrgBilling/Organization.tsx +++ b/packages/client/modules/userDashboard/components/OrgBilling/Organization.tsx @@ -21,6 +21,10 @@ const OrgMembers = lazy( import(/* webpackChunkName: 'OrgMembersRoot' */ '../../containers/OrgMembers/OrgMembersRoot') ) +const OrgTeamMembers = lazy( + () => import(/* webpackChunkName: 'OrgTeamMembers' */ '../OrgTeamMembers/OrgTeamMembersRoot') +) + const OrgDetails = lazy(() => import(/* webpackChunkName: 'OrgDetails' */ './OrgDetails')) const Authentication = lazy( () => @@ -70,13 +74,7 @@ const Organization = (props: Props) => { path={`${match.url}/${BILLING_PAGE}`} render={() => } /> - {isBillingLeader && ( - } - /> - )} + { path={`${match.url}/${AUTHENTICATION_PAGE}`} render={(p) => } /> + {isBillingLeader && ( + <> + } + /> + + + )} ) diff --git a/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx b/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx index a4d365a8e91..e56be6deb45 100644 --- a/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx +++ b/packages/client/modules/userDashboard/components/OrgMembers/OrgMembers.tsx @@ -75,6 +75,10 @@ const OrgMembers = (props: Props) => { ['BILLING_LEADER', 'ORG_ADMIN'].includes(node.role ?? '') ? count + 1 : count, 0 ) + const orgAdminCount = organizationUsers.edges.reduce( + (count, {node}) => (['ORG_ADMIN'].includes(node.role ?? '') ? count + 1 : count), + 0 + ) const exportToCSV = async () => { const rows = organizationUsers.edges.map((orgUser, idx) => { @@ -117,6 +121,7 @@ const OrgMembers = (props: Props) => { diff --git a/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMemberMenu.tsx b/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMemberMenu.tsx new file mode 100644 index 00000000000..7526eb55775 --- /dev/null +++ b/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMemberMenu.tsx @@ -0,0 +1,71 @@ +import styled from '@emotion/styled' +import React from 'react' +import graphql from 'babel-plugin-relay/macro' +import useAtmosphere from '~/hooks/useAtmosphere' +import {useFragment} from 'react-relay' +import {MenuProps} from '../../../../hooks/useMenu' +import Menu from '../../../../components/Menu' +import MenuItem from '../../../../components/MenuItem' +import MenuItemLabel from '../../../../components/MenuItemLabel' +import {OrgTeamMemberMenu_teamMember$key} from '../../../../__generated__/OrgTeamMemberMenu_teamMember.graphql' + +interface OrgTeamMemberMenuProps { + isLead: boolean + menuProps: MenuProps + isViewerLead: boolean + isViewerOrgAdmin: boolean + manageTeamMemberId?: string | null + teamMember: OrgTeamMemberMenu_teamMember$key + handleNavigate?: () => void + togglePromote: () => void + toggleRemove: () => void +} + +const StyledLabel = styled(MenuItemLabel)({ + padding: '4px 16px' +}) + +export const OrgTeamMemberMenu = (props: OrgTeamMemberMenuProps) => { + const { + isViewerLead, + isViewerOrgAdmin, + teamMember: teamMemberRef, + menuProps, + togglePromote, + toggleRemove + } = props + const teamMember = useFragment( + graphql` + fragment OrgTeamMemberMenu_teamMember on TeamMember { + isSelf + preferredName + userId + isLead + } + `, + teamMemberRef + ) + const atmosphere = useAtmosphere() + const {preferredName, userId} = teamMember + const {viewerId} = atmosphere + const isSelf = userId === viewerId + const isViewerTeamAdmin = isViewerLead || isViewerOrgAdmin + + return ( + + {isViewerTeamAdmin && (!isSelf || !isViewerLead) && ( + Promote {preferredName} to Team Lead} + key='promote' + onClick={togglePromote} + /> + )} + {isViewerTeamAdmin && !isSelf && ( + Remove {preferredName} from Team} + onClick={toggleRemove} + /> + )} + + ) +} diff --git a/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembers.tsx b/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembers.tsx new file mode 100644 index 00000000000..602a4b48ee5 --- /dev/null +++ b/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembers.tsx @@ -0,0 +1,104 @@ +import React from 'react' +import graphql from 'babel-plugin-relay/macro' +import {ArrowBack} from '@mui/icons-material' +import {PreloadedQuery, usePreloadedQuery} from 'react-relay' +import {OrgTeamMembersQuery} from '../../../../__generated__/OrgTeamMembersQuery.graphql' +import {OrgTeamMembersRow} from './OrgTeamMembersRow' +import {Button} from '../../../../ui/Button/Button' +import {Link} from 'react-router-dom' +import {ORGANIZATIONS} from '../../../../utils/constants' +import {MenuPosition} from '../../../../hooks/useCoords' +import useMenu from '../../../../hooks/useMenu' +import {OrgTeamMembersMenu} from './OrgTeamMembersMenu' +import {useDialogState} from '../../../../ui/Dialog/useDialogState' +import DeleteTeamDialog from '../../../../components/DeleteTeamDialog' + +interface Props { + queryRef: PreloadedQuery +} + +const query = graphql` + query OrgTeamMembersQuery($teamId: ID!) { + viewer { + team(teamId: $teamId) { + ...ArchiveTeam_team + id + billingTier + isOrgAdmin + isViewerLead + name + orgId + teamMembers(sortBy: "preferredName") { + id + isNotRemoved + ...AddTeamMemberModal_teamMembers + ...OrgTeamMembersRow_teamMember + } + tier + } + } + } +` + +export const OrgTeamMembers = (props: Props) => { + const {queryRef} = props + const data = usePreloadedQuery(query, queryRef) + const {viewer} = data + const {team} = viewer + const {menuPortal, menuProps} = useMenu(MenuPosition.UPPER_RIGHT) + + const { + open: openDeleteTeamDialog, + close: closeDeleteTeamDialog, + isOpen: isDeleteTeamDialogOpened + } = useDialogState() + + if (!team) return null + const {isViewerLead, isOrgAdmin: isViewerOrgAdmin, teamMembers} = team + + return ( +
+
+
+ +

{team.name}

+
+
+ +
+
+
+
{teamMembers.length} Active
+
+
+ {teamMembers.map((teamMember) => ( + + ))} +
+ + {menuPortal( + + )} + + {isDeleteTeamDialogOpened ? ( + + ) : null} +
+ ) +} diff --git a/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersMenu.tsx b/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersMenu.tsx new file mode 100644 index 00000000000..bc53031bfc8 --- /dev/null +++ b/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersMenu.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import {MenuProps} from '../../../../hooks/useMenu' +import Menu from '../../../../components/Menu' +import MenuItem from '../../../../components/MenuItem' + +interface OrgTeamMembersMenuProps { + menuProps: MenuProps + openDeleteTeamModal: () => void +} + +export const OrgTeamMembersMenu = (props: OrgTeamMembersMenuProps) => { + const {menuProps, openDeleteTeamModal} = props + const {closePortal} = menuProps + + return ( + + { + closePortal() + openDeleteTeamModal() + }} + /> + + ) +} diff --git a/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersRoot.tsx b/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersRoot.tsx new file mode 100644 index 00000000000..c0c959d084c --- /dev/null +++ b/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersRoot.tsx @@ -0,0 +1,25 @@ +import React, {Suspense} from 'react' +import orgTeamMembersQuery, {OrgTeamMembersQuery} from '~/__generated__/OrgTeamMembersQuery.graphql' +import useQueryLoaderNow from '../../../../hooks/useQueryLoaderNow' +import {LoaderSize} from '../../../../types/constEnums' +import {Loader} from '../../../../utils/relay/renderLoader' +import {OrgTeamMembers} from './OrgTeamMembers' +import useRouter from '../../../../hooks/useRouter' + +const OrgTeamMembersRoot = () => { + const {match} = useRouter<{teamId: string}>() + const { + params: {teamId} + } = match + const queryRef = useQueryLoaderNow(orgTeamMembersQuery, { + teamId + }) + + return ( + }> + {queryRef && } + + ) +} + +export default OrgTeamMembersRoot diff --git a/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersRow.tsx b/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersRow.tsx new file mode 100644 index 00000000000..02b643f744f --- /dev/null +++ b/packages/client/modules/userDashboard/components/OrgTeamMembers/OrgTeamMembersRow.tsx @@ -0,0 +1,117 @@ +import React from 'react' +import graphql from 'babel-plugin-relay/macro' +import {useFragment} from 'react-relay' +import {OrgTeamMembersRow_teamMember$key} from '../../../../__generated__/OrgTeamMembersRow_teamMember.graphql' +import {Avatar} from '../../../../ui/Avatar/Avatar' +import {AvatarFallback} from '../../../../ui/Avatar/AvatarFallback' +import {AvatarImage} from '../../../../ui/Avatar/AvatarImage' +import {Button} from '../../../../ui/Button/Button' +import {MoreVert} from '@mui/icons-material' +import {OrgTeamMemberMenu} from './OrgTeamMemberMenu' +import {MenuPosition} from '../../../../hooks/useCoords' +import useMenu from '../../../../hooks/useMenu' +import PromoteTeamMemberModal from '../../../teamDashboard/components/PromoteTeamMemberModal/PromoteTeamMemberModal' +import RemoveTeamMemberModal from '../../../teamDashboard/components/RemoveTeamMemberModal/RemoveTeamMemberModal' +import useModal from '../../../../hooks/useModal' +import useAtmosphere from '../../../../hooks/useAtmosphere' + +type Props = { + teamMemberRef: OrgTeamMembersRow_teamMember$key + isViewerLead: boolean + isViewerOrgAdmin: boolean +} + +export const OrgTeamMembersRow = (props: Props) => { + const {teamMemberRef} = props + const teamMember = useFragment( + graphql` + fragment OrgTeamMembersRow_teamMember on TeamMember { + ...OrgTeamMemberMenu_teamMember + userId + picture + preferredName + isLead + isOrgAdmin + isSelf + email + ...PromoteTeamMemberModal_teamMember + ...RemoveTeamMemberModal_teamMember + } + `, + teamMemberRef + ) + + const {isViewerLead, isViewerOrgAdmin} = props + const {isLead, isOrgAdmin, userId} = teamMember + + const atmosphere = useAtmosphere() + const {viewerId} = atmosphere + const isSelf = userId === viewerId + + const showMenuButton = (isViewerOrgAdmin && !isLead) || (isViewerLead && !isSelf && !isOrgAdmin) + + const {togglePortal, originRef, menuPortal, menuProps} = useMenu(MenuPosition.UPPER_RIGHT) + const { + closePortal: closePromote, + togglePortal: togglePromote, + modalPortal: portalPromote + } = useModal() + const { + closePortal: closeRemove, + togglePortal: toggleRemove, + modalPortal: portalRemove + } = useModal() + + return ( +
+
+ + + {teamMember.preferredName.substring(0, 2)} + +
+
+
+ {teamMember.preferredName}{' '} + {teamMember.isLead ? ( + + Team Lead + + ) : null} +
+ +
+
+ {showMenuButton && ( + + )} + {menuPortal( + + )} +
+ + {portalPromote()} + {portalRemove()} +
+ ) +} diff --git a/packages/client/modules/userDashboard/components/OrgTeams/OrgTeams.tsx b/packages/client/modules/userDashboard/components/OrgTeams/OrgTeams.tsx index e9bd39f71d7..3f544d7dd27 100644 --- a/packages/client/modules/userDashboard/components/OrgTeams/OrgTeams.tsx +++ b/packages/client/modules/userDashboard/components/OrgTeams/OrgTeams.tsx @@ -1,16 +1,12 @@ import React from 'react' import graphql from 'babel-plugin-relay/macro' -import styled from '@emotion/styled' -import Row from '../../../../components/Row/Row' -import Panel from '../../../../components/Panel/Panel' -import {ElementWidth} from '../../../../types/constEnums' import {useFragment} from 'react-relay' import OrgTeamsRow from './OrgTeamsRow' import {OrgTeams_organization$key} from '../../../../__generated__/OrgTeams_organization.graphql' - -const StyledPanel = styled(Panel)({ - maxWidth: ElementWidth.PANEL_WIDTH -}) +import AddTeamDialogRoot from '../../../../components/AddTeamDialogRoot' +import {Button} from '../../../../ui/Button/Button' +import {useDialogState} from '../../../../ui/Dialog/useDialogState' +import plural from '../../../../utils/plural' type Props = { organizationRef: OrgTeams_organization$key @@ -31,23 +27,48 @@ const OrgTeams = (props: Props) => { `, organizationRef ) + const { + open: openAddTeamDialog, + close: closeAddTeamDialog, + isOpen: isAddTeamDialogOpened + } = useDialogState() + const {allTeams, isBillingLeader} = organization if (!isBillingLeader) return null + return ( - <> -

{'Teams'}

- - -
-
Team Name
-
Lead
+
+
+

Teams

+
+ +
+
+ +
+
+
+
+ {allTeams.length} {plural(allTeams.length, 'Team')} +
- +
{allTeams.map((team) => ( ))} - - +
+ + {isAddTeamDialogOpened ? ( + + ) : null} +
) } diff --git a/packages/client/modules/userDashboard/components/OrgTeams/OrgTeamsRow.tsx b/packages/client/modules/userDashboard/components/OrgTeams/OrgTeamsRow.tsx index 00123a43b5d..d6da2e5e6c6 100644 --- a/packages/client/modules/userDashboard/components/OrgTeams/OrgTeamsRow.tsx +++ b/packages/client/modules/userDashboard/components/OrgTeams/OrgTeamsRow.tsx @@ -1,8 +1,9 @@ import React from 'react' import {Link} from 'react-router-dom' import graphql from 'babel-plugin-relay/macro' -import Row from '../../../../components/Row/Row' import {useFragment} from 'react-relay' +import {ChevronRight} from '@mui/icons-material' + import plural from '../../../../utils/plural' import {OrgTeamsRow_team$key} from '../../../../__generated__/OrgTeamsRow_team.graphql' @@ -30,42 +31,26 @@ const OrgTeamsRow = (props: Props) => { ) const {id: teamId, teamMembers, name} = team const teamMembersCount = teamMembers.length - const teamLeadEmail = teamMembers.find((member) => member.isLead)?.email ?? '' - const isViewerTeamLead = teamMembers.some( - (member) => member.isSelf && (member.isLead || member.isOrgAdmin) - ) + return ( - -
-
{name}
-
-
- {`${teamMembersCount} ${plural(teamMembersCount, 'member')}`} - {isViewerTeamLead && ( - <> - - - {'Manage Team'} - - - )} + +
+
+
{name}
+
+
+ {`${teamMembersCount} ${plural(teamMembersCount, 'member')}`} +
- - - {`${teamLeadEmail} ${isViewerTeamLead ? '(You)' : ''}`} - +
+
+
- + ) } diff --git a/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx b/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx index eda83c7b6dc..8a15e046096 100644 --- a/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx +++ b/packages/client/modules/userDashboard/components/OrgUserRow/OrgMemberRow.tsx @@ -18,13 +18,18 @@ import RoleTag from '../../../../components/Tag/RoleTag' import {MenuPosition} from '../../../../hooks/useCoords' import useMenu from '../../../../hooks/useMenu' import useModal from '../../../../hooks/useModal' -import useTooltip from '../../../../hooks/useTooltip' import defaultUserAvatar from '../../../../styles/theme/images/avatar-user.svg' import {Breakpoint} from '../../../../types/constEnums' import lazyPreload from '../../../../utils/lazyPreload' import withMutationProps, {WithMutationProps} from '../../../../utils/relay/withMutationProps' -import {OrgMemberRow_organization$key} from '../../../../__generated__/OrgMemberRow_organization.graphql' -import {OrgMemberRow_organizationUser$key} from '../../../../__generated__/OrgMemberRow_organizationUser.graphql' +import { + OrgMemberRow_organization$key, + OrgMemberRow_organization$data +} from '../../../../__generated__/OrgMemberRow_organization.graphql' +import { + OrgMemberRow_organizationUser$key, + OrgMemberRow_organizationUser$data +} from '../../../../__generated__/OrgMemberRow_organizationUser.graphql' import BaseTag from '../../../../components/Tag/BaseTag' const AvatarBlock = styled('div')({ @@ -59,6 +64,7 @@ const MenuToggleBlock = styled('div')({ interface Props extends WithMutationProps { billingLeaderCount: number + orgAdminCount: number organizationUser: OrgMemberRow_organizationUser$key organization: OrgMemberRow_organization$key } @@ -90,29 +96,157 @@ const BillingLeaderActionMenu = lazyPreload( /* webpackChunkName: 'BillingLeaderActionMenu' */ '../../../../components/BillingLeaderActionMenu' ) ) +const OrgAdminActionMenu = lazyPreload( + () => + import(/* webpackChunkName: 'OrgAdminActionMenu' */ '../../../../components/OrgAdminActionMenu') +) const RemoveFromOrgModal = lazyPreload( () => import(/* webpackChunkName: 'RemoveFromOrgModal' */ '../RemoveFromOrgModal/RemoveFromOrgModal') ) +interface UserAvatarProps { + picture?: string +} + +const UserAvatar: React.FC = ({picture}) => ( + + {picture ? ( + + ) : ( + default avatar + )} + +) + +interface UserInfoProps { + preferredName: string + email: string + isBillingLeader: boolean + isOrgAdmin: boolean + inactive: boolean | null + newUserUntil: string +} + +const UserInfo: React.FC = ({ + preferredName, + email, + isBillingLeader, + isOrgAdmin, + inactive, + newUserUntil +}) => ( + + + {preferredName} + {isBillingLeader && Billing Leader} + {isOrgAdmin && Org Admin} + {inactive && !isBillingLeader && !isOrgAdmin && Inactive} + {new Date(newUserUntil) > new Date() && New} + + + {email} + + +) + +interface UserActionsProps { + isViewerOrgAdmin: boolean + isViewerBillingLeader: boolean + isViewerLastOrgAdmin: boolean + isViewerLastBillingLeader: boolean + organization: OrgMemberRow_organization$data + organizationUser: OrgMemberRow_organizationUser$data + preferredName: string + viewerId: string +} + +const UserActions: React.FC = ({ + isViewerOrgAdmin, + isViewerBillingLeader, + isViewerLastOrgAdmin, + isViewerLastBillingLeader, + organizationUser, + organization, + preferredName, + viewerId +}) => { + const {orgId} = organization + const { + user: {userId} + } = organizationUser + const {togglePortal, originRef, menuPortal, menuProps} = useMenu(MenuPosition.UPPER_RIGHT) + const {togglePortal: toggleLeave, modalPortal: leaveModal} = useModal() + const {togglePortal: toggleRemove, modalPortal: removeModal} = useModal() + const actionMenuProps = { + menuProps, + originRef, + togglePortal, + toggleLeave, + toggleRemove, + isViewerLastOrgAdmin, + isViewerLastBillingLeader, + organization, + organizationUser + } + + const showLeaveButton = !isViewerOrgAdmin && !isViewerBillingLeader && viewerId === userId + + return ( + + + {showLeaveButton && ( + + Leave Organization + + )} + {(isViewerOrgAdmin || (isViewerBillingLeader && !isViewerLastBillingLeader)) && ( + + + + )} + {isViewerOrgAdmin && menuPortal()} + {!isViewerOrgAdmin && + isViewerBillingLeader && + menuPortal()} + {leaveModal()} + {removeModal( + + )} + + + ) +} + const OrgMemberRow = (props: Props) => { const atmosphere = useAtmosphere() const { billingLeaderCount, + orgAdminCount, organizationUser: organizationUserRef, organization: organizationRef } = props + const organization = useFragment( graphql` fragment OrgMemberRow_organization on Organization { isViewerBillingLeader: isBillingLeader + isViewerOrgAdmin: isOrgAdmin orgId: id ...BillingLeaderActionMenu_organization + ...OrgAdminActionMenu_organization } `, organizationRef ) + const organizationUser = useFragment( graphql` fragment OrgMemberRow_organizationUser on OrganizationUser { @@ -126,103 +260,49 @@ const OrgMemberRow = (props: Props) => { role newUserUntil ...BillingLeaderActionMenu_organizationUser + ...OrgAdminActionMenu_organizationUser } `, organizationUserRef ) - const {orgId, isViewerBillingLeader} = organization - const {newUserUntil, user, role} = organizationUser + + const {isViewerBillingLeader, isViewerOrgAdmin} = organization + + const { + newUserUntil, + user: {email, inactive, picture, preferredName}, + role + } = organizationUser + + const {viewerId} = atmosphere + const isBillingLeader = role === 'BILLING_LEADER' const isOrgAdmin = role === 'ORG_ADMIN' - const {email, inactive, picture, preferredName, userId} = user const isViewerLastBillingLeader = isViewerBillingLeader && isBillingLeader && billingLeaderCount === 1 - const {viewerId} = atmosphere - const {togglePortal, originRef, menuPortal, menuProps} = useMenu(MenuPosition.UPPER_RIGHT) - const {togglePortal: toggleLeave, modalPortal: leaveModal} = useModal() - const {togglePortal: toggleRemove, modalPortal: removeModal} = useModal() - const { - tooltipPortal, - openTooltip, - closeTooltip, - originRef: tooltipRef - } = useTooltip(MenuPosition.LOWER_RIGHT) - const canViewMenu = !isViewerLastBillingLeader && organizationUser.role !== 'ORG_ADMIN' + const isViewerLastOrgAdmin = isViewerOrgAdmin && isOrgAdmin && orgAdminCount === 1 return ( - - {picture ? ( - - ) : ( - - )} - - - - {preferredName} - {isBillingLeader && {'Billing Leader'}} - {isOrgAdmin && {'Org Admin'}} - {inactive && !isBillingLeader && !isOrgAdmin && {'Inactive'}} - {new Date(newUserUntil) > new Date() && {'New'}} - - - {email} - - - - - {!isBillingLeader && !isOrgAdmin && viewerId === userId && ( - - Leave Organization - - )} - {!canViewMenu && ( - - {tooltipPortal( - isViewerLastBillingLeader ? ( -
- {'You need to promote another Billing Leader'} -
- {'before you can remove this role.'} -
- ) : ( -
Contact support (love@parabol.co) to remove the Org Admin role
- ) - )} - -
- )} - {isViewerBillingLeader && canViewMenu && ( - - - - )} - {menuPortal( - - )} - {leaveModal()} - {removeModal( - - )} -
-
+ + +
) } diff --git a/packages/client/modules/userDashboard/containers/OrgMembers/OrgMembersRoot.tsx b/packages/client/modules/userDashboard/containers/OrgMembers/OrgMembersRoot.tsx index b0a7ab43373..8242ebf2b8e 100644 --- a/packages/client/modules/userDashboard/containers/OrgMembers/OrgMembersRoot.tsx +++ b/packages/client/modules/userDashboard/containers/OrgMembers/OrgMembersRoot.tsx @@ -15,6 +15,7 @@ const OrgMembersRoot = (props: Props) => { orgId, first: 10000 }) + return ( }> {queryRef && } diff --git a/packages/client/mutations/ArchiveTeamMutation.ts b/packages/client/mutations/ArchiveTeamMutation.ts index 15afd828d4b..218fdd0c7c1 100644 --- a/packages/client/mutations/ArchiveTeamMutation.ts +++ b/packages/client/mutations/ArchiveTeamMutation.ts @@ -30,6 +30,14 @@ graphql` activeMeetings { id } + organization { + allTeams { + id + } + viewerTeams { + id + } + } } teamTemplateIds } diff --git a/packages/client/package.json b/packages/client/package.json index 364c5dbd367..81691d805d0 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -82,6 +82,8 @@ "@radix-ui/react-select": "^1.2.2", "@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-alert-dialog": "1.0.5", "@sentry/browser": "^5.8.0", "@stripe/react-stripe-js": "^1.16.5", "@stripe/stripe-js": "^1.47.0", diff --git a/packages/client/ui/AlertDialog/AlertDialog.tsx b/packages/client/ui/AlertDialog/AlertDialog.tsx new file mode 100644 index 00000000000..8b4fdf6a396 --- /dev/null +++ b/packages/client/ui/AlertDialog/AlertDialog.tsx @@ -0,0 +1,5 @@ +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' + +const AlertDialog = AlertDialogPrimitive.Root + +export {AlertDialog} diff --git a/packages/client/ui/AlertDialog/AlertDialogAction.tsx b/packages/client/ui/AlertDialog/AlertDialogAction.tsx new file mode 100644 index 00000000000..75b93fb02a9 --- /dev/null +++ b/packages/client/ui/AlertDialog/AlertDialogAction.tsx @@ -0,0 +1,18 @@ +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' +import clsx from 'clsx' + +const AlertDialogAction = React.forwardRef< + HTMLButtonElement, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName diff --git a/packages/client/ui/AlertDialog/AlertDialogCancel.tsx b/packages/client/ui/AlertDialog/AlertDialogCancel.tsx new file mode 100644 index 00000000000..201ca648679 --- /dev/null +++ b/packages/client/ui/AlertDialog/AlertDialogCancel.tsx @@ -0,0 +1,18 @@ +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' +import clsx from 'clsx' + +const AlertDialogCancel = React.forwardRef< + HTMLButtonElement, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName diff --git a/packages/client/ui/AlertDialog/AlertDialogContent.tsx b/packages/client/ui/AlertDialog/AlertDialogContent.tsx new file mode 100644 index 00000000000..dbcd0de0582 --- /dev/null +++ b/packages/client/ui/AlertDialog/AlertDialogContent.tsx @@ -0,0 +1,23 @@ +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' +import clsx from 'clsx' +import {AlertDialogOverlay} from './AlertDialogOverlay' +import {AlertDialogPortal} from './AlertDialog' + +const AlertDialogContent = React.forwardRef< + HTMLDivElement, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName diff --git a/packages/client/ui/AlertDialog/AlertDialogDescription.tsx b/packages/client/ui/AlertDialog/AlertDialogDescription.tsx new file mode 100644 index 00000000000..7c940b02fec --- /dev/null +++ b/packages/client/ui/AlertDialog/AlertDialogDescription.tsx @@ -0,0 +1,15 @@ +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' +import clsx from 'clsx' + +const AlertDialogDescription = React.forwardRef< + HTMLParagraphElement, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + +)) +AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName diff --git a/packages/client/ui/AlertDialog/AlertDialogFooter.tsx b/packages/client/ui/AlertDialog/AlertDialogFooter.tsx new file mode 100644 index 00000000000..0148d9a8c39 --- /dev/null +++ b/packages/client/ui/AlertDialog/AlertDialogFooter.tsx @@ -0,0 +1,10 @@ +import * as React from 'react' +import clsx from 'clsx' + +const AlertDialogFooter = ({className, ...props}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = 'AlertDialogFooter' diff --git a/packages/client/ui/AlertDialog/AlertDialogHeader.tsx b/packages/client/ui/AlertDialog/AlertDialogHeader.tsx new file mode 100644 index 00000000000..869b09ccfc3 --- /dev/null +++ b/packages/client/ui/AlertDialog/AlertDialogHeader.tsx @@ -0,0 +1,7 @@ +import * as React from 'react' +import clsx from 'clsx' + +const AlertDialogHeader = ({className, ...props}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = 'AlertDialogHeader' diff --git a/packages/client/ui/AlertDialog/AlertDialogOverlay.tsx b/packages/client/ui/AlertDialog/AlertDialogOverlay.tsx new file mode 100644 index 00000000000..e867c7ed363 --- /dev/null +++ b/packages/client/ui/AlertDialog/AlertDialogOverlay.tsx @@ -0,0 +1,18 @@ +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' +import clsx from 'clsx' + +export const AlertDialogOverlay = React.forwardRef< + HTMLDivElement, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName diff --git a/packages/client/ui/AlertDialog/AlertDialogPortal.tsx b/packages/client/ui/AlertDialog/AlertDialogPortal.tsx new file mode 100644 index 00000000000..c4e8d56916f --- /dev/null +++ b/packages/client/ui/AlertDialog/AlertDialogPortal.tsx @@ -0,0 +1,3 @@ +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' + +const AlertDialogPortal = AlertDialogPrimitive.Portal diff --git a/packages/client/ui/AlertDialog/AlertDialogTitle.tsx b/packages/client/ui/AlertDialog/AlertDialogTitle.tsx new file mode 100644 index 00000000000..c7c14e011c5 --- /dev/null +++ b/packages/client/ui/AlertDialog/AlertDialogTitle.tsx @@ -0,0 +1,15 @@ +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' +import clsx from 'clsx' + +const AlertDialogTitle = React.forwardRef< + HTMLHeadingElement, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName diff --git a/packages/client/ui/AlertDialog/AlertDialogTrigger.tsx b/packages/client/ui/AlertDialog/AlertDialogTrigger.tsx new file mode 100644 index 00000000000..0a20feddfc1 --- /dev/null +++ b/packages/client/ui/AlertDialog/AlertDialogTrigger.tsx @@ -0,0 +1,3 @@ +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger diff --git a/packages/client/ui/Avatar/Avatar.tsx b/packages/client/ui/Avatar/Avatar.tsx new file mode 100644 index 00000000000..23bbecd22a6 --- /dev/null +++ b/packages/client/ui/Avatar/Avatar.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import * as AvatarPrimitive from '@radix-ui/react-avatar' +import clsx from 'clsx' + +export const Avatar = React.forwardRef< + HTMLSpanElement, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + +)) + +Avatar.displayName = AvatarPrimitive.Root.displayName diff --git a/packages/client/ui/Avatar/AvatarFallback.tsx b/packages/client/ui/Avatar/AvatarFallback.tsx new file mode 100644 index 00000000000..482648102a8 --- /dev/null +++ b/packages/client/ui/Avatar/AvatarFallback.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import * as AvatarPrimitive from '@radix-ui/react-avatar' +import clsx from 'clsx' + +export const AvatarFallback = React.forwardRef< + HTMLSpanElement, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + +)) + +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName diff --git a/packages/client/ui/Avatar/AvatarImage.tsx b/packages/client/ui/Avatar/AvatarImage.tsx new file mode 100644 index 00000000000..66f41abad5d --- /dev/null +++ b/packages/client/ui/Avatar/AvatarImage.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import * as AvatarPrimitive from '@radix-ui/react-avatar' +import clsx from 'clsx' + +export const AvatarImage = React.forwardRef< + HTMLImageElement, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + +)) + +AvatarImage.displayName = AvatarPrimitive.Image.displayName diff --git a/packages/client/ui/Button/Button.tsx b/packages/client/ui/Button/Button.tsx index a60c67700d7..90554dc662e 100644 --- a/packages/client/ui/Button/Button.tsx +++ b/packages/client/ui/Button/Button.tsx @@ -7,20 +7,21 @@ type Size = 'sm' | 'md' | 'lg' | 'default' type Shape = 'pill' | 'circle' | 'default' const BASE_STYLES = - 'cursor-pointer inline-flex items-center justify-center whitespace-nowrap font-semibold transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50' + 'cursor-pointer inline-flex items-center justify-center whitespace-nowrap transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50' // TODO: make sure the styles match the designs const VARIANT_STYLES: Record = { - primary: 'bg-primary text-white hover:bg-primary/90', - destructive: 'bg-tomato-500 text-white hover:bg-tomato-500/90', - outline: 'text-slate-900 border border-slate-400 hover:bg-slate-200 px-2.5 py-1 bg-transparent', - secondary: 'bg-sky-500 text-white hover:bg-sky-500/80', - ghost: 'hover:bg-accent', + primary: 'bg-gradient-to-r from-tomato-600 to-rose-500 text-white font-semibold hover:opacity-90', + destructive: 'bg-tomato-500 text-white font-semibold hover:bg-tomato-500/90', + outline: + 'text-slate-900 border border-slate-400 hover:bg-slate-200 px-2.5 py-1 bg-transparent font-semibold', + secondary: 'bg-sky-500 text-white hover:bg-sky-500/80 font-semibold', + ghost: 'hover:opacity-80 bg-transparent font-semibold', link: 'text-primary underline-offset-4 hover:underline' } const SIZE_STYLES: Record = { - default: 'px-4 py-2 text-xs', + default: '', sm: 'h-7 px-3 text-xs', md: 'h-9 px-4 text-sm', lg: 'h-11 px-8 text-base' @@ -29,18 +30,18 @@ const SIZE_STYLES: Record = { const SHAPE_STYLES: Record = { pill: 'rounded-full', circle: 'rounded-full aspect-square', - default: 'rounded-md' + default: '' } export interface ButtonProps extends React.ButtonHTMLAttributes { asChild?: boolean variant: Variant size?: Size - shape: Shape + shape?: Shape } const Button = React.forwardRef( - ({className, variant, size, shape, asChild = false, ...props}, ref) => { + ({className, variant, size = 'default', shape = 'default', asChild = false, ...props}, ref) => { const Comp = asChild ? Slot : 'button' return ( = new GraphQLObjectType { const viewerId = getUserId(authToken) return isUserBillingLeader(viewerId, orgId, dataLoader) } }, + isOrgAdmin: { + type: new GraphQLNonNull(GraphQLBoolean), + description: 'true if the viewer holds the the org admin role on the org', + resolve: async ({id: orgId}, _args: unknown, {authToken, dataLoader}) => { + const viewerId = getUserId(authToken) + return isUserOrgAdmin(viewerId, orgId, dataLoader) + } + }, name: { type: new GraphQLNonNull(GraphQLString), description: 'The name of the organization' diff --git a/packages/server/utils/authorization.ts b/packages/server/utils/authorization.ts index 4537f03e77c..dd873e6c885 100644 --- a/packages/server/utils/authorization.ts +++ b/packages/server/utils/authorization.ts @@ -4,6 +4,7 @@ import AuthToken from '../database/types/AuthToken' import OrganizationUser from '../database/types/OrganizationUser' import {DataLoaderWorker} from '../graphql/graphql' import {RDatum} from '../database/stricterR' +import {OrgUserRole} from '../database/types/OrganizationUser' export const getUserId = (authToken: any) => { return authToken && typeof authToken === 'object' ? (authToken.sub as string) : '' @@ -51,10 +52,12 @@ export const isTeamLead = async (userId: string, teamId: string, dataLoader: Dat interface Options { clearCache?: boolean } -export const isUserBillingLeader = async ( + +const isUserAnyRoleIn = async ( userId: string, orgId: string, dataLoader: DataLoaderWorker, + roles: OrgUserRole[], options?: Options ) => { const organizationUser = await dataLoader @@ -63,9 +66,23 @@ export const isUserBillingLeader = async ( if (options && options.clearCache) { dataLoader.get('organizationUsersByUserId').clear(userId) } - return organizationUser - ? organizationUser.role === 'BILLING_LEADER' || organizationUser.role === 'ORG_ADMIN' - : false + return organizationUser && organizationUser.role ? roles.includes(organizationUser.role) : false +} +export const isUserBillingLeader = async ( + userId: string, + orgId: string, + dataLoader: DataLoaderWorker, + options?: Options +) => { + return isUserAnyRoleIn(userId, orgId, dataLoader, ['BILLING_LEADER', 'ORG_ADMIN'], options) +} +export const isUserOrgAdmin = async ( + userId: string, + orgId: string, + dataLoader: DataLoaderWorker, + options?: Options +) => { + return isUserAnyRoleIn(userId, orgId, dataLoader, ['ORG_ADMIN'], options) } export const isUserInOrg = async (userId: string, orgId: string, dataLoader: DataLoaderWorker) => { diff --git a/yarn.lock b/yarn.lock index af959f0896f..ffa47f0a625 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5704,6 +5704,19 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-alert-dialog@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.0.5.tgz#70dd529cbf1e4bff386814d3776901fcaa131b8c" + integrity sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-dialog" "1.0.5" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-arrow@1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz#c24f7968996ed934d57fe6cde5d6ec7266e1d25d" @@ -5712,6 +5725,17 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-primitive" "1.0.3" +"@radix-ui/react-avatar@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz#de9a5349d9e3de7bbe990334c4d2011acbbb9623" + integrity sha512-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-layout-effect" "1.0.1" + "@radix-ui/react-collection@1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.2.tgz#d50da00bfa2ac14585319efdbbb081d4c5a29a97" @@ -5762,6 +5786,27 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-dialog@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz#71657b1b116de6c7a0b03242d7d43e01062c7300" + integrity sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-dismissable-layer" "1.0.5" + "@radix-ui/react-focus-guards" "1.0.1" + "@radix-ui/react-focus-scope" "1.0.4" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-portal" "1.0.4" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-use-controllable-state" "1.0.1" + aria-hidden "^1.1.1" + react-remove-scroll "2.5.5" + "@radix-ui/react-dialog@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.0.4.tgz#06bce6c16bb93eb36d7a8589e665a20f4c1c52c1" @@ -5852,6 +5897,16 @@ "@radix-ui/react-primitive" "1.0.3" "@radix-ui/react-use-callback-ref" "1.0.1" +"@radix-ui/react-focus-scope@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz#2ac45fce8c5bb33eb18419cdc1905ef4f1906525" + integrity sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-id@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.0.tgz#8d43224910741870a45a8c9d092f25887bb6d11e" @@ -11230,6 +11285,7 @@ draft-js-utils@^1.4.0: "draft-js@https://github.com/mattkrick/draft-js/tarball/559a21968370c4944511657817d601a6c4ade0f6": version "0.10.5" + uid "025fddba56f21aaf3383aee778e0b17025c9a7bc" resolved "https://github.com/mattkrick/draft-js/tarball/559a21968370c4944511657817d601a6c4ade0f6#025fddba56f21aaf3383aee778e0b17025c9a7bc" dependencies: fbjs "^0.8.15"