Files
Verso/services/web/frontend/js/features/share-project-modal/components/invite.tsx
T
ilkin-overleaf 6ccbefc3f8 Merge pull request #32941 from overleaf/ii-share-modal-manage-access
[web] Share modal redesign: invited people screen

GitOrigin-RevId: bf7f83dccc245e41eca8087fc27c1b411f025b0d
2026-04-28 08:06:42 +00:00

195 lines
6.1 KiB
TypeScript

import { useCallback } from 'react'
import { useShareProjectContext } from './share-project-modal'
import { useTranslation } from 'react-i18next'
import MemberPrivileges from './member-privileges'
import { resendInvite, revokeInvite } from '../utils/api'
import { useProjectContext } from '@/shared/context/project-context'
import { sendMB } from '@/infrastructure/event-tracking'
import OLRow from '@/shared/components/ol/ol-row'
import OLCol from '@/shared/components/ol/ol-col'
import OLTooltip from '@/shared/components/ol/ol-tooltip'
import OLButton from '@/shared/components/ol/ol-button'
import OLBadge from '@/shared/components/ol/ol-badge'
import OLDropdownMenuItem from '@/shared/components/ol/ol-dropdown-menu-item'
import {
Dropdown,
DropdownMenu,
DropdownToggle,
} from '@/shared/components/dropdown/dropdown-menu'
import ShareProjectModalRow from '@/features/share-project-modal/components/share-project-modal-row'
import MaterialIcon from '@/shared/components/material-icon'
import { ProjectMember } from '@/shared/context/types/project-metadata'
import { useFeatureFlag } from '@/shared/context/split-test-context'
export default function Invite({
invite,
isProjectOwner,
}: {
invite: ProjectMember
isProjectOwner: boolean
}) {
const isSharingUpdatesEnabled = useFeatureFlag('sharing-updates')
const { t } = useTranslation()
return isSharingUpdatesEnabled ? (
<ShareProjectModalRow>
<div className="d-inline-flex align-items-center h5 m-0 gap-2">
<MaterialIcon type="person" unfilled />
<div className="px-2">{invite.email}</div>
<OLBadge bg="light" text="dark">
{t('pending_invite')}
</OLBadge>
</div>
{isProjectOwner ? (
<Dropdown align="end" onSelect={() => {}}>
<DropdownToggle
variant="ghost"
className="d-flex align-items-center gap-2 no-default-caret"
>
<MemberPrivileges privileges={invite.privileges} />
<MaterialIcon type="keyboard_arrow_down" />
</DropdownToggle>
<DropdownMenu>
<ResendInvite invite={invite} />
<RevokeInvite invite={invite} />
</DropdownMenu>
</Dropdown>
) : (
<div className="h5 m-0 px-4 fw-semibold">
<div className="form-control-plaintext border-0">
<MemberPrivileges privileges={invite.privileges} />
</div>
</div>
)}
</ShareProjectModalRow>
) : (
<OLRow className="project-invite">
<OLCol xs={8}>
<div>{invite.email}</div>
<div className="small">
{t('invite_not_accepted')}
.&nbsp;
{isProjectOwner && <ResendInvite invite={invite} />}
</div>
</OLCol>
<OLCol xs={3} className="text-end">
<MemberPrivileges privileges={invite.privileges} />
</OLCol>
{isProjectOwner && (
<OLCol xs={1} className="text-center">
<RevokeInvite invite={invite} />
</OLCol>
)}
</OLRow>
)
}
function ResendInvite({ invite }: { invite: ProjectMember }) {
const isSharingUpdatesEnabled = useFeatureFlag('sharing-updates')
const { t } = useTranslation()
const { monitorRequest, setError, inFlight, setSuccessActionMessage } =
useShareProjectContext()
const { projectId } = useProjectContext()
// const buttonRef = useRef(null)
//
const handleClick = useCallback(
() =>
monitorRequest(() => resendInvite(projectId, invite))
.then(() => setSuccessActionMessage(t('invite_resent')))
.catch(error => {
if (error?.response?.status === 404) {
setError('invite_expired')
}
if (error?.response?.status === 429) {
setError('invite_resend_limit_hit')
}
})
.finally(() => {
// NOTE: disabled as react-bootstrap v0.33.1 isn't forwarding the ref to the `button`
// if (buttonRef.current) {
// buttonRef.current.blur()
// }
if (document.activeElement) {
;(document.activeElement as HTMLElement).blur()
}
}),
[invite, monitorRequest, projectId, setError, setSuccessActionMessage, t]
)
return isSharingUpdatesEnabled ? (
<OLDropdownMenuItem
as="button"
leadingIcon={<MaterialIcon type="mail" unfilled />}
onClick={handleClick}
disabled={inFlight}
>
{t('resend_invite')}
</OLDropdownMenuItem>
) : (
<OLButton
variant="link"
className="btn-inline-link"
onClick={handleClick}
disabled={inFlight}
// ref={buttonRef}
>
{t('resend')}
</OLButton>
)
}
function RevokeInvite({ invite }: { invite: ProjectMember }) {
const isSharingUpdatesEnabled = useFeatureFlag('sharing-updates')
const { t } = useTranslation()
const { monitorRequest, setSuccessActionMessage } = useShareProjectContext()
const { projectId, project, updateProject } = useProjectContext()
const { invites, members } = project || {}
function handleClick(event: React.MouseEvent) {
event.preventDefault()
monitorRequest(() => revokeInvite(projectId, invite)).then(() => {
const updatedInvites =
invites?.filter(existing => existing !== invite) || []
updateProject({
invites: updatedInvites,
})
sendMB('collaborator-invite-revoked', {
project_id: projectId,
current_invites_amount: updatedInvites.length,
current_collaborators_amount: members?.length || 0,
})
setSuccessActionMessage(t('invite_revoked'))
})
}
return isSharingUpdatesEnabled ? (
<OLDropdownMenuItem
as="button"
leadingIcon={<MaterialIcon type="block" unfilled />}
variant="danger"
onClick={handleClick}
>
{t('revoke_invite')}
</OLDropdownMenuItem>
) : (
<OLTooltip
id="revoke-invite"
description={t('revoke_invite')}
overlayProps={{ placement: 'bottom' }}
>
<OLButton
variant="link"
onClick={handleClick}
aria-label={t('revoke')}
className="btn-inline-link text-decoration-none"
>
<MaterialIcon type="clear" />
</OLButton>
</OLTooltip>
)
}