diff --git a/services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.js b/services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.js index cfe2459c50..0431c48696 100644 --- a/services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.js +++ b/services/web/frontend/js/features/file-tree/contexts/file-tree-selectable.js @@ -75,7 +75,7 @@ function fileTreeSelectableReadOnlyReducer(selectedEntityIds, action) { } export function FileTreeSelectableProvider({ onSelect, children }) { - const { _id: projectId, rootDoc_id: rootDocId } = useProjectContext( + const { _id: projectId, rootDocId } = useProjectContext( projectContextPropTypes ) const { permissionsLevel } = useEditorContext(editorContextPropTypes) @@ -187,7 +187,7 @@ FileTreeSelectableProvider.propTypes = { const projectContextPropTypes = { _id: PropTypes.string.isRequired, - rootDoc_id: PropTypes.string, + rootDocId: PropTypes.string, } const editorContextPropTypes = { diff --git a/services/web/frontend/js/features/pdf-preview/util/compiler.js b/services/web/frontend/js/features/pdf-preview/util/compiler.js index 3c60b75d58..cf727593ee 100644 --- a/services/web/frontend/js/features/pdf-preview/util/compiler.js +++ b/services/web/frontend/js/features/pdf-preview/util/compiler.js @@ -15,7 +15,8 @@ const searchParams = new URLSearchParams(window.location.search) export default class DocumentCompiler { constructor({ - project, + projectId, + rootDocId, setChangedAt, setCompiling, setData, @@ -24,7 +25,8 @@ export default class DocumentCompiler { cleanupCompileResult, signal, }) { - this.project = project + this.projectId = projectId + this.rootDocId = rootDocId this.setChangedAt = setChangedAt this.setCompiling = setCompiling this.setData = setData @@ -84,7 +86,7 @@ export default class DocumentCompiler { const t0 = performance.now() const data = await postJSON( - `/project/${this.project._id}/compile?${params}`, + `/project/${this.projectId}/compile?${params}`, { body: { rootDoc_id: this.getRootDocOverrideId(), @@ -122,7 +124,7 @@ export default class DocumentCompiler { // if it contains "\documentclass" then use this as the root doc getRootDocOverrideId() { // only override when not in the root doc itself - if (this.currentDoc.doc_id !== this.project.rootDoc_id) { + if (this.currentDoc.doc_id !== this.rootDocId) { const snapshot = this.currentDoc.getSnapshot() if (snapshot && isMainFile(snapshot)) { @@ -177,7 +179,7 @@ export default class DocumentCompiler { const params = this.buildPostCompileParams() - return postJSON(`/project/${this.project._id}/compile/stop?${params}`, { + return postJSON(`/project/${this.projectId}/compile/stop?${params}`, { signal: this.signal, }) .catch(error => { @@ -193,7 +195,7 @@ export default class DocumentCompiler { clearCache() { const params = this.buildPostCompileParams() - return deleteJSON(`/project/${this.project._id}/output?${params}`, { + return deleteJSON(`/project/${this.projectId}/output?${params}`, { signal: this.signal, }).catch(error => { console.error(error) diff --git a/services/web/frontend/js/features/share-project-modal/components/add-collaborators.js b/services/web/frontend/js/features/share-project-modal/components/add-collaborators.js index da2b2dea01..0c131dcae5 100644 --- a/services/web/frontend/js/features/share-project-modal/components/add-collaborators.js +++ b/services/web/frontend/js/features/share-project-modal/components/add-collaborators.js @@ -20,11 +20,11 @@ export default function AddCollaborators() { const { updateProject, setInFlight, setError } = useShareProjectContext() - const project = useProjectContext() + const { _id: projectId, members, invites } = useProjectContext() const currentMemberEmails = useMemo( - () => (project.members || []).map(member => member.email).sort(), - [project.members] + () => (members || []).map(member => member.email).sort(), + [members] ) const nonMemberContacts = useMemo(() => { @@ -73,14 +73,14 @@ export default function AddCollaborators() { let data try { - const invite = (project.invites || []).find( + const invite = (invites || []).find( invite => invite.email === normalisedEmail ) if (invite) { - data = await resendInvite(project, invite) + data = await resendInvite(projectId, invite) } else { - data = await sendInvite(project, email, privileges) + data = await sendInvite(projectId, email, privileges) } } catch (error) { setInFlight(false) @@ -98,15 +98,15 @@ export default function AddCollaborators() { setInFlight(false) } else if (data.invite) { updateProject({ - invites: project.invites.concat(data.invite), + invites: invites.concat(data.invite), }) } else if (data.users) { updateProject({ - members: project.members.concat(data.users), + members: members.concat(data.users), }) } else if (data.user) { updateProject({ - members: project.members.concat(data.user), + members: members.concat(data.user), }) } diff --git a/services/web/frontend/js/features/share-project-modal/components/edit-member.js b/services/web/frontend/js/features/share-project-modal/components/edit-member.js index 6c72f08da8..6e792856b8 100644 --- a/services/web/frontend/js/features/share-project-modal/components/edit-member.js +++ b/services/web/frontend/js/features/share-project-modal/components/edit-member.js @@ -27,7 +27,7 @@ export default function EditMember({ member }) { }, [member.privileges]) const { updateProject, monitorRequest } = useShareProjectContext() - const project = useProjectContext() + const { _id: projectId, members } = useProjectContext() function handleSubmit(event) { event.preventDefault() @@ -36,12 +36,12 @@ export default function EditMember({ member }) { setConfirmingOwnershipTransfer(true) } else { monitorRequest(() => - updateMember(project, member, { + updateMember(projectId, member, { privilegeLevel: privileges, }) ).then(() => { updateProject({ - members: project.members.map(item => + members: members.map(item => item._id === member._id ? { ...item, privileges } : item ), }) @@ -118,16 +118,18 @@ SelectPrivilege.propTypes = { function RemoveMemberAction({ member }) { const { t } = useTranslation() const { updateProject, monitorRequest } = useShareProjectContext() - const project = useProjectContext() + const { _id: projectId, members } = useProjectContext() function handleClick(event) { event.preventDefault() - monitorRequest(() => removeMemberFromProject(project, member)).then(() => { - updateProject({ - members: project.members.filter(existing => existing !== member), - }) - }) + monitorRequest(() => removeMemberFromProject(projectId, member)).then( + () => { + updateProject({ + members: members.filter(existing => existing !== member), + }) + } + ) } return ( diff --git a/services/web/frontend/js/features/share-project-modal/components/invite.js b/services/web/frontend/js/features/share-project-modal/components/invite.js index 5440beb896..83c7429f6d 100644 --- a/services/web/frontend/js/features/share-project-modal/components/invite.js +++ b/services/web/frontend/js/features/share-project-modal/components/invite.js @@ -41,20 +41,20 @@ Invite.propTypes = { function ResendInvite({ invite }) { const { monitorRequest } = useShareProjectContext() - const project = useProjectContext() + const { _id: projectId } = useProjectContext() // const buttonRef = useRef(null) // const handleClick = useCallback( () => - monitorRequest(() => resendInvite(project, invite)).finally(() => { + monitorRequest(() => resendInvite(projectId, invite)).finally(() => { // NOTE: disabled as react-bootstrap v0.33.1 isn't forwarding the ref to the `button` // if (buttonRef.current) { // buttonRef.current.blur() // } document.activeElement.blur() }), - [invite, monitorRequest, project] + [invite, monitorRequest, projectId] ) return ( @@ -75,14 +75,14 @@ ResendInvite.propTypes = { function RevokeInvite({ invite }) { const { t } = useTranslation() const { updateProject, monitorRequest } = useShareProjectContext() - const project = useProjectContext() + const { _id: projectId, invites } = useProjectContext() function handleClick(event) { event.preventDefault() - monitorRequest(() => revokeInvite(project, invite)).then(() => { + monitorRequest(() => revokeInvite(projectId, invite)).then(() => { updateProject({ - invites: project.invites.filter(existing => existing !== invite), + invites: invites.filter(existing => existing !== invite), }) }) } diff --git a/services/web/frontend/js/features/share-project-modal/components/link-sharing.js b/services/web/frontend/js/features/share-project-modal/components/link-sharing.js index 5f77f88051..9294ab484e 100644 --- a/services/web/frontend/js/features/share-project-modal/components/link-sharing.js +++ b/services/web/frontend/js/features/share-project-modal/components/link-sharing.js @@ -14,27 +14,29 @@ export default function LinkSharing() { const { monitorRequest } = useShareProjectContext() - const project = useProjectContext() + const { _id: projectId, publicAccessLevel } = useProjectContext() // set the access level of a project const setAccessLevel = useCallback( - publicAccesLevel => { + newPublicAccessLevel => { setInflight(true) - monitorRequest(() => setProjectAccessLevel(project, publicAccesLevel)) + monitorRequest(() => + setProjectAccessLevel(projectId, newPublicAccessLevel) + ) .then(() => { // NOTE: not calling `updateProject` here as it receives data via // project:publicAccessLevel:changed and project:tokens:changed // over the websocket connection - // TODO: eventTracking.sendMB('project-make-token-based') when publicAccesLevel is 'tokenBased' + // TODO: eventTracking.sendMB('project-make-token-based') when newPublicAccessLevel is 'tokenBased' }) .finally(() => { setInflight(false) }) }, - [monitorRequest, project] + [monitorRequest, projectId] ) - switch (project.publicAccesLevel) { + switch (publicAccessLevel) { // Private (with token-access available) case 'private': return ( @@ -56,7 +58,7 @@ export default function LinkSharing() { return ( ) @@ -96,7 +98,7 @@ PrivateSharing.propTypes = { } function TokenBasedSharing({ setAccessLevel, inflight }) { - const project = useProjectContext() + const { tokens } = useProjectContext() return ( @@ -122,7 +124,7 @@ function TokenBasedSharing({ setAccessLevel, inflight }) { @@ -132,7 +134,7 @@ function TokenBasedSharing({ setAccessLevel, inflight }) { @@ -181,7 +183,7 @@ LegacySharing.propTypes = { } export function ReadOnlyTokenLink() { - const project = useProjectContext() + const { tokens } = useProjectContext() return ( @@ -191,7 +193,7 @@ export function ReadOnlyTokenLink() { diff --git a/services/web/frontend/js/features/share-project-modal/components/owner-info.js b/services/web/frontend/js/features/share-project-modal/components/owner-info.js index e7ec7a8653..13ac8b97e0 100644 --- a/services/web/frontend/js/features/share-project-modal/components/owner-info.js +++ b/services/web/frontend/js/features/share-project-modal/components/owner-info.js @@ -3,11 +3,11 @@ import { Col, Row } from 'react-bootstrap' import { Trans } from 'react-i18next' export default function OwnerInfo() { - const project = useProjectContext() + const { owner } = useProjectContext() return ( - {project.owner?.email} + {owner?.email} diff --git a/services/web/frontend/js/features/share-project-modal/components/send-invites-notice.js b/services/web/frontend/js/features/share-project-modal/components/send-invites-notice.js index 2c34d65ab9..dad789e670 100644 --- a/services/web/frontend/js/features/share-project-modal/components/send-invites-notice.js +++ b/services/web/frontend/js/features/share-project-modal/components/send-invites-notice.js @@ -4,12 +4,12 @@ import { Trans } from 'react-i18next' import { useProjectContext } from '../../../shared/context/project-context' export default function SendInvitesNotice() { - const project = useProjectContext() + const { publicAccessLevel } = useProjectContext() return ( - + ) diff --git a/services/web/frontend/js/features/share-project-modal/components/send-invites.js b/services/web/frontend/js/features/share-project-modal/components/send-invites.js index 12392a64e5..d538dfc202 100644 --- a/services/web/frontend/js/features/share-project-modal/components/send-invites.js +++ b/services/web/frontend/js/features/share-project-modal/components/send-invites.js @@ -5,24 +5,21 @@ import AddCollaboratorsUpgrade from './add-collaborators-upgrade' import { useProjectContext } from '../../../shared/context/project-context' export default function SendInvites() { - const project = useProjectContext() + const { members, invites, features } = useProjectContext() // whether the project has not reached the collaborator limit const canAddCollaborators = useMemo(() => { - if (!project) { + if (!features) { return false } - if (project.features.collaborators === -1) { + if (features.collaborators === -1) { // infinite collaborators return true } - return ( - project.members.length + project.invites.length < - project.features.collaborators - ) - }, [project]) + return members.length + invites.length < features.collaborators + }, [members, invites, features]) return ( diff --git a/services/web/frontend/js/features/share-project-modal/components/share-modal-body.js b/services/web/frontend/js/features/share-project-modal/components/share-modal-body.js index f4bd66df32..7b0cf3773c 100644 --- a/services/web/frontend/js/features/share-project-modal/components/share-modal-body.js +++ b/services/web/frontend/js/features/share-project-modal/components/share-modal-body.js @@ -18,7 +18,7 @@ export default function ShareModalBody() { splitTestVariants: PropTypes.object, }) - const project = useProjectContext() + const { invites, members } = useProjectContext() switch (splitTestVariants['project-share-modal-paywall']) { case 'new-copy-top': @@ -35,7 +35,7 @@ export default function ShareModalBody() { - {project.members.map(member => + {members.map(member => isAdmin ? ( ) : ( @@ -43,7 +43,7 @@ export default function ShareModalBody() { ) )} - {project.invites.map(invite => ( + {invites.map(invite => ( ))} @@ -64,7 +64,7 @@ export default function ShareModalBody() { <> - {project.members.map(member => + {members.map(member => isAdmin ? ( ) : ( @@ -72,7 +72,7 @@ export default function ShareModalBody() { ) )} - {project.invites.map(invite => ( + {invites.map(invite => ( ))} @@ -99,7 +99,7 @@ export default function ShareModalBody() { - {project.members.map(member => + {members.map(member => isAdmin ? ( ) : ( @@ -107,7 +107,7 @@ export default function ShareModalBody() { ) )} - {project.invites.map(invite => ( + {invites.map(invite => ( ))} diff --git a/services/web/frontend/js/features/share-project-modal/components/share-project-modal.js b/services/web/frontend/js/features/share-project-modal/components/share-project-modal.js index b474bc10bf..6fc79fe1a6 100644 --- a/services/web/frontend/js/features/share-project-modal/components/share-project-modal.js +++ b/services/web/frontend/js/features/share-project-modal/components/share-project-modal.js @@ -7,7 +7,10 @@ import React, { } from 'react' import PropTypes from 'prop-types' import ShareProjectModalContent from './share-project-modal-content' -import { useProjectContext } from '../../../shared/context/project-context' +import { + useProjectContext, + projectShape, +} from '../../../shared/context/project-context' import { useSplitTestContext } from '../../../shared/context/split-test-context' import { sendMB } from '../../../infrastructure/event-tracking' @@ -37,32 +40,6 @@ export function useShareProjectContext() { return context } -const projectShape = { - _id: PropTypes.string.isRequired, - members: PropTypes.arrayOf( - PropTypes.shape({ - _id: PropTypes.string.isRequired, - }) - ), - invites: PropTypes.arrayOf( - PropTypes.shape({ - _id: PropTypes.string.isRequired, - }) - ), - name: PropTypes.string, - features: PropTypes.shape({ - collaborators: PropTypes.number, - }), - publicAccesLevel: PropTypes.string, - tokens: PropTypes.shape({ - readOnly: PropTypes.string, - readAndWrite: PropTypes.string, - }), - owner: PropTypes.shape({ - email: PropTypes.string, - }), -} - const ShareProjectModal = React.memo(function ShareProjectModal({ handleHide, show, diff --git a/services/web/frontend/js/features/share-project-modal/components/transfer-ownership-modal.js b/services/web/frontend/js/features/share-project-modal/components/transfer-ownership-modal.js index c806aec710..50bf61b64a 100644 --- a/services/web/frontend/js/features/share-project-modal/components/transfer-ownership-modal.js +++ b/services/web/frontend/js/features/share-project-modal/components/transfer-ownership-modal.js @@ -12,13 +12,13 @@ export default function TransferOwnershipModal({ member, cancel }) { const [inflight, setInflight] = useState(false) const [error, setError] = useState(false) - const project = useProjectContext() + const { _id: projectId, name: projectName } = useProjectContext() function confirm() { setError(false) setInflight(true) - transferProjectOwnership(project, member) + transferProjectOwnership(projectId, member) .then(() => { reload() }) @@ -39,7 +39,7 @@ export default function TransferOwnershipModal({ member, cancel }) {

, ]} />

diff --git a/services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js b/services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js index 11060a0e9c..7f52490bd2 100644 --- a/services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js +++ b/services/web/frontend/js/features/share-project-modal/controllers/react-share-project-modal-controller.js @@ -45,7 +45,7 @@ export default App.controller( ide.socket.on('project:membership:changed', data => { if (data.members) { - listProjectMembers($scope.project) + listProjectMembers($scope.project._id) .then(({ members }) => { if (members) { $scope.$applyAsync(() => { @@ -59,7 +59,7 @@ export default App.controller( } if (data.invites) { - listProjectInvites($scope.project) + listProjectInvites($scope.project._id) .then(({ invites }) => { if (invites) { $scope.$applyAsync(() => { diff --git a/services/web/frontend/js/features/share-project-modal/utils/api.js b/services/web/frontend/js/features/share-project-modal/utils/api.js index ad01268433..d58a42c735 100644 --- a/services/web/frontend/js/features/share-project-modal/utils/api.js +++ b/services/web/frontend/js/features/share-project-modal/utils/api.js @@ -6,11 +6,11 @@ import { } from '../../../infrastructure/fetch-json' import { executeV2Captcha } from './captcha' -export function sendInvite(project, email, privileges) { +export function sendInvite(projectId, email, privileges) { return executeV2Captcha( window.ExposedSettings.recaptchaDisabled?.invite ).then(grecaptchaResponse => { - return postJSON(`/project/${project._id}/invite`, { + return postJSON(`/project/${projectId}/invite`, { body: { email, // TODO: normalisedEmail? privileges, @@ -20,48 +20,42 @@ export function sendInvite(project, email, privileges) { }) } -export function resendInvite(project, invite) { - return postJSON(`/project/${project._id}/invite/${invite._id}/resend`) +export function resendInvite(projectId, invite) { + return postJSON(`/project/${projectId}/invite/${invite._id}/resend`) } -export function revokeInvite(project, invite) { - return deleteJSON(`/project/${project._id}/invite/${invite._id}`) +export function revokeInvite(projectId, invite) { + return deleteJSON(`/project/${projectId}/invite/${invite._id}`) } -export function updateMember(project, member, data) { - return putJSON(`/project/${project._id}/users/${member._id}`, { +export function updateMember(projectId, member, data) { + return putJSON(`/project/${projectId}/users/${member._id}`, { body: data, }) } -export function removeMemberFromProject(project, member) { - return deleteJSON(`/project/${project._id}/users/${member._id}`) +export function removeMemberFromProject(projectId, member) { + return deleteJSON(`/project/${projectId}/users/${member._id}`) } -export function transferProjectOwnership(project, member) { - return postJSON(`/project/${project._id}/transfer-ownership`, { +export function transferProjectOwnership(projectId, member) { + return postJSON(`/project/${projectId}/transfer-ownership`, { body: { user_id: member._id, }, }) } -export function setProjectAccessLevel(project, publicAccessLevel) { - return postJSON(`/project/${project._id}/settings/admin`, { +export function setProjectAccessLevel(projectId, publicAccessLevel) { + return postJSON(`/project/${projectId}/settings/admin`, { body: { publicAccessLevel }, }) } -// export function updateProjectAdminSettings(project, data) { -// return postJSON(`/project/${project._id}/settings/admin`, { -// body: data -// }) -// } - -export function listProjectMembers(project) { - return getJSON(`/project/${project._id}/members`) +export function listProjectMembers(projectId) { + return getJSON(`/project/${projectId}/members`) } -export function listProjectInvites(project) { - return getJSON(`/project/${project._id}/invites`) +export function listProjectInvites(projectId) { + return getJSON(`/project/${projectId}/invites`) } diff --git a/services/web/frontend/js/shared/context/compile-context.js b/services/web/frontend/js/shared/context/compile-context.js index 9551e34f76..689a45dc5e 100644 --- a/services/web/frontend/js/shared/context/compile-context.js +++ b/services/web/frontend/js/shared/context/compile-context.js @@ -64,9 +64,7 @@ export function CompileProvider({ children }) { const { hasPremiumCompile, isProjectOwner } = useEditorContext() - const project = useProjectContext() - - const projectId = project._id + const { _id: projectId, rootDocId } = useProjectContext() // whether a compile is in progress const [compiling, setCompiling] = useState(false) @@ -169,7 +167,8 @@ export function CompileProvider({ children }) { // the document compiler const [compiler] = useState(() => { return new DocumentCompiler({ - project, + projectId, + rootDocId, setChangedAt, setCompiling, setData, diff --git a/services/web/frontend/js/shared/context/project-context.js b/services/web/frontend/js/shared/context/project-context.js index 43433abb20..f41a1bcbc0 100644 --- a/services/web/frontend/js/shared/context/project-context.js +++ b/services/web/frontend/js/shared/context/project-context.js @@ -4,39 +4,41 @@ import useScopeValue from '../hooks/use-scope-value' 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']), - trackChangesVisible: PropTypes.bool, - references: PropTypes.bool, - mendeley: PropTypes.bool, - zotero: PropTypes.bool, - }), - publicAccesLevel: PropTypes.string, - tokens: PropTypes.shape({ - readOnly: PropTypes.string, - readAndWrite: PropTypes.string, - }), - owner: PropTypes.shape({ +export const projectShape = { + _id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + rootDocId: PropTypes.string, + members: PropTypes.arrayOf( + PropTypes.shape({ _id: PropTypes.string.isRequired, - email: PropTypes.string.isRequired, - }), + }) + ), + invites: PropTypes.arrayOf( + PropTypes.shape({ + _id: PropTypes.string.isRequired, + }) + ), + features: PropTypes.shape({ + collaborators: PropTypes.number, + compileGroup: PropTypes.oneOf(['alpha', 'standard', 'priority']), + trackChangesVisible: PropTypes.bool, + references: PropTypes.bool, + mendeley: PropTypes.bool, + zotero: PropTypes.bool, }), + publicAccessLevel: PropTypes.string, + tokens: PropTypes.shape({ + readOnly: PropTypes.string, + readAndWrite: PropTypes.string, + }), + owner: PropTypes.shape({ + _id: PropTypes.string.isRequired, + email: PropTypes.string.isRequired, + }), +} + +ProjectContext.Provider.propTypes = { + value: PropTypes.shape(projectShape), } export function useProjectContext(propTypes) { @@ -70,12 +72,42 @@ const projectFallback = { export function ProjectProvider({ children }) { const [project] = useScopeValue('project', true) + const { + _id, + name, + rootDoc_id: rootDocId, + members, + invites, + features, + publicAccesLevel: publicAccessLevel, + tokens, + owner, + } = project || projectFallback + const value = useMemo(() => { return { - ...projectFallback, - ...project, + _id, + name, + rootDocId, + members, + invites, + features, + publicAccessLevel, + tokens, + owner, } - }, [project]) + }, [ + _id, + name, + rootDocId, + members, + invites, + features, + publicAccessLevel, + tokens, + owner, + ]) + return ( {children} )