overleaf/services/web/frontend/js/features/share-project-modal/components/restricted-link-sharing/share-project-modal.tsx
roo hutton 2dcf87e3f6 [web] Share modal shows downgraded editors (#20015)
* add hasBeenDowngraded prop for EditMember

* reduce padding on share modal collab row, add prompt to hasBeenDowngraded Select

* share modal select styling tweaks to allow for inline warning icon

* always show editor limit subtitle when in downgraded state

* add AccessLevelsChanged warning, tweak owner row styling

* conditionally set hasBeenDowngraded prop. make invited member row styling more consistent between warning/enforcement

* add an info state for access level changed notification

* add notification for lost edit access on collaborator share modal, TSify SendInvitesNotice

* fix member privilege alignment in collaborator share modal

* show blue upgrade CTA when some pending editors have been resolved

* automatically show share modal to owners when has pending editors or is over collab limit

* only show lost edit access warning in read-only share modal to pending editors

---------

Co-authored-by: Thomas <thomas-@users.noreply.github.com>
GitOrigin-RevId: e3b88052a48b8f598299ffc55b7c24cb793da151
2024-08-27 08:04:49 +00:00

180 lines
4.6 KiB
TypeScript

import React, {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from 'react'
import ShareProjectModalContent from './share-project-modal-content'
import { useProjectContext } from '@/shared/context/project-context'
import { useSplitTestContext } from '@/shared/context/split-test-context'
import { sendMB } from '@/infrastructure/event-tracking'
import { ProjectContextUpdateValue } from '@/shared/context/types/project-context'
import { useEditorContext } from '@/shared/context/editor-context'
import customLocalStorage from '@/infrastructure/local-storage'
type ShareProjectContextValue = {
updateProject: (project: ProjectContextUpdateValue) => void
monitorRequest: <T extends Promise<unknown>>(request: () => T) => T
inFlight: boolean
setInFlight: React.Dispatch<
React.SetStateAction<ShareProjectContextValue['inFlight']>
>
error: string | undefined
setError: React.Dispatch<
React.SetStateAction<ShareProjectContextValue['error']>
>
}
const SHOW_MODAL_COOLDOWN_PERIOD = 24 * 60 * 60 * 1000 // 24 hours
const ShareProjectContext = createContext<ShareProjectContextValue | undefined>(
undefined
)
export function useShareProjectContext() {
const context = useContext(ShareProjectContext)
if (!context) {
throw new Error(
'useShareProjectContext is only available inside ShareProjectProvider'
)
}
return context
}
type ShareProjectModalProps = {
handleHide: () => void
show: boolean
handleOpen: () => void
animation?: boolean
}
const ShareProjectModal = React.memo(function ShareProjectModal({
handleHide,
show,
handleOpen,
animation = true,
}: ShareProjectModalProps) {
const [inFlight, setInFlight] =
useState<ShareProjectContextValue['inFlight']>(false)
const [error, setError] = useState<ShareProjectContextValue['error']>()
const project = useProjectContext()
const { isProjectOwner } = useEditorContext()
const { splitTestVariants } = useSplitTestContext()
// split test: link-sharing-warning
// show the new share modal if project owner
// is over collaborator limit or has pending editors (once every 24 hours)
useEffect(() => {
const hasExceededCollaboratorLimit = () => {
if (!isProjectOwner || !project.features) {
return false
}
if (project.features.collaborators === -1) {
return false
}
return (
project.members.filter(member => member.privileges === 'readAndWrite')
.length > (project.features.collaborators ?? 1) ||
project.members.some(member => member.pendingEditor)
)
}
if (hasExceededCollaboratorLimit()) {
const localStorageKey = `last-shown-share-modal.${project._id}`
const lastShownShareModalTime =
customLocalStorage.getItem(localStorageKey)
if (
!lastShownShareModalTime ||
lastShownShareModalTime + SHOW_MODAL_COOLDOWN_PERIOD < Date.now()
) {
handleOpen()
customLocalStorage.setItem(localStorageKey, Date.now())
}
}
}, [project, isProjectOwner, handleOpen])
// send tracking event when the modal is opened
useEffect(() => {
if (show) {
sendMB('share-modal-opened', {
splitTestVariant: splitTestVariants['null-test-share-modal'],
project_id: project._id,
})
}
}, [splitTestVariants, project._id, show])
// reset error when the modal is opened
useEffect(() => {
if (show) {
setError(undefined)
}
}, [show])
// close the modal if not in flight
const cancel = useCallback(() => {
if (!inFlight) {
handleHide()
}
}, [handleHide, inFlight])
// update `error` and `inFlight` while sending a request
const monitorRequest = useCallback(request => {
setError(undefined)
setInFlight(true)
const promise = request()
promise.catch((error: { data?: Record<string, string> }) => {
setError(
error.data?.errorReason ||
error.data?.error ||
'generic_something_went_wrong'
)
})
promise.finally(() => {
setInFlight(false)
})
return promise
}, [])
// merge the new data with the old project data
const updateProject = useCallback(
data => Object.assign(project, data),
[project]
)
if (!project) {
return null
}
return (
<ShareProjectContext.Provider
value={{
updateProject,
monitorRequest,
inFlight,
setInFlight,
error,
setError,
}}
>
<ShareProjectModalContent
animation={animation}
cancel={cancel}
error={error}
inFlight={inFlight}
show={show}
/>
</ShareProjectContext.Provider>
)
})
export default ShareProjectModal