Merge pull request #4245 from overleaf/msm-extract-project-context

React `project-context`

GitOrigin-RevId: 6a23437d6e6a328ff5854622ff903d348db1f8b8
This commit is contained in:
Miguel Serrano 2021-06-25 10:13:17 +02:00 committed by Copybot
parent d10294a58a
commit b7802674d5
21 changed files with 177 additions and 120 deletions

View file

@ -10,7 +10,7 @@ import PropTypes from 'prop-types'
import { v4 as uuid } from 'uuid'
import { useApplicationContext } from '../../../shared/context/application-context'
import { useEditorContext } from '../../../shared/context/editor-context'
import { useProjectContext } from '../../../shared/context/project-context'
import { getJSON, postJSON } from '../../../infrastructure/fetch-json'
import { appendMessage, prependMessages } from '../utils/message-list-appender'
import useBrowserWindow from '../../../shared/hooks/use-browser-window'
@ -120,8 +120,8 @@ export function ChatProvider({ children }) {
const { user } = useApplicationContext({
user: PropTypes.shape({ id: PropTypes.string.isRequired }),
})
const { projectId } = useEditorContext({
projectId: PropTypes.string.isRequired,
const { _id: projectId } = useProjectContext({
_id: PropTypes.string.isRequired,
})
const { chatIsOpen } = useLayoutContext({ chatIsOpen: PropTypes.bool })

View file

@ -5,16 +5,20 @@ import { useEditorContext } from '../../../shared/context/editor-context'
import { useChatContext } from '../../chat/context/chat-context'
import { useLayoutContext } from '../../../shared/context/layout-context'
import { useApplicationContext } from '../../../shared/context/application-context'
import { useProjectContext } from '../../../shared/context/project-context'
const applicationContextPropTypes = {
user: PropTypes.object,
}
const projectContextPropTypes = {
name: PropTypes.string.isRequired,
}
const editorContextPropTypes = {
cobranding: PropTypes.object,
loading: PropTypes.bool,
isRestrictedTokenMember: PropTypes.bool,
projectName: PropTypes.string.isRequired,
renameProject: PropTypes.func.isRequired,
isProjectOwner: PropTypes.bool,
}
@ -43,11 +47,12 @@ const EditorNavigationToolbarRoot = React.memo(
}) {
const { user } = useApplicationContext(applicationContextPropTypes)
const { name: projectName } = useProjectContext(projectContextPropTypes)
const {
cobranding,
loading,
isRestrictedTokenMember,
projectName,
renameProject,
isProjectOwner,
} = useEditorContext(editorContextPropTypes)

View file

@ -5,7 +5,7 @@ import { Trans, useTranslation } from 'react-i18next'
import Icon from '../../../shared/components/icon'
import { formatTime, relativeDate } from '../../utils/format-date'
import { postJSON } from '../../../infrastructure/fetch-json'
import { useEditorContext } from '../../../shared/context/editor-context'
import { useProjectContext } from '../../../shared/context/project-context'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import useAbortController from '../../../shared/hooks/use-abort-controller'
@ -32,8 +32,8 @@ function shortenedUrl(url) {
}
export default function FileViewHeader({ file, storeReferencesKeys }) {
const { projectId } = useEditorContext({
projectId: PropTypes.string.isRequired,
const { _id: projectId } = useProjectContext({
_id: PropTypes.string.isRequired,
})
const { t } = useTranslation()

View file

@ -1,9 +1,9 @@
import PropTypes from 'prop-types'
import { useEditorContext } from '../../../shared/context/editor-context'
import { useProjectContext } from '../../../shared/context/project-context'
export default function FileViewImage({ fileName, fileId, onLoad, onError }) {
const { projectId } = useEditorContext({
projectId: PropTypes.string.isRequired,
const { _id: projectId } = useProjectContext({
_id: PropTypes.string.isRequired,
})
return (

View file

@ -1,12 +1,12 @@
import { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { useEditorContext } from '../../../shared/context/editor-context'
import { useProjectContext } from '../../../shared/context/project-context'
const MAX_FILE_SIZE = 2 * 1024 * 1024
export default function FileViewText({ file, onLoad, onError }) {
const { projectId } = useEditorContext({
projectId: PropTypes.string.isRequired,
const { _id: projectId } = useProjectContext({
_id: PropTypes.string.isRequired,
})
const [textPreview, setTextPreview] = useState('')

View file

@ -7,7 +7,7 @@ import OutlineRoot from './outline-root'
import Icon from '../../../shared/components/icon'
import localStorage from '../../../infrastructure/local-storage'
import withErrorBoundary from '../../../infrastructure/error-boundary'
import { useEditorContext } from '../../../shared/context/editor-context'
import { useProjectContext } from '../../../shared/context/project-context'
const OutlinePane = React.memo(function OutlinePane({
isTexFile,
@ -19,8 +19,8 @@ const OutlinePane = React.memo(function OutlinePane({
}) {
const { t } = useTranslation()
const { projectId } = useEditorContext({
projectId: PropTypes.string.isRequired,
const { _id: projectId } = useProjectContext({
_id: PropTypes.string.isRequired,
})
const storageKey = `file_outline.expanded.${projectId}`

View file

@ -2,14 +2,12 @@ import { useState, useMemo } from 'react'
import { useTranslation, Trans } from 'react-i18next'
import { Form, FormGroup, FormControl, Button } from 'react-bootstrap'
import { useMultipleSelection } from 'downshift'
import {
useProjectContext,
useShareProjectContext,
} from './share-project-modal'
import { useShareProjectContext } from './share-project-modal'
import SelectCollaborators from './select-collaborators'
import { resendInvite, sendInvite } from '../utils/api'
import { useUserContacts } from '../hooks/use-user-contacts'
import useIsMounted from '../../../shared/hooks/use-is-mounted'
import { useProjectContext } from '../../../shared/context/project-context'
export default function AddCollaborators() {
const [privileges, setPrivileges] = useState('readAndWrite')

View file

@ -1,10 +1,7 @@
import { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { Trans, useTranslation } from 'react-i18next'
import {
useProjectContext,
useShareProjectContext,
} from './share-project-modal'
import { useShareProjectContext } from './share-project-modal'
import Icon from '../../../shared/components/icon'
import TransferOwnershipModal from './transfer-ownership-modal'
import {
@ -17,6 +14,7 @@ import {
Tooltip,
} from 'react-bootstrap'
import { removeMemberFromProject, updateMember } from '../utils/api'
import { useProjectContext } from '../../../shared/context/project-context'
export default function EditMember({ member }) {
const [privileges, setPrivileges] = useState(member.privileges)

View file

@ -1,14 +1,12 @@
import { useCallback } from 'react'
import PropTypes from 'prop-types'
import {
useProjectContext,
useShareProjectContext,
} from './share-project-modal'
import { useShareProjectContext } from './share-project-modal'
import Icon from '../../../shared/components/icon'
import { Button, Col, Row, OverlayTrigger, Tooltip } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import MemberPrivileges from './member-privileges'
import { resendInvite, revokeInvite } from '../utils/api'
import { useProjectContext } from '../../../shared/context/project-context'
export default function Invite({ invite, isAdmin }) {
return (

View file

@ -3,12 +3,10 @@ import PropTypes from 'prop-types'
import { Button, Col, OverlayTrigger, Row, Tooltip } from 'react-bootstrap'
import { Trans } from 'react-i18next'
import Icon from '../../../shared/components/icon'
import {
useProjectContext,
useShareProjectContext,
} from './share-project-modal'
import { useShareProjectContext } from './share-project-modal'
import { setProjectAccessLevel } from '../utils/api'
import CopyLink from '../../../shared/components/copy-link'
import { useProjectContext } from '../../../shared/context/project-context'
export default function LinkSharing() {
const [inflight, setInflight] = useState(false)

View file

@ -1,4 +1,4 @@
import { useProjectContext } from './share-project-modal'
import { useProjectContext } from '../../../shared/context/project-context'
import { Col, Row } from 'react-bootstrap'
import { Trans } from 'react-i18next'

View file

@ -1,7 +1,7 @@
import { Col, Row } from 'react-bootstrap'
import PropTypes from 'prop-types'
import { Trans } from 'react-i18next'
import { useProjectContext } from './share-project-modal'
import { useProjectContext } from '../../../shared/context/project-context'
export default function SendInvitesNotice() {
const project = useProjectContext()

View file

@ -1,8 +1,8 @@
import { useMemo } from 'react'
import { Row } from 'react-bootstrap'
import { useProjectContext } from './share-project-modal'
import AddCollaborators from './add-collaborators'
import AddCollaboratorsUpgrade from './add-collaborators-upgrade'
import { useProjectContext } from '../../../shared/context/project-context'
export default function SendInvites() {
const project = useProjectContext()

View file

@ -1,7 +1,4 @@
import {
useProjectContext,
useShareProjectContext,
} from './share-project-modal'
import { useShareProjectContext } from './share-project-modal'
import EditMember from './edit-member'
import LinkSharing from './link-sharing'
import Invite from './invite'
@ -9,6 +6,7 @@ import SendInvites from './send-invites'
import ViewMember from './view-member'
import OwnerInfo from './owner-info'
import SendInvitesNotice from './send-invites-notice'
import { useProjectContext } from '../../../shared/context/project-context'
export default function ShareModalBody() {
const { isAdmin } = useShareProjectContext()

View file

@ -7,7 +7,7 @@ import React, {
} from 'react'
import PropTypes from 'prop-types'
import ShareProjectModalContent from './share-project-modal-content'
import useScopeValue from '../../../shared/context/util/scope-value-hook'
import { useProjectContext } from '../../../shared/context/project-context'
const ShareProjectContext = createContext()
@ -35,7 +35,7 @@ export function useShareProjectContext() {
return context
}
const projectShape = PropTypes.shape({
const projectShape = {
_id: PropTypes.string.isRequired,
members: PropTypes.arrayOf(
PropTypes.shape({
@ -59,24 +59,6 @@ const projectShape = PropTypes.shape({
owner: PropTypes.shape({
email: PropTypes.string,
}),
})
const ProjectContext = createContext()
ProjectContext.Provider.propTypes = {
value: projectShape,
}
export function useProjectContext() {
const context = useContext(ProjectContext)
if (!context) {
throw new Error(
'useProjectContext is only available inside ShareProjectProvider'
)
}
return context
}
const ShareProjectModal = React.memo(function ShareProjectModal({
@ -88,7 +70,7 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
const [inFlight, setInFlight] = useState(false)
const [error, setError] = useState()
const [project, setProject] = useScopeValue('project', true)
const project = useProjectContext(projectShape)
// reset error when the modal is opened
useEffect(() => {
@ -127,12 +109,9 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
}, [])
// merge the new data with the old project data
const updateProject = useCallback(
data => {
setProject(project => Object.assign(project, data))
},
[setProject]
)
const updateProject = useCallback(data => Object.assign(project, data), [
project,
])
if (!project) {
return null
@ -150,15 +129,13 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
setError,
}}
>
<ProjectContext.Provider value={project}>
<ShareProjectModalContent
animation={animation}
cancel={cancel}
error={error}
inFlight={inFlight}
show={show}
/>
</ProjectContext.Provider>
<ShareProjectModalContent
animation={animation}
cancel={cancel}
error={error}
inFlight={inFlight}
show={show}
/>
</ShareProjectContext.Provider>
)
})

View file

@ -2,11 +2,11 @@ import { useState } from 'react'
import { Modal, Button } from 'react-bootstrap'
import { Trans } from 'react-i18next'
import PropTypes from 'prop-types'
import { useProjectContext } from './share-project-modal'
import Icon from '../../../shared/components/icon'
import { transferProjectOwnership } from '../utils/api'
import AccessibleModal from '../../../shared/components/accessible-modal'
import { reload } from '../utils/location'
import { useProjectContext } from '../../../shared/context/project-context'
export default function TransferOwnershipModal({ member, cancel }) {
const [inflight, setInflight] = useState(false)

View file

@ -9,6 +9,7 @@ import PropTypes from 'prop-types'
import useScopeValue from './util/scope-value-hook'
import useBrowserWindow from '../hooks/use-browser-window'
import { useIdeContext } from './ide-context'
import { useProjectContext } from './project-context'
export const EditorContext = createContext()
@ -27,43 +28,48 @@ EditorContext.Provider.propTypes = {
}),
hasPremiumCompile: PropTypes.bool,
loading: PropTypes.bool,
projectRootDocId: PropTypes.string,
projectId: PropTypes.string.isRequired,
projectName: PropTypes.string.isRequired,
renameProject: PropTypes.func.isRequired,
isProjectOwner: PropTypes.bool,
isRestrictedTokenMember: PropTypes.bool,
rootFolder: PropTypes.object,
rootFolder: PropTypes.shape({
children: PropTypes.arrayOf(PropTypes.shape({ type: PropTypes.string })),
}),
}),
}
export function EditorProvider({ children, settings }) {
const ide = useIdeContext()
const cobranding = useMemo(
() =>
window.brandVariation
? {
logoImgUrl: window.brandVariation.logo_url,
brandVariationName: window.brandVariation.name,
brandVariationId: window.brandVariation.id,
brandId: window.brandVariation.brand_id,
brandVariationHomeUrl: window.brandVariation.home_url,
publishGuideHtml: window.brandVariation.publish_guide_html,
partner: window.brandVariation.partner,
brandedMenu: window.brandVariation.branded_menu,
submitBtnHtml: window.brandVariation.submit_button_html,
}
: undefined,
[]
)
const { owner, features } = useProjectContext({
owner: PropTypes.shape({
_id: PropTypes.string.isRequired,
}),
features: PropTypes.shape({
compileGroup: PropTypes.string,
}),
})
const cobranding = useMemo(() => {
if (window.brandVariation) {
return {
logoImgUrl: window.brandVariation.logo_url,
brandVariationName: window.brandVariation.name,
brandVariationId: window.brandVariation.id,
brandId: window.brandVariation.brand_id,
brandVariationHomeUrl: window.brandVariation.home_url,
publishGuideHtml: window.brandVariation.publish_guide_html,
partner: window.brandVariation.partner,
brandedMenu: window.brandVariation.branded_menu,
submitBtnHtml: window.brandVariation.submit_button_html,
}
} else {
return undefined
}
}, [])
const [loading] = useScopeValue('state.loading')
const [projectRootDocId] = useScopeValue('project.rootDoc_id')
const [projectName, setProjectName] = useScopeValue('project.name')
const [compileGroup] = useScopeValue('project.features.compileGroup')
const [rootFolder] = useScopeValue('rootFolder')
const [ownerId] = useScopeValue('project.owner._id')
const renameProject = useCallback(
newName => {
@ -100,24 +106,19 @@ export function EditorProvider({ children, settings }) {
const value = useMemo(
() => ({
cobranding,
hasPremiumCompile: compileGroup === 'priority',
hasPremiumCompile: features?.compileGroup === 'priority',
loading,
projectId: window.project_id,
projectRootDocId,
projectName: projectName || '', // initially might be empty in Angular
renameProject,
isProjectOwner: ownerId === window.user.id,
isProjectOwner: owner?._id === window.user.id,
isRestrictedTokenMember: window.isRestrictedTokenMember,
rootFolder,
}),
[
cobranding,
compileGroup,
features?.compileGroup,
loading,
ownerId,
projectName,
projectRootDocId,
renameProject,
owner?._id,
rootFolder,
]
)

View file

@ -0,0 +1,72 @@
import { createContext, useContext } from 'react'
import PropTypes from 'prop-types'
import useScopeValue from './util/scope-value-hook'
const ProjectContext = createContext()
ProjectContext.Provider.propTypes = {
value: PropTypes.shape({
_id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
rootDoc_id: PropTypes.string,
members: PropTypes.arrayOf(
PropTypes.shape({
_id: PropTypes.string.isRequired,
})
),
invites: PropTypes.arrayOf(
PropTypes.shape({
_id: PropTypes.string.isRequired,
})
),
features: PropTypes.shape({
collaborators: PropTypes.number,
compileGroup: PropTypes.oneOf(['alpha', 'standard', 'priority'])
.isRequired,
}),
publicAccesLevel: PropTypes.string,
tokens: PropTypes.shape({
readOnly: PropTypes.string,
readAndWrite: PropTypes.string,
}),
owner: PropTypes.shape({
_id: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
}),
}),
}
export function useProjectContext(propTypes) {
const context = useContext(ProjectContext)
if (!context) {
throw new Error(
'useProjectContext is only available inside ProjectProvider'
)
}
PropTypes.checkPropTypes(
propTypes,
context,
'data',
'ProjectContext.Provider'
)
return context
}
export function ProjectProvider({ children }) {
const [project] = useScopeValue('project', true)
// when the provider is created the project is still not added to the Angular scope.
// Name is also populated to prevent errors in existing React components
const value = project || { _id: window.project_id, name: '' }
return (
<ProjectContext.Provider value={value}>{children}</ProjectContext.Provider>
)
}
ProjectProvider.propTypes = {
children: PropTypes.any,
}

View file

@ -7,18 +7,21 @@ import { EditorProvider } from './editor-context'
import { CompileProvider } from './compile-context'
import { LayoutProvider } from './layout-context'
import { ChatProvider } from '../../features/chat/context/chat-context'
import { ProjectProvider } from './project-context'
export function ContextRoot({ children, ide, settings }) {
return (
<ApplicationProvider>
<IdeProvider ide={ide}>
<EditorProvider settings={settings}>
<CompileProvider>
<LayoutProvider>
<ChatProvider>{children}</ChatProvider>
</LayoutProvider>
</CompileProvider>
</EditorProvider>
<ProjectProvider>
<EditorProvider settings={settings}>
<CompileProvider>
<LayoutProvider>
<ChatProvider>{children}</ChatProvider>
</LayoutProvider>
</CompileProvider>
</EditorProvider>
</ProjectProvider>
</IdeProvider>
</ApplicationProvider>
)

View file

@ -23,8 +23,10 @@ describe('<ShareProjectModal/>', function () {
name: 'Test Project',
features: {
collaborators: 10,
compileGroup: 'standard',
},
owner: {
_id: 'member_author',
email: 'project-owner@example.com',
},
members: [],
@ -623,6 +625,7 @@ describe('<ShareProjectModal/>', function () {
publicAccesLevel: 'tokenBased',
features: {
collaborators: 0,
compileGroup: 'standard',
},
},
},

View file

@ -9,6 +9,7 @@ import { LayoutProvider } from '../../../frontend/js/shared/context/layout-conte
import { ChatProvider } from '../../../frontend/js/features/chat/context/chat-context'
import { IdeProvider } from '../../../frontend/js/shared/context/ide-context'
import { get } from 'lodash'
import { ProjectProvider } from '../../../frontend/js/shared/context/project-context'
export function EditorProviders({
user = { id: '123abd' },
@ -28,8 +29,11 @@ export function EditorProviders({
const $scope = {
project: {
_id: window.project_id,
name: 'project-name',
owner: {
_id: '124abd',
email: 'owner@example.com',
},
},
ui: {
@ -50,9 +54,11 @@ export function EditorProviders({
return (
<ApplicationProvider>
<IdeProvider ide={window._ide}>
<EditorProvider settings={{}}>
<LayoutProvider>{children}</LayoutProvider>
</EditorProvider>
<ProjectProvider>
<EditorProvider settings={{}}>
<LayoutProvider>{children}</LayoutProvider>
</EditorProvider>
</ProjectProvider>
</IdeProvider>
</ApplicationProvider>
)