Merge pull request #6236 from overleaf/ta-project-context-strict

Strict Project Context

GitOrigin-RevId: a0f7f2b3dcb29fbd0102dcb920cf5424a921d583
This commit is contained in:
Timothée Alby 2022-01-10 16:47:10 +01:00 committed by Copybot
parent 24bd557485
commit 6319455e91
16 changed files with 162 additions and 157 deletions

View file

@ -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 = {

View file

@ -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)

View file

@ -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),
})
}

View file

@ -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 (

View file

@ -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),
})
})
}

View file

@ -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 (
<LegacySharing
setAccessLevel={setAccessLevel}
accessLevel={project.publicAccesLevel}
accessLevel={publicAccessLevel}
inflight={inflight}
/>
)
@ -96,7 +98,7 @@ PrivateSharing.propTypes = {
}
function TokenBasedSharing({ setAccessLevel, inflight }) {
const project = useProjectContext()
const { tokens } = useProjectContext()
return (
<Row className="public-access-level">
@ -122,7 +124,7 @@ function TokenBasedSharing({ setAccessLevel, inflight }) {
<Trans i18nKey="anyone_with_link_can_edit" />
</strong>
<AccessToken
token={project?.tokens?.readAndWrite}
token={tokens?.readAndWrite}
path="/"
tooltipId="tooltip-copy-link-rw"
/>
@ -132,7 +134,7 @@ function TokenBasedSharing({ setAccessLevel, inflight }) {
<Trans i18nKey="anyone_with_link_can_view" />
</strong>
<AccessToken
token={project?.tokens?.readOnly}
token={tokens?.readOnly}
path="/read/"
tooltipId="tooltip-copy-link-ro"
/>
@ -181,7 +183,7 @@ LegacySharing.propTypes = {
}
export function ReadOnlyTokenLink() {
const project = useProjectContext()
const { tokens } = useProjectContext()
return (
<Row className="public-access-level">
@ -191,7 +193,7 @@ export function ReadOnlyTokenLink() {
<Trans i18nKey="anyone_with_link_can_view" />
</strong>
<AccessToken
token={project?.tokens?.readOnly}
token={tokens?.readOnly}
path="/read/"
tooltipId="tooltip-copy-link-ro"
/>

View file

@ -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 (
<Row className="project-member">
<Col xs={7}>{project.owner?.email}</Col>
<Col xs={7}>{owner?.email}</Col>
<Col xs={3} className="text-left">
<Trans i18nKey="owner" />
</Col>

View file

@ -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 (
<Row className="public-access-level public-access-level--notice">
<Col xs={12} className="text-center">
<AccessLevel level={project.publicAccesLevel} />
<AccessLevel level={publicAccessLevel} />
</Col>
</Row>
)

View file

@ -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 (
<Row className="invite-controls">

View file

@ -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() {
<OwnerInfo />
{project.members.map(member =>
{members.map(member =>
isAdmin ? (
<EditMember key={member._id} member={member} />
) : (
@ -43,7 +43,7 @@ export default function ShareModalBody() {
)
)}
{project.invites.map(invite => (
{invites.map(invite => (
<Invite key={invite._id} invite={invite} isAdmin={isAdmin} />
))}
@ -64,7 +64,7 @@ export default function ShareModalBody() {
<>
<OwnerInfo />
{project.members.map(member =>
{members.map(member =>
isAdmin ? (
<EditMember key={member._id} member={member} />
) : (
@ -72,7 +72,7 @@ export default function ShareModalBody() {
)
)}
{project.invites.map(invite => (
{invites.map(invite => (
<Invite key={invite._id} invite={invite} isAdmin={isAdmin} />
))}
@ -99,7 +99,7 @@ export default function ShareModalBody() {
<OwnerInfo />
{project.members.map(member =>
{members.map(member =>
isAdmin ? (
<EditMember key={member._id} member={member} />
) : (
@ -107,7 +107,7 @@ export default function ShareModalBody() {
)
)}
{project.invites.map(invite => (
{invites.map(invite => (
<Invite key={invite._id} invite={invite} isAdmin={isAdmin} />
))}

View file

@ -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,

View file

@ -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 }) {
<p>
<Trans
i18nKey="project_ownership_transfer_confirmation_1"
values={{ user: member.email, project: project.name }}
values={{ user: member.email, project: projectName }}
components={[<strong key="strong-1" />, <strong key="strong-2" />]}
/>
</p>

View file

@ -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(() => {

View file

@ -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`)
}

View file

@ -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,

View file

@ -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 (
<ProjectContext.Provider value={value}>{children}</ProjectContext.Provider>
)