import { useState, useEffect, useMemo } from 'react' import PropTypes from 'prop-types' import { Trans, useTranslation } from 'react-i18next' import { useShareProjectContext } from './share-project-modal' import TransferOwnershipModal from './transfer-ownership-modal' import { removeMemberFromProject, updateMember } from '../utils/api' import { useProjectContext } from '@/shared/context/project-context' import { sendMB } from '@/infrastructure/event-tracking' import { Select } from '@/shared/components/select' import type { ProjectContextMember } from '@/shared/context/types/project-context' import { PermissionsLevel } from '@/features/ide-react/types/permissions' import { linkSharingEnforcementDate } from '../utils/link-sharing' import OLButton from '@/features/ui/components/ol/ol-button' import OLFormGroup from '@/features/ui/components/ol/ol-form-group' import OLCol from '@/features/ui/components/ol/ol-col' import MaterialIcon from '@/shared/components/material-icon' import getMeta from '@/utils/meta' import { useUserContext } from '@/shared/context/user-context' import { isSplitTestEnabled } from '@/utils/splitTestUtils' import { upgradePlan } from '@/main/account-upgrade' type PermissionsOption = PermissionsLevel | 'removeAccess' | 'downgraded' type EditMemberProps = { member: ProjectContextMember hasExceededCollaboratorLimit: boolean hasBeenDowngraded: boolean canAddCollaborators: boolean isReviewerOnFreeProject?: boolean } type Privilege = { key: PermissionsOption label: string } export default function EditMember({ member, hasExceededCollaboratorLimit, hasBeenDowngraded, canAddCollaborators, isReviewerOnFreeProject, }: EditMemberProps) { const [privileges, setPrivileges] = useState( member.privileges ) const [confirmingOwnershipTransfer, setConfirmingOwnershipTransfer] = useState(false) const [privilegeChangePending, setPrivilegeChangePending] = useState(false) const { t } = useTranslation() // update the local state if the member's privileges change externally useEffect(() => { setPrivileges(member.privileges) }, [member.privileges]) const { updateProject, monitorRequest } = useShareProjectContext() const { _id: projectId, members, invites } = useProjectContext() const user = useUserContext() // Immediately commit this change if it's lower impact (eg. editor > viewer) // but show a confirmation button for removing access function handlePrivilegeChange(newPrivileges: PermissionsOption) { sendMB('collaborator-role-change', { previousMode: member.privileges, newMode: newPrivileges, ownerId: user.id, }) setPrivileges(newPrivileges) if (newPrivileges !== 'removeAccess') { commitPrivilegeChange(newPrivileges) } else { setPrivilegeChangePending(true) } } function shouldWarnMember() { return ( hasExceededCollaboratorLimit && ['readAndWrite', 'review'].includes(privileges) ) } function commitPrivilegeChange(newPrivileges: PermissionsOption) { setPrivileges(newPrivileges) setPrivilegeChangePending(false) if (newPrivileges === 'owner') { setConfirmingOwnershipTransfer(true) } else if (newPrivileges === 'removeAccess') { monitorRequest(() => removeMemberFromProject(projectId, member)).then( () => { const updatedMembers = members.filter(existing => existing !== member) updateProject({ members: updatedMembers, }) sendMB('collaborator-removed', { project_id: projectId, current_collaborators_amount: updatedMembers.length, current_invites_amount: invites.length, }) } ) } else if ( newPrivileges === 'readAndWrite' || newPrivileges === 'review' || newPrivileges === 'readOnly' ) { monitorRequest(() => updateMember(projectId, member, { privilegeLevel: newPrivileges, }) ).then(() => { updateProject({ members: members.map(item => item._id === member._id ? { ...item, newPrivileges } : item ), }) }) } } if (confirmingOwnershipTransfer) { return ( { setConfirmingOwnershipTransfer(false) setPrivileges(member.privileges) }} /> ) } const confirmRemoval = privileges !== member.privileges && privilegeChangePending return (
{ e.preventDefault() if (privilegeChangePending) { commitPrivilegeChange(privileges) } }} >
{member.email} {member.pendingEditor && (
{t('view_only_downgraded')}
)} {member.pendingReviewer && (
{t('view_only_reviewer_downgraded')}
)} {shouldWarnMember() && (
{t('will_lose_edit_access_on_date', { date: linkSharingEnforcementDate, })}
)} {isReviewerOnFreeProject && (
upgradePlan('track-changes')} />, ]} />
)}
{confirmRemoval && ( setPrivileges(member.privileges)} /> )}
{hasBeenDowngraded && !confirmRemoval && ( )} { if (value) { handlePrivilegeChange(value.key) } }} hasBeenDowngraded={hasBeenDowngraded && !confirmRemoval} canAddCollaborators={canAddCollaborators} />
) } EditMember.propTypes = { member: PropTypes.shape({ _id: PropTypes.string.isRequired, email: PropTypes.string.isRequired, privileges: PropTypes.string.isRequired, }), hasExceededCollaboratorLimit: PropTypes.bool.isRequired, canAddCollaborators: PropTypes.bool.isRequired, } type SelectPrivilegeProps = { value: string handleChange: (item: Privilege | null | undefined) => void hasBeenDowngraded: boolean canAddCollaborators: boolean } function SelectPrivilege({ value, handleChange, hasBeenDowngraded, canAddCollaborators, }: SelectPrivilegeProps) { const { t } = useTranslation() const { features } = useProjectContext() const privileges = useMemo( (): Privilege[] => getMeta('ol-isReviewerRoleEnabled') ? [ { key: 'owner', label: t('make_owner') }, { key: 'readAndWrite', label: t('editor') }, { key: 'review', label: t('reviewer') }, { key: 'readOnly', label: t('viewer') }, { key: 'removeAccess', label: t('remove_access') }, ] : [ { key: 'owner', label: t('make_owner') }, { key: 'readAndWrite', label: t('editor') }, { key: 'readOnly', label: t('viewer') }, { key: 'removeAccess', label: t('remove_access') }, ], [t] ) const downgradedPseudoPrivilege: Privilege = { key: 'downgraded', label: t('select_access_level'), } function getPrivilegeSubtitle(privilege: PermissionsOption) { if (!['readAndWrite', 'review'].includes(privilege)) { return '' } if (hasBeenDowngraded) { if (isSplitTestEnabled('reviewer-role')) { return t('limited_to_n_collaborators_per_project', { count: features.collaborators, }) } else { return t('limited_to_n_editors', { count: features.collaborators }) } } else if ( !canAddCollaborators && !['readAndWrite', 'review'].includes(value) ) { if (isSplitTestEnabled('reviewer-role')) { return t('limited_to_n_collaborators_per_project', { count: features.collaborators, }) } else { return t('limited_to_n_editors_per_project', { count: features.collaborators, }) } } else { return '' } } function isPrivilegeDisabled(privilege: PermissionsOption) { return ( !canAddCollaborators && ['readAndWrite', 'review'].includes(privilege) && (hasBeenDowngraded || !['readAndWrite', 'review'].includes(value)) ) } return (