| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345 |
- import { useState, useEffect, useMemo } from 'react'
- 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 { ProjectMember } from '@/shared/context/types/project-metadata'
- 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 { useUserContext } from '@/shared/context/user-context'
- import { upgradePlan } from '@/main/account-upgrade'
- type PermissionsOption = PermissionsLevel | 'removeAccess' | 'downgraded'
- type EditMemberProps = {
- member: ProjectMember
- 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<PermissionsOption>(
- 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 { monitorRequest } = useShareProjectContext()
- const { projectId, project, updateProject } = useProjectContext()
- const { members, invites } = project || {}
- 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 || 0,
- })
- }
- )
- } 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 (
- <TransferOwnershipModal
- member={member}
- cancel={() => {
- setConfirmingOwnershipTransfer(false)
- setPrivileges(member.privileges)
- }}
- />
- )
- }
- const confirmRemoval =
- privileges !== member.privileges && privilegeChangePending
- return (
- <form
- id="share-project-form"
- onSubmit={e => {
- e.preventDefault()
- if (privilegeChangePending) {
- commitPrivilegeChange(privileges)
- }
- }}
- >
- <OLFormGroup className="project-member row">
- <OLCol xs={8}>
- <div className="project-member-email-icon">
- <MaterialIcon
- type={
- shouldWarnMember() ||
- member.pendingEditor ||
- member.pendingReviewer
- ? 'warning'
- : 'person'
- }
- className={
- shouldWarnMember() ||
- member.pendingEditor ||
- member.pendingReviewer
- ? 'project-member-warning'
- : undefined
- }
- />
- <div className="email-warning">
- {member.email}
- {member.pendingEditor && (
- <div className="subtitle">{t('view_only_downgraded')}</div>
- )}
- {member.pendingReviewer && (
- <div className="subtitle">
- {t('view_only_reviewer_downgraded')}
- </div>
- )}
- {shouldWarnMember() && (
- <div className="subtitle">
- {t('will_lose_edit_access_on_date', {
- date: linkSharingEnforcementDate,
- })}
- </div>
- )}
- {isReviewerOnFreeProject && (
- <div className="small">
- <Trans
- i18nKey="comment_only_upgrade_to_enable_track_changes"
- components={[
- // eslint-disable-next-line react/jsx-key
- <OLButton
- variant="link"
- className="btn-inline-link"
- onClick={() => upgradePlan('track-changes')}
- />,
- ]}
- />
- </div>
- )}
- </div>
- </div>
- </OLCol>
- <OLCol xs={4} className="project-member-actions">
- {confirmRemoval && (
- <ChangePrivilegesActions
- handleReset={() => setPrivileges(member.privileges)}
- />
- )}
- <div className="project-member-select">
- {hasBeenDowngraded && !confirmRemoval && (
- <MaterialIcon type="warning" className="project-member-warning" />
- )}
- <SelectPrivilege
- value={privileges}
- handleChange={value => {
- if (value) {
- handlePrivilegeChange(value.key)
- }
- }}
- hasBeenDowngraded={hasBeenDowngraded && !confirmRemoval}
- canAddCollaborators={canAddCollaborators}
- />
- </div>
- </OLCol>
- </OLFormGroup>
- </form>
- )
- }
- 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[] =>
- features.trackChangesVisible
- ? [
- { 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') },
- ],
- [features.trackChangesVisible, t]
- )
- const downgradedPseudoPrivilege: Privilege = {
- key: 'downgraded',
- label: t('select_access_level'),
- }
- function getPrivilegeSubtitle(privilege: PermissionsOption) {
- if (!['readAndWrite', 'review'].includes(privilege)) {
- return ''
- }
- if (
- hasBeenDowngraded ||
- (!canAddCollaborators && !['readAndWrite', 'review'].includes(value))
- ) {
- return t('limited_to_n_collaborators_per_project', {
- count: features.collaborators,
- })
- } else {
- return ''
- }
- }
- function isPrivilegeDisabled(privilege: PermissionsOption) {
- return (
- !canAddCollaborators &&
- ['readAndWrite', 'review'].includes(privilege) &&
- (hasBeenDowngraded || !['readAndWrite', 'review'].includes(value))
- )
- }
- return (
- <Select
- items={privileges}
- itemToKey={item => item.key}
- itemToString={item => (item ? item.label : '')}
- itemToSubtitle={item => (item ? getPrivilegeSubtitle(item.key) : '')}
- itemToDisabled={item => (item ? isPrivilegeDisabled(item.key) : false)}
- defaultItem={privileges.find(item => item.key === value)}
- selected={
- hasBeenDowngraded
- ? downgradedPseudoPrivilege
- : privileges.find(item => item.key === value)
- }
- name="privileges"
- onSelectedItemChanged={handleChange}
- selectedIcon
- />
- )
- }
- type ChangePrivilegesActionsProps = {
- handleReset: React.ComponentProps<typeof OLButton>['onClick']
- }
- function ChangePrivilegesActions({
- handleReset,
- }: ChangePrivilegesActionsProps) {
- const { t } = useTranslation()
- return (
- <div className="text-center">
- <OLButton type="submit" size="sm" variant="primary">
- {t('change_or_cancel-change')}
- </OLButton>
- <div className="text-sm">
- {t('change_or_cancel-or')}
-
- <OLButton
- variant="link"
- className="btn-inline-link"
- onClick={handleReset}
- >
- {t('change_or_cancel-cancel')}
- </OLButton>
- </div>
- </div>
- )
- }
|