-
Notifications
You must be signed in to change notification settings - Fork 336
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: managing teams #9285
feat: managing teams #9285
Changes from 13 commits
6225717
d97058a
97fed26
5919989
8e6020b
7cd6631
7496901
6d2bac8
a73feae
1e45eb4
8b84345
5ec917c
7e568eb
65f1b99
c14c0ec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
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`) | ||
} | ||
|
||
const labelStyles = `text-left text-sm font-semibold mb-3 text-slate-600` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 these vars are just used in one place, so I find it kinda confusing that they're defined here rather than inline with the rest of the classNames There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree! I'll update |
||
const fieldsetStyles = `mx-0 mb-6 flex flex-col w-full p-0` | ||
|
||
return ( | ||
<Dialog isOpen={isOpen} onClose={onClose}> | ||
<DialogContent className='z-10'> | ||
<DialogTitle className='mb-4'>Delete Team</DialogTitle> | ||
|
||
<fieldset className={fieldsetStyles}> | ||
<label className={labelStyles}> | ||
Please type your team name to confirm. <b>This action can't be undone.</b> | ||
</label> | ||
<Input | ||
autoFocus | ||
onChange={(e) => { | ||
e.preventDefault() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 |
||
if (e.target.value === teamName) setTypedTeamName(true) | ||
else setTypedTeamName(false) | ||
}} | ||
placeholder={teamName} | ||
/> | ||
{error && ( | ||
<div className='mt-2 text-sm font-semibold text-tomato-500'>{error.message}</div> | ||
)} | ||
</fieldset> | ||
|
||
<DialogActions> | ||
<FlatPrimaryButton size='medium' onClick={handleDeleteTeam} disabled={!typedTeamName}> | ||
I understand the consequences, delete this team | ||
</FlatPrimaryButton> | ||
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton> | ||
</DialogActions> | ||
</DialogContent> | ||
</Dialog> | ||
) | ||
} | ||
|
||
export default DeleteTeamDialog |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import React, {Suspense} from 'react' | ||
import DeleteTeamDialog from './DeleteTeamDialog' | ||
import {Loader} from '../utils/relay/renderLoader' | ||
|
||
interface Props { | ||
onClose: () => void | ||
onDeleteTeam: (teamId: string) => void | ||
teamId: string | ||
teamName: string | ||
teamOrgId: string | ||
} | ||
|
||
const DeleteTeamDialogRoot = (props: Props) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. -1 I know you didn't write this, but I think we should delete this file. The convention is using Root components for lazy loading where we have a separate query. As there's no query here, we should delete it and just use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh for sure, nice catch |
||
const {onClose, onDeleteTeam, teamId, teamName, teamOrgId} = props | ||
|
||
return ( | ||
<Suspense fallback={<Loader />}> | ||
<DeleteTeamDialog | ||
onDeleteTeam={onDeleteTeam} | ||
isOpen={true} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 rather than There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you, I done did it this way :) |
||
onClose={onClose} | ||
teamId={teamId} | ||
teamName={teamName} | ||
teamOrgId={teamOrgId} | ||
/> | ||
</Suspense> | ||
) | ||
} | ||
|
||
export default DeleteTeamDialogRoot |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
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 withMutationProps, {WithMutationProps} from '../utils/relay/withMutationProps' | ||
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 extends WithMutationProps { | ||
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, | ||
submitting, | ||
submitMutation, | ||
onError, | ||
onCompleted, | ||
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 {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 ( | ||
<> | ||
<Menu ariaLabel={'Select your action'} {...menuProps}> | ||
{!isOrgAdmin && <MenuItem label='Promote to Org Admin' onClick={setRole('ORG_ADMIN')} />} | ||
{!isOrgAdmin && !isBillingLeader && ( | ||
<MenuItem label='Promote to Billing Leader' onClick={setRole('BILLING_LEADER')} /> | ||
)} | ||
{isOrgAdmin && !isSelf && ( | ||
<MenuItem label='Change to Billing Leader' onClick={setRole('BILLING_LEADER')} /> | ||
)} | ||
{((role && !isSelf) || canRemoveSelf) && ( | ||
<MenuItem label={`Remove ${roleName} role`} onClick={setRole(null)} /> | ||
)} | ||
{canRemoveSelf && <MenuItem label='Leave Organization' onClick={toggleLeave} />} | ||
{!isSelf && <MenuItem label='Remove from Organization' onClick={toggleRemove} />} | ||
{isSelf && !canRemoveSelf && <MenuItem label='Contact [email protected] to be removed' />} | ||
</Menu> | ||
</> | ||
) | ||
} | ||
|
||
export default withMutationProps(OrgAdminActionMenu) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 we're normally now using the hook There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for helping me climb the learning curve. I refactored this |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<Menu ariaLabel={'Select your action'} {...menuProps}> | ||
{isViewerTeamAdmin && (!isSelf || !isViewerLead) && ( | ||
<MenuItem | ||
label={<StyledLabel>Promote {preferredName} to Team Lead</StyledLabel>} | ||
key='promote' | ||
onClick={togglePromote} | ||
/> | ||
)} | ||
{isViewerTeamAdmin && !isSelf && ( | ||
<MenuItem | ||
label={<StyledLabel>Remove {preferredName} from Team</StyledLabel>} | ||
onClick={toggleRemove} | ||
/> | ||
)} | ||
</Menu> | ||
) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pwa-chrome is no longer supported on my version of VS Code, the type is now simply
chrome