mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-24 17:51:51 +02:00
Move project context out of scope value store (#26615)
* Refactor project context to not use scope store * Fix Cypress tests for project context changes * Fix frontend React Testing Library tests for project context changes * Remove redundant code * Fix some project types in tests * Remove unused import and fix a type * Throw an error if updating the project in the project context before joining the project * Fix some review panel tests * Remove unused imports GitOrigin-RevId: 2f0c928b651f387aa980c29aef7d1ba0649790a7
This commit is contained in:
@@ -200,7 +200,7 @@ export const ChatProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
clientId.current = chatClientIdGenerator.generate()
|
||||
}
|
||||
const user = useUserContext()
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
|
||||
const { chatIsOpen: chatIsOpenOldEditor } = useLayoutContext()
|
||||
const { selectedTab: selectedRailTab, isOpen: railIsOpen } = useRailContext()
|
||||
|
||||
@@ -13,13 +13,9 @@ const EditorCloneProjectModalWrapper = React.memo(
|
||||
handleHide: () => void
|
||||
openProject: ({ project_id }: { project_id: string }) => void
|
||||
}) {
|
||||
const {
|
||||
_id: projectId,
|
||||
name: projectName,
|
||||
tags: projectTags,
|
||||
} = useProjectContext()
|
||||
const { project, tags: projectTags } = useProjectContext()
|
||||
|
||||
if (!projectName) {
|
||||
if (!project) {
|
||||
// wait for useProjectContext
|
||||
return null
|
||||
} else {
|
||||
@@ -28,8 +24,8 @@ const EditorCloneProjectModalWrapper = React.memo(
|
||||
handleHide={handleHide}
|
||||
show={show}
|
||||
handleAfterCloned={openProject}
|
||||
projectId={projectId}
|
||||
projectName={projectName}
|
||||
projectId={project._id}
|
||||
projectName={project.name}
|
||||
projectTags={projectTags}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
export default function DownloadPDF() {
|
||||
const { t } = useTranslation()
|
||||
const { pdfDownloadUrl, pdfUrl } = useCompileContext()
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
|
||||
function sendDownloadEvent() {
|
||||
eventTracking.sendMB('download-pdf-button-click', {
|
||||
|
||||
@@ -6,7 +6,7 @@ import MaterialIcon from '@/shared/components/material-icon'
|
||||
|
||||
export default function DownloadSource() {
|
||||
const { t } = useTranslation()
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
|
||||
function sendDownloadEvent() {
|
||||
eventTracking.sendMB('download-zip-button-click', {
|
||||
|
||||
@@ -1,40 +1,38 @@
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useIdeContext } from '../../../shared/context/ide-context'
|
||||
import useScopeValue from '../../../shared/hooks/use-scope-value'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import type { ProjectSettings } from '../utils/api'
|
||||
|
||||
export default function useProjectWideSettingsSocketListener() {
|
||||
const { socket } = useIdeContext()
|
||||
|
||||
const [project, setProject] = useScopeValue<ProjectSettings | undefined>(
|
||||
'project'
|
||||
)
|
||||
const { project, updateProject } = useProjectContext()
|
||||
|
||||
const setCompiler = useCallback(
|
||||
(compiler: ProjectSettings['compiler']) => {
|
||||
if (project) {
|
||||
setProject({ ...project, compiler })
|
||||
updateProject({ compiler })
|
||||
}
|
||||
},
|
||||
[project, setProject]
|
||||
[project, updateProject]
|
||||
)
|
||||
|
||||
const setImageName = useCallback(
|
||||
(imageName: ProjectSettings['imageName']) => {
|
||||
if (project) {
|
||||
setProject({ ...project, imageName })
|
||||
updateProject({ imageName })
|
||||
}
|
||||
},
|
||||
[project, setProject]
|
||||
[project, updateProject]
|
||||
)
|
||||
|
||||
const setSpellCheckLanguage = useCallback(
|
||||
(spellCheckLanguage: ProjectSettings['spellCheckLanguage']) => {
|
||||
if (project) {
|
||||
setProject({ ...project, spellCheckLanguage })
|
||||
updateProject({ spellCheckLanguage })
|
||||
}
|
||||
},
|
||||
[project, setProject]
|
||||
[project, updateProject]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useCallback } from 'react'
|
||||
import useScopeValue from '../../../shared/hooks/use-scope-value'
|
||||
import type { ProjectSettings } from '../utils/api'
|
||||
import useRootDocId from './use-root-doc-id'
|
||||
import useSaveProjectSettings from './use-save-project-settings'
|
||||
import useSetSpellCheckLanguage from './use-set-spell-check-language'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
|
||||
export default function useProjectWideSettings() {
|
||||
// The value will be undefined on mount
|
||||
const [project] = useScopeValue<ProjectSettings | undefined>('project')
|
||||
const { project } = useProjectContext()
|
||||
const saveProjectSettings = useSaveProjectSettings()
|
||||
|
||||
const setCompiler = useCallback(
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
|
||||
import useScopeValue from '../../../shared/hooks/use-scope-value'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import type { ProjectSettings } from '../utils/api'
|
||||
import useSaveProjectSettings from './use-save-project-settings'
|
||||
|
||||
export default function useRootDocId() {
|
||||
const [rootDocId] =
|
||||
useScopeValue<ProjectSettings['rootDocId']>('project.rootDocId')
|
||||
const { project } = useProjectContext()
|
||||
const rootDocId = project?.rootDocId
|
||||
const { permissionsLevel } = useIdeReactContext()
|
||||
const saveProjectSettings = useSaveProjectSettings()
|
||||
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
import { type ProjectSettings, saveProjectSettings } from '../utils/api'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import useScopeValue from '../../../shared/hooks/use-scope-value'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
|
||||
export default function useSaveProjectSettings() {
|
||||
// projectSettings value will be undefined on mount
|
||||
const [projectSettings, setProjectSettings] = useScopeValue<
|
||||
ProjectSettings | undefined
|
||||
>('project')
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId, project, updateProject } = useProjectContext()
|
||||
|
||||
return async (
|
||||
key: keyof ProjectSettings,
|
||||
newSetting: ProjectSettings[keyof ProjectSettings]
|
||||
) => {
|
||||
if (projectSettings) {
|
||||
const currentSetting = projectSettings[key]
|
||||
if (project) {
|
||||
const currentSetting = project[key]
|
||||
if (currentSetting !== newSetting) {
|
||||
await saveProjectSettings(projectId, {
|
||||
[key]: newSetting,
|
||||
})
|
||||
setProjectSettings({ ...projectSettings, [key]: newSetting })
|
||||
|
||||
updateProject({ [key]: newSetting })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useCallback } from 'react'
|
||||
import useScopeValue from '../../../shared/hooks/use-scope-value'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import { type ProjectSettings, saveUserSettings } from '../utils/api'
|
||||
import useSaveProjectSettings from './use-save-project-settings'
|
||||
|
||||
export default function useSetSpellCheckLanguage() {
|
||||
const [spellCheckLanguage, setSpellCheckLanguage] = useScopeValue<
|
||||
ProjectSettings['spellCheckLanguage']
|
||||
>('project.spellCheckLanguage')
|
||||
const { project } = useProjectContext()
|
||||
const spellCheckLanguage = project?.spellCheckLanguage
|
||||
const saveProjectSettings = useSaveProjectSettings()
|
||||
|
||||
return useCallback(
|
||||
@@ -16,10 +15,8 @@ export default function useSetSpellCheckLanguage() {
|
||||
newSpellCheckLanguage !== spellCheckLanguage
|
||||
|
||||
if (allowUpdate) {
|
||||
setSpellCheckLanguage(newSpellCheckLanguage)
|
||||
|
||||
// Save project settings is created from hooks because it will save the value on
|
||||
// both server-side and client-side (angular scope)
|
||||
// both server-side and client-side (project context)
|
||||
saveProjectSettings('spellCheckLanguage', newSpellCheckLanguage)
|
||||
|
||||
// For user settings, we only need to save it on server-side,
|
||||
@@ -27,6 +24,6 @@ export default function useSetSpellCheckLanguage() {
|
||||
saveUserSettings('spellCheckLanguage', newSpellCheckLanguage)
|
||||
}
|
||||
},
|
||||
[setSpellCheckLanguage, spellCheckLanguage, saveProjectSettings]
|
||||
[spellCheckLanguage, saveProjectSettings]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { postJSON } from '../../../infrastructure/fetch-json'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { UserSettings } from '../../../../../types/user-settings'
|
||||
|
||||
export type ProjectSettings = {
|
||||
export interface ProjectSettings {
|
||||
compiler: ProjectCompiler
|
||||
imageName: string
|
||||
rootDocId: string
|
||||
|
||||
@@ -96,7 +96,7 @@ export function FolderUploadConflicts({
|
||||
setError: (error: string) => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
|
||||
// Don't allow overwriting files with a folder
|
||||
const hasFileConflict = conflicts.some(conflict => conflict.type === 'file')
|
||||
|
||||
@@ -191,7 +191,7 @@ function SelectProject({
|
||||
setSelectedProject,
|
||||
}: SelectProjectProps) {
|
||||
const { t } = useTranslation()
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
|
||||
const { data, error, loading } = useUserProjects()
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function FileTreeUploadDoc() {
|
||||
const { parentFolderId, cancel, droppedFiles, setDroppedFiles } =
|
||||
useFileTreeActionable()
|
||||
const { fileTreeData } = useFileTreeData()
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
|
||||
const [error, setError] = useState<string>()
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useLocation } from '../../../../shared/hooks/use-location'
|
||||
// handle "not-logged-in" errors by redirecting to the login page
|
||||
export default function RedirectToLogin() {
|
||||
const { t } = useTranslation()
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
const [secondsToRedirect, setSecondsToRedirect] = useState(10)
|
||||
const location = useLocation()
|
||||
|
||||
|
||||
@@ -25,14 +25,15 @@ function FileTreeItemMenuItems() {
|
||||
selectedFileName,
|
||||
} = useFileTreeActionable()
|
||||
|
||||
const { owner } = useProjectContext()
|
||||
const { project } = useProjectContext()
|
||||
const projectOwner = project?.owner?._id
|
||||
|
||||
const downloadWithAnalytics = useCallback(() => {
|
||||
// we are only interested in downloads of bib files WRT analytics, for the purposes of promoting the tpr integrations
|
||||
if (selectedFileName?.endsWith('.bib')) {
|
||||
eventTracking.sendMB('download-bib-file', { projectOwner: owner._id })
|
||||
eventTracking.sendMB('download-bib-file', { projectOwner })
|
||||
}
|
||||
}, [selectedFileName, owner])
|
||||
}, [selectedFileName, projectOwner])
|
||||
|
||||
const createWithAnalytics = useCallback(() => {
|
||||
eventTracking.sendMB('new-file-click', { location: 'file-menu' })
|
||||
|
||||
@@ -40,7 +40,7 @@ const FileTreeRoot = React.memo<{
|
||||
}) {
|
||||
const [fileTreeContainer, setFileTreeContainer] =
|
||||
useState<HTMLDivElement | null>(null)
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
const { fileTreeData } = useFileTreeData()
|
||||
const isReady = Boolean(projectId && fileTreeData)
|
||||
const newEditor = useIsNewEditorEnabled()
|
||||
|
||||
@@ -222,7 +222,7 @@ function fileTreeActionableReducer(state: State, action: Action) {
|
||||
export const FileTreeActionableProvider: FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
const { fileTreeReadOnly } = useFileTreeData()
|
||||
const { indexAllReferences } = useReferencesContext()
|
||||
const { write } = usePermissionsContext()
|
||||
|
||||
@@ -128,7 +128,8 @@ export const FileTreeSelectableProvider: FC<
|
||||
onSelect: (value: FindResult[]) => void
|
||||
}>
|
||||
> = ({ onSelect, children }) => {
|
||||
const { _id: projectId, rootDocId } = useProjectContext()
|
||||
const { projectId, project } = useProjectContext()
|
||||
const rootDocId = project?.rootDocId
|
||||
|
||||
const [initialSelectedEntityId] = usePersistedState(
|
||||
`doc.open_id.${projectId}`,
|
||||
|
||||
@@ -49,7 +49,7 @@ type FileViewHeaderProps = {
|
||||
}
|
||||
|
||||
export default function FileViewHeader({ file }: FileViewHeaderProps) {
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
const { fileTreeReadOnly } = useFileTreeData()
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function FileViewImage({
|
||||
onLoad: () => void
|
||||
onError: () => void
|
||||
}) {
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
return (
|
||||
<img
|
||||
src={fileUrl(projectId, file.id, file.hash)}
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function FileViewRefreshButton({
|
||||
setRefreshError,
|
||||
file,
|
||||
}: FileViewRefreshButtonProps) {
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const isMountedRef = useIsMounted()
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function FileViewText({
|
||||
onLoad: () => void
|
||||
onError: () => void
|
||||
}) {
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
|
||||
const [textPreview, setTextPreview] = useState('')
|
||||
const [shouldShowDots, setShouldShowDots] = useState(false)
|
||||
|
||||
@@ -78,13 +78,10 @@ const updatesInfoInitialState: HistoryContextValue['updatesInfo'] = {
|
||||
function useHistory() {
|
||||
const { view } = useLayoutContext()
|
||||
const user = useUserContext()
|
||||
const project = useProjectContext()
|
||||
const { projectId, project, features } = useProjectContext()
|
||||
const userId = user.id
|
||||
const projectId = project._id
|
||||
const projectOwnerId = project.owner?._id
|
||||
const userHasFullFeature = Boolean(
|
||||
project.features?.versioning || user.isAdmin
|
||||
)
|
||||
const projectOwnerId = project?.owner?._id
|
||||
const userHasFullFeature = Boolean(features.versioning || user.isAdmin)
|
||||
const currentUserIsOwner = projectOwnerId === userId
|
||||
|
||||
const [selection, setSelection] = useState<Selection>(selectionInitialState)
|
||||
|
||||
@@ -39,7 +39,9 @@ const FileTreeOpenContext = createContext<
|
||||
export const FileTreeOpenProvider: FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { rootDocId, owner } = useProjectContext()
|
||||
const { project } = useProjectContext()
|
||||
const rootDocId = project?.rootDocId
|
||||
const projectOwner = project?.owner?._id
|
||||
const { eventEmitter, projectJoined } = useIdeReactContext()
|
||||
const { openDocWithId, openInitialDoc } = useEditorManagerContext()
|
||||
const { currentDocumentId } = useEditorOpenDocContext()
|
||||
@@ -81,7 +83,7 @@ export const FileTreeOpenProvider: FC<React.PropsWithChildren> = ({
|
||||
openDocWithId(selected.entity._id, { keepCurrentView: true })
|
||||
if (selected.entity.name.endsWith('.bib')) {
|
||||
sendMB('open-bib-file', {
|
||||
projectOwner: owner._id,
|
||||
projectOwner,
|
||||
isSampleFile: selected.entity.name === 'sample.bib',
|
||||
linkedFileProvider: null,
|
||||
})
|
||||
@@ -96,7 +98,7 @@ export const FileTreeOpenProvider: FC<React.PropsWithChildren> = ({
|
||||
if (openFile) {
|
||||
if (selected?.entity?.name?.endsWith('.bib')) {
|
||||
sendMB('open-bib-file', {
|
||||
projectOwner: owner._id,
|
||||
projectOwner,
|
||||
isSampleFile: false,
|
||||
linkedFileProvider: (selected.entity as FileRef).linkedFileData
|
||||
?.provider,
|
||||
@@ -105,7 +107,7 @@ export const FileTreeOpenProvider: FC<React.PropsWithChildren> = ({
|
||||
window.dispatchEvent(new CustomEvent('file-view:file-opened'))
|
||||
}
|
||||
},
|
||||
[fileTreeReady, setOpenFile, openDocWithId, owner]
|
||||
[fileTreeReady, setOpenFile, openDocWithId, projectOwner]
|
||||
)
|
||||
|
||||
const handleFileTreeDelete = useCallback(
|
||||
|
||||
@@ -19,6 +19,8 @@ import { postJSON } from '@/infrastructure/fetch-json'
|
||||
import { ReactScopeEventEmitter } from '@/features/ide-react/scope-event-emitter/react-scope-event-emitter'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { type PermissionsLevel } from '@/features/ide-react/types/permissions'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import { ProjectMetadata } from '@/shared/context/types/project-metadata'
|
||||
|
||||
const LOADED_AT = new Date()
|
||||
|
||||
@@ -44,10 +46,6 @@ function populateIdeReactScope(store: ReactScopeValueStore) {
|
||||
store.set('settings', {})
|
||||
}
|
||||
|
||||
function populateProjectScope(store: ReactScopeValueStore) {
|
||||
store.allowNonExistentPath('project', true)
|
||||
}
|
||||
|
||||
function populatePdfScope(store: ReactScopeValueStore) {
|
||||
store.allowNonExistentPath('pdf', true)
|
||||
}
|
||||
@@ -61,7 +59,6 @@ export function createReactScopeValueStore() {
|
||||
// initialization code together with the context and would only populate
|
||||
// necessary values in the store, but this is simpler for now
|
||||
populateIdeReactScope(scopeStore)
|
||||
populateProjectScope(scopeStore)
|
||||
populatePdfScope(scopeStore)
|
||||
|
||||
scopeStore.allowNonExistentPath('hasLintingError')
|
||||
@@ -94,6 +91,8 @@ export const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
const [projectJoined, setProjectJoined] = useState(false)
|
||||
|
||||
const { socket, getSocketDebuggingInfo } = useConnectionContext()
|
||||
const { joinProject, project } = useProjectContext()
|
||||
const spellCheckLanguage = project?.spellCheckLanguage
|
||||
|
||||
const reportError = useCallback(
|
||||
(error: any, meta?: Record<string, any>) => {
|
||||
@@ -105,7 +104,7 @@ export const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
performance_now: performance.now(),
|
||||
release,
|
||||
client_load: LOADED_AT,
|
||||
spellCheckLanguage: scopeStore.get('project.spellCheckLanguage'),
|
||||
spellCheckLanguage,
|
||||
...getSocketDebuggingInfo(),
|
||||
}
|
||||
|
||||
@@ -124,17 +123,28 @@ export const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
},
|
||||
})
|
||||
},
|
||||
[release, projectId, getSocketDebuggingInfo, scopeStore]
|
||||
[release, projectId, getSocketDebuggingInfo, spellCheckLanguage]
|
||||
)
|
||||
|
||||
// Populate scope values when joining project, then fire project:joined event
|
||||
useEffect(() => {
|
||||
function handleJoinProjectResponse({
|
||||
project: { rootDoc_id: rootDocId, ..._project },
|
||||
project: {
|
||||
rootDoc_id: rootDocId,
|
||||
publicAccesLevel: publicAccessLevel,
|
||||
..._project
|
||||
},
|
||||
permissionsLevel,
|
||||
}: JoinProjectPayload) {
|
||||
const project = { ..._project, rootDocId }
|
||||
scopeStore.set('project', project)
|
||||
const project = { ..._project, rootDocId, publicAccessLevel }
|
||||
|
||||
// Cast the project from the payload as ProjectMetadata to ensure it has
|
||||
// the correct type for the context. It must be close enough because the
|
||||
// data structure hasn't changed and it worked previously. This type
|
||||
// coercion was previously sidestepped by adding the project to the scope
|
||||
// store, which does not enforce types.
|
||||
joinProject(project as unknown as ProjectMetadata)
|
||||
|
||||
setPermissionsLevel(permissionsLevel)
|
||||
// Make watchers update immediately
|
||||
scopeStore.flushUpdates()
|
||||
@@ -142,21 +152,12 @@ export const IdeReactProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
setProjectJoined(true)
|
||||
}
|
||||
|
||||
function handleMainBibliographyDocUpdated(payload: string) {
|
||||
scopeStore.set('project.mainBibliographyDoc_id', payload)
|
||||
}
|
||||
|
||||
socket.on('joinProjectResponse', handleJoinProjectResponse)
|
||||
socket.on('mainBibliographyDocUpdated', handleMainBibliographyDocUpdated)
|
||||
|
||||
return () => {
|
||||
socket.removeListener('joinProjectResponse', handleJoinProjectResponse)
|
||||
socket.removeListener(
|
||||
'mainBibliographyDocUpdated',
|
||||
handleMainBibliographyDocUpdated
|
||||
)
|
||||
}
|
||||
}, [socket, eventEmitter, scopeStore])
|
||||
}, [socket, eventEmitter, scopeStore, joinProject])
|
||||
|
||||
const ide = useMemo(() => {
|
||||
return {
|
||||
|
||||
@@ -124,7 +124,7 @@ export const OutlineProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
[openDocName]
|
||||
)
|
||||
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
const storageKey = `file_outline.expanded.${projectId}`
|
||||
|
||||
const [outlineExpanded, setOutlineExpanded] = useState(
|
||||
|
||||
@@ -98,7 +98,7 @@ export const PermissionsProvider: React.FC<React.PropsWithChildren> = ({
|
||||
const { permissionsLevel } = useIdeReactContext()
|
||||
const hasViewerPermissions = useViewerPermissions()
|
||||
const anonymous = getMeta('ol-anonymous')
|
||||
const project = useProjectContext()
|
||||
const { features } = useProjectContext()
|
||||
|
||||
useEffect(() => {
|
||||
let activePermissionsMap
|
||||
@@ -106,7 +106,7 @@ export const PermissionsProvider: React.FC<React.PropsWithChildren> = ({
|
||||
activePermissionsMap = linkSharingWarningPermissionsMap
|
||||
} else if (anonymous) {
|
||||
activePermissionsMap = anonymousPermissionsMap
|
||||
} else if (!project.features.trackChanges) {
|
||||
} else if (!features.trackChanges) {
|
||||
activePermissionsMap = noTrackChangesPermissionsMap
|
||||
} else {
|
||||
activePermissionsMap = permissionsMap
|
||||
@@ -117,7 +117,7 @@ export const PermissionsProvider: React.FC<React.PropsWithChildren> = ({
|
||||
permissionsLevel,
|
||||
setPermissions,
|
||||
hasViewerPermissions,
|
||||
project.features.trackChanges,
|
||||
features.trackChanges,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -75,10 +75,10 @@ export const ReactContextRoot: FC<
|
||||
<Providers.SplitTestProvider>
|
||||
<Providers.ModalsContextProvider>
|
||||
<Providers.ConnectionProvider>
|
||||
<Providers.IdeReactProvider>
|
||||
<Providers.UserProvider>
|
||||
<Providers.UserSettingsProvider>
|
||||
<Providers.ProjectProvider>
|
||||
<Providers.ProjectProvider>
|
||||
<Providers.IdeReactProvider>
|
||||
<Providers.UserProvider>
|
||||
<Providers.UserSettingsProvider>
|
||||
<Providers.SnapshotProvider>
|
||||
<Providers.DetachProvider>
|
||||
<Providers.EditorPropertiesProvider>
|
||||
@@ -128,10 +128,10 @@ export const ReactContextRoot: FC<
|
||||
</Providers.EditorPropertiesProvider>
|
||||
</Providers.DetachProvider>
|
||||
</Providers.SnapshotProvider>
|
||||
</Providers.ProjectProvider>
|
||||
</Providers.UserSettingsProvider>
|
||||
</Providers.UserProvider>
|
||||
</Providers.IdeReactProvider>
|
||||
</Providers.UserSettingsProvider>
|
||||
</Providers.UserProvider>
|
||||
</Providers.IdeReactProvider>
|
||||
</Providers.ProjectProvider>
|
||||
</Providers.ConnectionProvider>
|
||||
</Providers.ModalsContextProvider>
|
||||
</Providers.SplitTestProvider>
|
||||
|
||||
@@ -55,7 +55,7 @@ export const SnapshotContext = createContext<
|
||||
>(undefined)
|
||||
|
||||
export const SnapshotProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
const [snapshotLoadingState, setSnapshotLoadingState] =
|
||||
useState<SnapshotLoadingState>('')
|
||||
const [snapshotUpdater] = useState(() => new SnapshotUpdater(projectId))
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
listProjectInvites,
|
||||
listProjectMembers,
|
||||
} from '@/features/share-project-modal/utils/api'
|
||||
import useScopeValue from '@/shared/hooks/use-scope-value'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
|
||||
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
|
||||
import { useModalsContext } from '@/features/ide-react/context/modals-context'
|
||||
@@ -16,11 +16,9 @@ import { useLocation } from '@/shared/hooks/use-location'
|
||||
function useSocketListeners() {
|
||||
const { t } = useTranslation()
|
||||
const { socket } = useConnectionContext()
|
||||
const { permissionsLevel, projectId } = useIdeReactContext()
|
||||
const { showGenericMessageModal } = useModalsContext()
|
||||
const [, setPublicAccessLevel] = useScopeValue('project.publicAccesLevel')
|
||||
const [, setProjectMembers] = useScopeValue('project.members')
|
||||
const [, setProjectInvites] = useScopeValue('project.invites')
|
||||
const { permissionsLevel } = useIdeReactContext()
|
||||
const { projectId, updateProject } = useProjectContext()
|
||||
const location = useLocation()
|
||||
|
||||
useSocketListener(
|
||||
@@ -51,10 +49,10 @@ function useSocketListeners() {
|
||||
useCallback(
|
||||
(data: { newAccessLevel?: PublicAccessLevel }) => {
|
||||
if (data.newAccessLevel) {
|
||||
setPublicAccessLevel(data.newAccessLevel)
|
||||
updateProject({ publicAccessLevel: data.newAccessLevel })
|
||||
}
|
||||
},
|
||||
[setPublicAccessLevel]
|
||||
[updateProject]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -65,13 +63,13 @@ function useSocketListeners() {
|
||||
listProjectMembers(projectId)
|
||||
.then(({ members }) => {
|
||||
if (members) {
|
||||
setProjectMembers(members)
|
||||
updateProject({ members })
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
debugConsole.error('Error fetching members for project', err)
|
||||
})
|
||||
}, [projectId, setProjectMembers])
|
||||
}, [projectId, updateProject])
|
||||
)
|
||||
|
||||
useSocketListener(
|
||||
@@ -83,7 +81,7 @@ function useSocketListeners() {
|
||||
listProjectMembers(projectId)
|
||||
.then(({ members }) => {
|
||||
if (members) {
|
||||
setProjectMembers(members)
|
||||
updateProject({ members })
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -95,7 +93,7 @@ function useSocketListeners() {
|
||||
listProjectInvites(projectId)
|
||||
.then(({ invites }) => {
|
||||
if (invites) {
|
||||
setProjectInvites(invites)
|
||||
updateProject({ invites })
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -103,7 +101,29 @@ function useSocketListeners() {
|
||||
})
|
||||
}
|
||||
},
|
||||
[projectId, setProjectInvites, setProjectMembers, permissionsLevel]
|
||||
[projectId, updateProject, permissionsLevel]
|
||||
)
|
||||
)
|
||||
|
||||
useSocketListener(
|
||||
socket,
|
||||
'mainBibliographyDocUpdated',
|
||||
useCallback(
|
||||
(payload: string) => {
|
||||
updateProject({ mainBibliographyDocId: payload })
|
||||
},
|
||||
[updateProject]
|
||||
)
|
||||
)
|
||||
|
||||
useSocketListener(
|
||||
socket,
|
||||
'projectNameUpdated',
|
||||
useCallback(
|
||||
(payload: string) => {
|
||||
updateProject({ name: payload })
|
||||
},
|
||||
[updateProject]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'
|
||||
|
||||
export const DownloadProjectZip = () => {
|
||||
const { t } = useTranslation()
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
const sendDownloadEvent = useCallback(() => {
|
||||
sendMB('download-zip-button-click', {
|
||||
projectId,
|
||||
@@ -44,7 +44,7 @@ export const DownloadProjectZip = () => {
|
||||
export const DownloadProjectPDF = () => {
|
||||
const { t } = useTranslation()
|
||||
const { pdfDownloadUrl, pdfUrl } = useCompileContext()
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
const sendDownloadEvent = useCallback(() => {
|
||||
sendMB('download-pdf-button-click', {
|
||||
projectId,
|
||||
|
||||
@@ -9,7 +9,7 @@ import MaterialIcon from '@/shared/components/material-icon'
|
||||
function PdfHybridDownloadButton() {
|
||||
const { pdfDownloadUrl } = useCompileContext()
|
||||
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
|
||||
const { t } = useTranslation()
|
||||
const description = pdfDownloadUrl
|
||||
|
||||
@@ -23,7 +23,7 @@ type PdfJsViewerProps = {
|
||||
}
|
||||
|
||||
function PdfJsViewer({ url, pdfFile }: PdfJsViewerProps) {
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
|
||||
const { setError, firstRenderDone, highlights, position, setPosition } =
|
||||
useCompileContext()
|
||||
|
||||
@@ -28,7 +28,8 @@ export default function useSynctex(): {
|
||||
syncToCodeInFlight: boolean
|
||||
canSyncToPdf: boolean
|
||||
} {
|
||||
const { _id: projectId, rootDocId } = useProjectContext()
|
||||
const { projectId, project } = useProjectContext()
|
||||
const rootDocId = project?.rootDocId
|
||||
|
||||
const { clsiServerId, pdfFile, position, setShowLogs, setHighlights } =
|
||||
useCompileContext()
|
||||
|
||||
@@ -55,7 +55,7 @@ function ReviewModeSwitcher() {
|
||||
const mode = useCurrentMode()
|
||||
const { permissionsLevel } = useIdeReactContext()
|
||||
const { write, trackedWrite } = usePermissionsContext()
|
||||
const project = useProjectContext()
|
||||
const { features } = useProjectContext()
|
||||
const [showUpgradeModal, setShowUpgradeModal] = useState(false)
|
||||
const showViewOption = permissionsLevel === 'readOnly'
|
||||
const view = useCodeMirrorViewContext()
|
||||
@@ -100,7 +100,7 @@ function ReviewModeSwitcher() {
|
||||
view.focus()
|
||||
return
|
||||
}
|
||||
if (!project.features.trackChanges) {
|
||||
if (!features.trackChanges) {
|
||||
setShowUpgradeModal(true)
|
||||
} else {
|
||||
sendMB('editing-mode-change', {
|
||||
@@ -212,7 +212,7 @@ const ModeSwitcherToggleButtonContent = forwardRef<
|
||||
})
|
||||
|
||||
const user = useUserContext()
|
||||
const project = useProjectContext()
|
||||
const { features } = useProjectContext()
|
||||
const { reviewPanelOpen } = useLayoutContext()
|
||||
const { inactiveTutorials } = useEditorContext()
|
||||
|
||||
@@ -222,7 +222,7 @@ const ModeSwitcherToggleButtonContent = forwardRef<
|
||||
const canShowReviewModePromo =
|
||||
reviewPanelOpen &&
|
||||
currentMode !== 'review' &&
|
||||
project.features.trackChanges &&
|
||||
features.trackChanges &&
|
||||
user.signUpDate &&
|
||||
user.signUpDate < '2025-03-15' &&
|
||||
!hasCompletedReviewModeTutorial
|
||||
|
||||
@@ -26,7 +26,7 @@ function UpgradeTrackChangesModal({
|
||||
setShow,
|
||||
}: UpgradeTrackChangesModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const project = useProjectContext()
|
||||
const { project } = useProjectContext()
|
||||
const user = useUserContext()
|
||||
|
||||
return (
|
||||
@@ -62,9 +62,9 @@ function UpgradeTrackChangesModal({
|
||||
</ul>
|
||||
</OLCol>
|
||||
</OLRow>
|
||||
{project.owner && (
|
||||
{Boolean(project?.owner) && (
|
||||
<div className="text-center">
|
||||
{project.owner._id === user.id ? (
|
||||
{project?.owner._id === user.id ? (
|
||||
user.allowedFreeTrial ? (
|
||||
<OLButton
|
||||
variant="premium"
|
||||
|
||||
@@ -29,7 +29,8 @@ export const ChangesUsersContext = createContext<ChangesUsers | undefined>(
|
||||
export const ChangesUsersProvider: FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { _id: projectId, members, owner } = useProjectContext()
|
||||
const { projectId, project } = useProjectContext()
|
||||
const { members, owner } = project || {}
|
||||
const { isRestrictedTokenMember } = useEditorContext()
|
||||
|
||||
const [changesUsers, setChangesUsers] = useState<ChangesUsers>()
|
||||
@@ -49,6 +50,9 @@ export const ChangesUsersProvider: FC<React.PropsWithChildren> = ({
|
||||
|
||||
// add the project owner and members to the changes users data
|
||||
const value = useMemo(() => {
|
||||
if (!owner || !members) {
|
||||
return
|
||||
}
|
||||
const value: ChangesUsers = new Map(changesUsers)
|
||||
value.set(owner._id, { ...owner, id: owner._id })
|
||||
for (const member of members) {
|
||||
|
||||
@@ -49,7 +49,7 @@ const ThreadsActionsContext = createContext<ThreadsActions | undefined>(
|
||||
)
|
||||
|
||||
export const ThreadsProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
const { currentDocument } = useEditorOpenDocContext()
|
||||
const { isRestrictedTokenMember } = useEditorContext()
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useEditorPropertiesContext } from '@/features/ide-react/context/editor-
|
||||
import { useUserContext } from '@/shared/context/user-context'
|
||||
import { postJSON } from '@/infrastructure/fetch-json'
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
import { ProjectContextValue } from '@/shared/context/types/project-context'
|
||||
import { ProjectMetadata } from '@/shared/context/types/project-metadata'
|
||||
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
|
||||
|
||||
export type TrackChangesState = {
|
||||
@@ -48,14 +48,14 @@ export const TrackChangesStateProvider: FC<React.PropsWithChildren> = ({
|
||||
}) => {
|
||||
const permissions = usePermissionsContext()
|
||||
const { socket } = useConnectionContext()
|
||||
const project = useProjectContext()
|
||||
const { projectId, project, features } = useProjectContext()
|
||||
const user = useUserContext()
|
||||
const { setWantTrackChanges } = useEditorPropertiesContext()
|
||||
|
||||
// TODO: update project.trackChangesState instead?
|
||||
const [trackChangesValue, setTrackChangesValue] = useState<
|
||||
ProjectContextValue['trackChangesState']
|
||||
>(project.trackChangesState ?? false)
|
||||
ProjectMetadata['trackChangesState']
|
||||
>(project?.trackChangesState ?? false)
|
||||
|
||||
useSocketListener(socket, 'toggle-track-changes', setTrackChangesValue)
|
||||
|
||||
@@ -88,11 +88,11 @@ export const TrackChangesStateProvider: FC<React.PropsWithChildren> = ({
|
||||
|
||||
const saveTrackChanges = useCallback(
|
||||
async (trackChangesBody: SaveTrackChangesRequestBody) => {
|
||||
postJSON(`/project/${project._id}/track_changes`, {
|
||||
postJSON(`/project/${projectId}/track_changes`, {
|
||||
body: trackChangesBody,
|
||||
})
|
||||
},
|
||||
[project._id]
|
||||
[projectId]
|
||||
)
|
||||
|
||||
const saveTrackChangesForCurrentUser = useCallback(
|
||||
@@ -122,7 +122,7 @@ export const TrackChangesStateProvider: FC<React.PropsWithChildren> = ({
|
||||
useCallback(() => {
|
||||
if (
|
||||
user.id &&
|
||||
project.features.trackChanges &&
|
||||
features.trackChanges &&
|
||||
permissions.write &&
|
||||
!onForEveryone
|
||||
) {
|
||||
@@ -139,7 +139,7 @@ export const TrackChangesStateProvider: FC<React.PropsWithChildren> = ({
|
||||
onForMembers,
|
||||
onForEveryone,
|
||||
permissions.write,
|
||||
project.features.trackChanges,
|
||||
features.trackChanges,
|
||||
user.id,
|
||||
])
|
||||
)
|
||||
|
||||
@@ -23,7 +23,7 @@ const safeParse = (value: string) => {
|
||||
}
|
||||
|
||||
export default function useOverviewFileCollapsed(docId: DocId) {
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
const [collapsedDocs, setCollapsedDocs] = usePersistedState<
|
||||
Record<DocId, boolean>,
|
||||
string
|
||||
|
||||
@@ -6,7 +6,7 @@ import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
|
||||
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
|
||||
|
||||
export default function useProjectRanges() {
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
const [error, setError] = useState<Error>()
|
||||
const [projectRanges, setProjectRanges] = useState<Map<string, Ranges>>()
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
@@ -23,9 +23,10 @@ export default function AddCollaborators({ readOnly }: { readOnly?: boolean }) {
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { updateProject, setInFlight, setError } = useShareProjectContext()
|
||||
const { setInFlight, setError } = useShareProjectContext()
|
||||
|
||||
const { _id: projectId, members, invites, features } = useProjectContext()
|
||||
const { projectId, project, features, updateProject } = useProjectContext()
|
||||
const { members, invites } = project || {}
|
||||
|
||||
const currentMemberEmails = useMemo(
|
||||
() => (members || []).map(member => member.email).sort(),
|
||||
@@ -82,9 +83,7 @@ export default function AddCollaborators({ readOnly }: { readOnly?: boolean }) {
|
||||
let data
|
||||
|
||||
try {
|
||||
const invite = (invites || []).find(
|
||||
invite => invite.email === normalisedEmail
|
||||
)
|
||||
const invite = invites?.find(invite => invite.email === normalisedEmail)
|
||||
|
||||
if (invite) {
|
||||
data = await resendInvite(projectId, invite)
|
||||
@@ -109,8 +108,8 @@ export default function AddCollaborators({ readOnly }: { readOnly?: boolean }) {
|
||||
// invitation is only populated on successful invite, meaning that for paywall and other cases this will be null
|
||||
successful_invite: !!data.invite,
|
||||
users_updated: !!(data.users || data.user),
|
||||
current_collaborators_amount: members.length,
|
||||
current_invites_amount: invites.length,
|
||||
current_collaborators_amount: members?.length || 0,
|
||||
current_invites_amount: invites?.length || 0,
|
||||
role,
|
||||
previousEditorsAmount,
|
||||
previousReviewersAmount,
|
||||
@@ -144,15 +143,15 @@ export default function AddCollaborators({ readOnly }: { readOnly?: boolean }) {
|
||||
setInFlight(false)
|
||||
} else if (data.invite) {
|
||||
updateProject({
|
||||
invites: invites.concat(data.invite),
|
||||
invites: invites?.concat(data.invite) || [data.invite],
|
||||
})
|
||||
} else if (data.users) {
|
||||
updateProject({
|
||||
members: members.concat(data.users),
|
||||
members: members?.concat(data.users) || data.users,
|
||||
})
|
||||
} else if (data.user) {
|
||||
updateProject({
|
||||
members: members.concat(data.user),
|
||||
members: members?.concat(data.user) || [data.user],
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { removeMemberFromProject, updateMember } from '../utils/api'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import { sendMB } from '@/infrastructure/event-tracking'
|
||||
import { Select } from '@/shared/components/select'
|
||||
import type { ProjectContextMember } from '@/shared/context/types/project-context'
|
||||
import type { ProjectMember } from '@/shared/context/types/project-metadata'
|
||||
import { PermissionsLevel } from '@/features/ide-react/types/permissions'
|
||||
import { linkSharingEnforcementDate } from '../utils/link-sharing'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
@@ -19,7 +19,7 @@ import { upgradePlan } from '@/main/account-upgrade'
|
||||
type PermissionsOption = PermissionsLevel | 'removeAccess' | 'downgraded'
|
||||
|
||||
type EditMemberProps = {
|
||||
member: ProjectContextMember
|
||||
member: ProjectMember
|
||||
hasExceededCollaboratorLimit: boolean
|
||||
hasBeenDowngraded: boolean
|
||||
canAddCollaborators: boolean
|
||||
@@ -51,8 +51,9 @@ export default function EditMember({
|
||||
setPrivileges(member.privileges)
|
||||
}, [member.privileges])
|
||||
|
||||
const { updateProject, monitorRequest } = useShareProjectContext()
|
||||
const { _id: projectId, members, invites } = useProjectContext()
|
||||
const { monitorRequest } = useShareProjectContext()
|
||||
const { projectId, project, updateProject } = useProjectContext()
|
||||
const { members, invites } = project || {}
|
||||
const user = useUserContext()
|
||||
|
||||
// Immediately commit this change if it's lower impact (eg. editor > viewer)
|
||||
@@ -88,14 +89,15 @@ export default function EditMember({
|
||||
} else if (newPrivileges === 'removeAccess') {
|
||||
monitorRequest(() => removeMemberFromProject(projectId, member)).then(
|
||||
() => {
|
||||
const updatedMembers = members.filter(existing => existing !== member)
|
||||
const updatedMembers =
|
||||
members?.filter(existing => existing !== member) || []
|
||||
updateProject({
|
||||
members: updatedMembers,
|
||||
})
|
||||
sendMB('collaborator-removed', {
|
||||
project_id: projectId,
|
||||
current_collaborators_amount: updatedMembers.length,
|
||||
current_invites_amount: invites.length,
|
||||
current_invites_amount: invites?.length || 0,
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -110,9 +112,10 @@ export default function EditMember({
|
||||
})
|
||||
).then(() => {
|
||||
updateProject({
|
||||
members: members.map(item =>
|
||||
item._id === member._id ? { ...item, newPrivileges } : item
|
||||
),
|
||||
members:
|
||||
members?.map(item =>
|
||||
item._id === member._id ? { ...item, newPrivileges } : item
|
||||
) || [],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ const EditorOverLimitModal = () => {
|
||||
|
||||
const { isProjectOwner } = useEditorContext()
|
||||
const { permissionsLevel } = useIdeReactContext()
|
||||
const { members, features, _id: projectId } = useProjectContext()
|
||||
const { project, features, projectId } = useProjectContext()
|
||||
const members = project?.members
|
||||
|
||||
const handleHide = () => {
|
||||
setShow(false)
|
||||
@@ -24,7 +25,12 @@ const EditorOverLimitModal = () => {
|
||||
useEffect(() => {
|
||||
const showModalCooldownHours = 24
|
||||
const hasExceededCollaboratorLimit = () => {
|
||||
if (isProjectOwner || !features || permissionsLevel === 'readOnly') {
|
||||
if (
|
||||
isProjectOwner ||
|
||||
!features ||
|
||||
!members ||
|
||||
permissionsLevel === 'readOnly'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@ import OLCol from '@/features/ui/components/ol/ol-col'
|
||||
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { ProjectContextMember } from '@/shared/context/types/project-context'
|
||||
import { ProjectMember } from '@/shared/context/types/project-metadata'
|
||||
|
||||
export default function Invite({
|
||||
invite,
|
||||
isProjectOwner,
|
||||
}: {
|
||||
invite: ProjectContextMember
|
||||
invite: ProjectMember
|
||||
isProjectOwner: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
@@ -44,10 +44,10 @@ export default function Invite({
|
||||
)
|
||||
}
|
||||
|
||||
function ResendInvite({ invite }: { invite: ProjectContextMember }) {
|
||||
function ResendInvite({ invite }: { invite: ProjectMember }) {
|
||||
const { t } = useTranslation()
|
||||
const { monitorRequest, setError, inFlight } = useShareProjectContext()
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
|
||||
// const buttonRef = useRef(null)
|
||||
//
|
||||
@@ -87,23 +87,25 @@ function ResendInvite({ invite }: { invite: ProjectContextMember }) {
|
||||
)
|
||||
}
|
||||
|
||||
function RevokeInvite({ invite }: { invite: ProjectContextMember }) {
|
||||
function RevokeInvite({ invite }: { invite: ProjectMember }) {
|
||||
const { t } = useTranslation()
|
||||
const { updateProject, monitorRequest } = useShareProjectContext()
|
||||
const { _id: projectId, invites, members } = useProjectContext()
|
||||
const { monitorRequest } = 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)
|
||||
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,
|
||||
current_collaborators_amount: members?.length || 0,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -33,7 +33,8 @@ export default function LinkSharing() {
|
||||
|
||||
const { monitorRequest } = useShareProjectContext()
|
||||
|
||||
const { _id: projectId, publicAccessLevel } = useProjectContext()
|
||||
const { projectId, project } = useProjectContext()
|
||||
const { publicAccessLevel } = project || {}
|
||||
|
||||
// set the access level of a project
|
||||
const setAccessLevel = useCallback(
|
||||
@@ -145,7 +146,7 @@ function TokenBasedSharing({
|
||||
showLinks: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
|
||||
const [tokens, setTokens] = useState<Tokens | null>(null)
|
||||
|
||||
@@ -244,7 +245,7 @@ function LegacySharing({
|
||||
|
||||
export function ReadOnlyTokenLink() {
|
||||
const { t } = useTranslation()
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
|
||||
const [tokens, setTokens] = useState<Tokens | null>(null)
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ProjectContextMember } from '@/shared/context/types/project-context'
|
||||
import { ProjectMember } from '@/shared/context/types/project-metadata'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function MemberPrivileges({
|
||||
privileges,
|
||||
}: {
|
||||
privileges: ProjectContextMember['privileges']
|
||||
privileges: ProjectMember['privileges']
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
||||
@@ -6,14 +6,14 @@ import MaterialIcon from '@/shared/components/material-icon'
|
||||
|
||||
export default function OwnerInfo() {
|
||||
const { t } = useTranslation()
|
||||
const { owner } = useProjectContext()
|
||||
const { project } = useProjectContext()
|
||||
|
||||
return (
|
||||
<OLRow className="project-member">
|
||||
<OLCol xs={8}>
|
||||
<div className="project-member-email-icon">
|
||||
<MaterialIcon type="person" />
|
||||
<div className="email-warning">{owner?.email}</div>
|
||||
<div className="email-warning">{project?.owner.email}</div>
|
||||
</div>
|
||||
</OLCol>
|
||||
<OLCol xs={4} className="text-end">
|
||||
|
||||
@@ -7,7 +7,8 @@ import OLRow from '@/features/ui/components/ol/ol-row'
|
||||
import OLCol from '@/features/ui/components/ol/ol-col'
|
||||
|
||||
export default function SendInvitesNotice() {
|
||||
const { publicAccessLevel } = useProjectContext()
|
||||
const { project } = useProjectContext()
|
||||
const { publicAccessLevel } = project || {}
|
||||
const { isPendingEditor } = useEditorContext()
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ import RecaptchaConditions from '@/shared/components/recaptcha-conditions'
|
||||
import getMeta from '@/utils/meta'
|
||||
|
||||
export default function ShareModalBody() {
|
||||
const { members, invites, features } = useProjectContext()
|
||||
const { project, features } = useProjectContext()
|
||||
const { members, invites } = project || {}
|
||||
const { isProjectOwner } = useEditorContext()
|
||||
|
||||
// whether the project has not reached the collaborator limit
|
||||
@@ -26,12 +27,12 @@ export default function ShareModalBody() {
|
||||
return true
|
||||
}
|
||||
|
||||
const editorInvites = invites.filter(
|
||||
invite => invite.privileges !== 'readOnly'
|
||||
).length
|
||||
const editorInvites =
|
||||
invites?.filter(invite => invite.privileges !== 'readOnly').length || 0
|
||||
|
||||
return (
|
||||
members.filter(member => member.privileges !== 'readOnly').length +
|
||||
(members?.filter(member => member.privileges !== 'readOnly').length ||
|
||||
0) +
|
||||
editorInvites <
|
||||
(features.collaborators ?? 1)
|
||||
)
|
||||
@@ -40,11 +41,11 @@ export default function ShareModalBody() {
|
||||
// determine if some but not all pending editors' permissions have been resolved,
|
||||
// for moving between warning and info notification states etc.
|
||||
const somePendingEditorsResolved = useMemo(() => {
|
||||
return (
|
||||
members.some(member =>
|
||||
return Boolean(
|
||||
members?.some(member =>
|
||||
['readAndWrite', 'review'].includes(member.privileges)
|
||||
) &&
|
||||
members.some(member => member.pendingEditor || member.pendingReviewer)
|
||||
members?.some(member => member.pendingEditor || member.pendingReviewer)
|
||||
)
|
||||
}, [members])
|
||||
|
||||
@@ -56,13 +57,14 @@ export default function ShareModalBody() {
|
||||
if (features.collaborators === -1) {
|
||||
return false
|
||||
}
|
||||
return members.some(
|
||||
member => member.pendingEditor || member.pendingReviewer
|
||||
return (
|
||||
members?.some(member => member.pendingEditor || member.pendingReviewer) ||
|
||||
false
|
||||
)
|
||||
}, [features, isProjectOwner, members])
|
||||
|
||||
const hasExceededCollaboratorLimit = useMemo(() => {
|
||||
if (!isProjectOwner || !features) {
|
||||
if (!isProjectOwner || !features || !members) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -77,6 +79,9 @@ export default function ShareModalBody() {
|
||||
}, [features, isProjectOwner, members])
|
||||
|
||||
const sortedMembers = useMemo(() => {
|
||||
if (!members) {
|
||||
return []
|
||||
}
|
||||
return [
|
||||
...members.filter(member => member.privileges === 'readAndWrite'),
|
||||
...members.filter(member => member.pendingEditor),
|
||||
@@ -126,7 +131,7 @@ export default function ShareModalBody() {
|
||||
)
|
||||
)}
|
||||
|
||||
{invites.map(invite => (
|
||||
{(invites || []).map(invite => (
|
||||
<Invite
|
||||
key={invite._id}
|
||||
invite={invite}
|
||||
|
||||
@@ -9,12 +9,10 @@ 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<
|
||||
@@ -61,7 +59,7 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
|
||||
useState<ShareProjectContextValue['inFlight']>(false)
|
||||
const [error, setError] = useState<ShareProjectContextValue['error']>()
|
||||
|
||||
const project = useProjectContext()
|
||||
const { project, projectId } = useProjectContext()
|
||||
const { isProjectOwner } = useEditorContext()
|
||||
|
||||
const { splitTestVariants } = useSplitTestContext()
|
||||
@@ -70,7 +68,7 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
|
||||
// is over collaborator limit or has pending editors (once every 24 hours)
|
||||
useEffect(() => {
|
||||
const hasExceededCollaboratorLimit = () => {
|
||||
if (!isProjectOwner || !project.features) {
|
||||
if (!isProjectOwner || !project || !project.features) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -88,7 +86,7 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
|
||||
}
|
||||
|
||||
if (hasExceededCollaboratorLimit()) {
|
||||
const localStorageKey = `last-shown-share-modal.${project._id}`
|
||||
const localStorageKey = `last-shown-share-modal.${projectId}`
|
||||
const lastShownShareModalTime =
|
||||
customLocalStorage.getItem(localStorageKey)
|
||||
if (
|
||||
@@ -99,17 +97,17 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
|
||||
customLocalStorage.setItem(localStorageKey, Date.now())
|
||||
}
|
||||
}
|
||||
}, [project, isProjectOwner, handleOpen])
|
||||
}, [project, isProjectOwner, handleOpen, projectId])
|
||||
|
||||
// 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,
|
||||
project_id: projectId,
|
||||
})
|
||||
}
|
||||
}, [splitTestVariants, project._id, show])
|
||||
}, [splitTestVariants, projectId, show])
|
||||
|
||||
// reset error when the modal is opened
|
||||
useEffect(() => {
|
||||
@@ -147,12 +145,6 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
|
||||
return promise
|
||||
}, [])
|
||||
|
||||
// merge the new data with the old project data
|
||||
const updateProject = useCallback(
|
||||
(data: ProjectContextUpdateValue) => Object.assign(project, data),
|
||||
[project]
|
||||
)
|
||||
|
||||
if (!project) {
|
||||
return null
|
||||
}
|
||||
@@ -160,7 +152,6 @@ const ShareProjectModal = React.memo(function ShareProjectModal({
|
||||
return (
|
||||
<ShareProjectContext.Provider
|
||||
value={{
|
||||
updateProject,
|
||||
monitorRequest,
|
||||
inFlight,
|
||||
setInFlight,
|
||||
|
||||
@@ -12,13 +12,13 @@ import OLModal, {
|
||||
import OLNotification from '@/features/ui/components/ol/ol-notification'
|
||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||
import { Spinner } from 'react-bootstrap'
|
||||
import { ProjectContextMember } from '@/shared/context/types/project-context'
|
||||
import { ProjectMember } from '@/shared/context/types/project-metadata'
|
||||
|
||||
export default function TransferOwnershipModal({
|
||||
member,
|
||||
cancel,
|
||||
}: {
|
||||
member: ProjectContextMember
|
||||
member: ProjectMember
|
||||
cancel: () => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
@@ -27,7 +27,7 @@ export default function TransferOwnershipModal({
|
||||
const [error, setError] = useState(false)
|
||||
const location = useLocation()
|
||||
|
||||
const { _id: projectId, name: projectName } = useProjectContext()
|
||||
const { projectId, name: projectName } = useProjectContext()
|
||||
|
||||
function confirm() {
|
||||
setError(false)
|
||||
|
||||
@@ -2,13 +2,9 @@ import MemberPrivileges from './member-privileges'
|
||||
import OLRow from '@/features/ui/components/ol/ol-row'
|
||||
import OLCol from '@/features/ui/components/ol/ol-col'
|
||||
import MaterialIcon from '@/shared/components/material-icon'
|
||||
import { ProjectContextMember } from '@/shared/context/types/project-context'
|
||||
import { ProjectMember } from '@/shared/context/types/project-metadata'
|
||||
|
||||
export default function ViewMember({
|
||||
member,
|
||||
}: {
|
||||
member: ProjectContextMember
|
||||
}) {
|
||||
export default function ViewMember({ member }: { member: ProjectMember }) {
|
||||
return (
|
||||
<OLRow className="project-member">
|
||||
<OLCol xs={8}>
|
||||
|
||||
@@ -12,7 +12,8 @@ const ViewOnlyAccessModal = () => {
|
||||
|
||||
const { isProjectOwner, isPendingEditor } = useEditorContext()
|
||||
const { permissionsLevel } = useIdeReactContext()
|
||||
const { members, features, _id: projectId } = useProjectContext()
|
||||
const { features, projectId, project } = useProjectContext()
|
||||
const members = project?.members
|
||||
|
||||
const handleHide = () => {
|
||||
setShow(false)
|
||||
|
||||
@@ -34,7 +34,7 @@ export const FigureModalOtherProjectSource: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const view = useCodeMirrorViewContext()
|
||||
const { dispatch } = useFigureModalContext()
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
const { loading: projectsLoading, data: projects, error } = useUserProjects()
|
||||
const [selectedProject, setSelectedProject] = useState<null | Project>(null)
|
||||
const { hasLinkedProjectFileFeature, hasLinkedProjectOutputFileFeature } =
|
||||
|
||||
@@ -34,7 +34,7 @@ export const FigureModalUploadFileSource: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const view = useCodeMirrorViewContext()
|
||||
const { dispatch, pastedImageData } = useFigureModalContext()
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
const { rootFile } = useCurrentProjectFolders()
|
||||
const [folder, setFolder] = useState<File | null>(null)
|
||||
const [nameDirty, setNameDirty] = useState<boolean>(false)
|
||||
|
||||
@@ -46,7 +46,7 @@ export const FigureModalUrlSource: FC = () => {
|
||||
const [url, setUrl] = useState<string>('')
|
||||
const [nameDirty, setNameDirty] = useState<boolean>(false)
|
||||
const [name, setName] = useState<string>('')
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
const { rootFile } = useCurrentProjectFolders()
|
||||
const [folder, setFolder] = useState<File>(rootFile)
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { EditorState } from '@codemirror/state'
|
||||
import useScopeValue from '../../../shared/hooks/use-scope-value'
|
||||
import useScopeEventEmitter from '../../../shared/hooks/use-scope-event-emitter'
|
||||
import useEventListener from '../../../shared/hooks/use-event-listener'
|
||||
import useScopeEventListener from '../../../shared/hooks/use-scope-event-listener'
|
||||
@@ -55,6 +54,7 @@ import { GotoOffsetOptions } from '@/features/ide-react/context/editor-manager-c
|
||||
import { GotoLineOptions } from '@/features/ide-react/types/goto-line-options'
|
||||
import { useOnlineUsersContext } from '@/features/ide-react/context/online-users-context'
|
||||
import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
|
||||
import { useEditorPropertiesContext } from '@/features/ide-react/context/editor-properties-context'
|
||||
import { SearchQuery } from '@codemirror/search'
|
||||
@@ -92,17 +92,13 @@ function useCodeMirrorScope(view: EditorView) {
|
||||
|
||||
const { onlineUserCursorHighlights } = useOnlineUsersContext()
|
||||
|
||||
let [spellCheckLanguage] = useScopeValue<string>('project.spellCheckLanguage')
|
||||
const { project, features: projectFeatures } = useProjectContext()
|
||||
let spellCheckLanguage = project?.spellCheckLanguage || ''
|
||||
// spell check is off when read-only
|
||||
if (!permissions.write && !permissions.trackedWrite) {
|
||||
spellCheckLanguage = ''
|
||||
}
|
||||
|
||||
const [projectFeatures] =
|
||||
useScopeValue<Record<string, boolean | string | number | undefined>>(
|
||||
'project.features'
|
||||
)
|
||||
|
||||
const hunspellManager = useHunspell(spellCheckLanguage)
|
||||
|
||||
const { showVisual: visual, trackChanges } = useEditorPropertiesContext()
|
||||
|
||||
@@ -19,7 +19,8 @@ export const WordCountClient: FC = () => {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(false)
|
||||
const [data, setData] = useState<WordCountData | null>(null)
|
||||
const { projectSnapshot, rootDocId } = useProjectContext()
|
||||
const { projectSnapshot, project } = useProjectContext()
|
||||
const rootDocId = project?.rootDocId
|
||||
const { spellCheckLanguage } = useProjectSettingsContext()
|
||||
const { openDocs } = useEditorManagerContext()
|
||||
const { currentDocument } = useEditorOpenDocContext()
|
||||
|
||||
@@ -10,7 +10,7 @@ import { debugConsole } from '@/utils/debugging'
|
||||
import { WordCounts } from '@/features/word-count-modal/components/word-counts'
|
||||
|
||||
export const WordCountServer: FC = () => {
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
const { clsiServerId } = useLocalCompileContext()
|
||||
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
@@ -9,9 +9,7 @@ import {
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import useScopeValue from '../hooks/use-scope-value'
|
||||
import useBrowserWindow from '../hooks/use-browser-window'
|
||||
import { useIdeContext } from './ide-context'
|
||||
import { useProjectContext } from './project-context'
|
||||
import { useDetachContext } from './detach-context'
|
||||
import getMeta from '../../utils/meta'
|
||||
@@ -46,12 +44,18 @@ export const EditorContext = createContext<
|
||||
>(undefined)
|
||||
|
||||
export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
const { socket } = useIdeContext()
|
||||
const { id: userId, featureUsage } = useUserContext()
|
||||
const { role } = useDetachContext()
|
||||
const { showGenericMessageModal } = useModalsContext()
|
||||
|
||||
const { owner, features, _id: projectId, members } = useProjectContext()
|
||||
const {
|
||||
features,
|
||||
projectId,
|
||||
project,
|
||||
name: projectName,
|
||||
updateProject,
|
||||
} = useProjectContext()
|
||||
const { owner, members } = project || {}
|
||||
|
||||
const cobranding = useMemo(() => {
|
||||
const brandVariation = getMeta('ol-brandVariation')
|
||||
@@ -71,8 +75,6 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
)
|
||||
}, [])
|
||||
|
||||
const [projectName, setProjectName] = useScopeValue('project.name')
|
||||
|
||||
const [inactiveTutorials, setInactiveTutorials] = useState(
|
||||
() => getMeta('ol-inactiveTutorials') || []
|
||||
)
|
||||
@@ -95,10 +97,12 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
|
||||
const isPendingEditor = useMemo(
|
||||
() =>
|
||||
members?.some(
|
||||
member =>
|
||||
member._id === userId &&
|
||||
(member.pendingEditor || member.pendingReviewer)
|
||||
Boolean(
|
||||
members?.some(
|
||||
member =>
|
||||
member._id === userId &&
|
||||
(member.pendingEditor || member.pendingReviewer)
|
||||
)
|
||||
),
|
||||
[members, userId]
|
||||
)
|
||||
@@ -110,33 +114,25 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
[inactiveTutorials]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (socket) {
|
||||
socket.on('projectNameUpdated', setProjectName)
|
||||
return () => socket.removeListener('projectNameUpdated', setProjectName)
|
||||
}
|
||||
}, [socket, setProjectName])
|
||||
|
||||
const renameProject = useCallback(
|
||||
(newName: string) => {
|
||||
setProjectName((oldName: string) => {
|
||||
if (oldName !== newName) {
|
||||
saveProjectSettings(projectId, { name: newName }).catch(
|
||||
(response: any) => {
|
||||
setProjectName(oldName)
|
||||
const { data, status } = response
|
||||
const oldName = projectName
|
||||
if (newName !== oldName) {
|
||||
updateProject({ name: newName })
|
||||
saveProjectSettings(projectId, { name: newName }).catch(
|
||||
(response: any) => {
|
||||
updateProject({ name: oldName })
|
||||
const { data, status } = response
|
||||
|
||||
showGenericMessageModal(
|
||||
'Error renaming project',
|
||||
status === 400 ? data : 'Please try again in a moment'
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
return newName
|
||||
})
|
||||
showGenericMessageModal(
|
||||
'Error renaming project',
|
||||
status === 400 ? data : 'Please try again in a moment'
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
[setProjectName, projectId, showGenericMessageModal]
|
||||
[projectName, updateProject, projectId, showGenericMessageModal]
|
||||
)
|
||||
|
||||
const { setTitle } = useBrowserWindow()
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
FC,
|
||||
useEffect,
|
||||
} from 'react'
|
||||
import useScopeValue from '../hooks/use-scope-value'
|
||||
import {
|
||||
renameInTree,
|
||||
deleteInTree,
|
||||
@@ -20,7 +19,6 @@ import useDeepCompareEffect from '../../shared/hooks/use-deep-compare-effect'
|
||||
import { docsInFolder } from '@/features/file-tree/util/docs-in-folder'
|
||||
import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
|
||||
import { Folder } from '../../../../types/folder'
|
||||
import { Project } from '../../../../types/project'
|
||||
import { MainDocument } from '../../../../types/project-settings'
|
||||
import { FindResult } from '@/features/file-tree/util/path'
|
||||
import {
|
||||
@@ -28,6 +26,7 @@ import {
|
||||
useSnapshotContext,
|
||||
} from '@/features/ide-react/context/snapshot-context'
|
||||
import importOverleafModules from '../../../macros/import-overleaf-module.macro'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
|
||||
|
||||
const { buildFileTree, createFolder } =
|
||||
@@ -182,7 +181,7 @@ export function useFileTreeData() {
|
||||
export const FileTreeDataProvider: FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [project] = useScopeValue<Project>('project')
|
||||
const { project } = useProjectContext()
|
||||
const { currentDocumentId, setOpenDocName } = useEditorOpenDocContext()
|
||||
const { permissionsLevel } = useIdeReactContext()
|
||||
const { fileTreeFromHistory, snapshot, snapshotVersion } =
|
||||
@@ -195,7 +194,7 @@ export const FileTreeDataProvider: FC<React.PropsWithChildren> = ({
|
||||
useEffect(() => {
|
||||
if (fileTreeFromHistory) return
|
||||
setRootFolder(project?.rootFolder)
|
||||
}, [project, fileTreeFromHistory])
|
||||
}, [project?.rootFolder, fileTreeFromHistory])
|
||||
|
||||
useEffect(() => {
|
||||
if (!fileTreeFromHistory) return
|
||||
|
||||
@@ -131,13 +131,8 @@ export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
|
||||
const { currentDocument } = useEditorOpenDocContext()
|
||||
const { role } = useDetachContext()
|
||||
|
||||
const {
|
||||
_id: projectId,
|
||||
rootDocId,
|
||||
joinedOnce,
|
||||
imageName,
|
||||
compiler: compilerName,
|
||||
} = useProjectContext()
|
||||
const { projectId, joinedOnce, project } = useProjectContext()
|
||||
const { rootDocId, imageName, compiler: compilerName } = project || {}
|
||||
|
||||
const { pdfPreviewOpen } = useLayoutContext()
|
||||
|
||||
|
||||
@@ -1,10 +1,31 @@
|
||||
import { FC, createContext, useContext, useMemo, useState } from 'react'
|
||||
import useScopeValue from '../hooks/use-scope-value'
|
||||
import {
|
||||
FC,
|
||||
createContext,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { ProjectContextValue } from './types/project-context'
|
||||
import { ProjectUpdate, ProjectMetadata } from './types/project-metadata'
|
||||
import { ProjectSnapshot } from '@/infrastructure/project-snapshot'
|
||||
import { Tag } from '../../../../app/src/Features/Tags/types'
|
||||
|
||||
const ProjectContext = createContext<ProjectContextValue | undefined>(undefined)
|
||||
type ProjectContextValue = {
|
||||
projectId: ProjectMetadata['_id']
|
||||
project: ProjectMetadata | null
|
||||
joinProject: (project: ProjectMetadata) => void
|
||||
updateProject: (projectUpdate: ProjectUpdate) => void
|
||||
joinedOnce: boolean
|
||||
projectSnapshot: ProjectSnapshot
|
||||
tags: Tag[]
|
||||
features: ProjectMetadata['features']
|
||||
name: ProjectMetadata['name']
|
||||
}
|
||||
|
||||
export const ProjectContext = createContext<ProjectContextValue | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
export function useProjectContext() {
|
||||
const context = useContext(ProjectContext)
|
||||
@@ -18,35 +39,33 @@ export function useProjectContext() {
|
||||
return context
|
||||
}
|
||||
|
||||
// when the provider is created the project is still not added to the Angular
|
||||
// scope. A few props are populated to prevent errors in existing React
|
||||
// components
|
||||
const projectFallback = {
|
||||
_id: getMeta('ol-project_id'),
|
||||
name: '',
|
||||
features: {},
|
||||
}
|
||||
|
||||
export const ProjectProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
const [project] = useScopeValue('project')
|
||||
const joinedOnce = !!project
|
||||
const [joinedOnce, setJoinedOnce] = useState(false)
|
||||
const [project, setProject] = useState<ProjectMetadata | null>(null)
|
||||
|
||||
const {
|
||||
_id,
|
||||
compiler,
|
||||
imageName,
|
||||
name,
|
||||
rootDocId,
|
||||
members,
|
||||
invites,
|
||||
features,
|
||||
publicAccesLevel: publicAccessLevel,
|
||||
owner,
|
||||
trackChangesState,
|
||||
mainBibliographyDoc_id: mainBibliographyDocId,
|
||||
} = project || projectFallback
|
||||
// Expose some project properties with fallbacks for convenience
|
||||
const projectId = project ? project._id : getMeta('ol-project_id')
|
||||
const name = project ? project.name : ''
|
||||
const features = project ? project.features : {}
|
||||
|
||||
const [projectSnapshot] = useState(() => new ProjectSnapshot(_id))
|
||||
const joinProject = useCallback((projectData: ProjectMetadata) => {
|
||||
setProject(projectData)
|
||||
setJoinedOnce(true)
|
||||
}, [])
|
||||
|
||||
const updateProject = useCallback((projectUpdateData: ProjectUpdate) => {
|
||||
setProject(projectData => {
|
||||
// Only perform the update if `project` is already set, otherwise we could
|
||||
// end up with an incomplete project object
|
||||
if (!projectData) {
|
||||
throw new Error('Project not initialized. Use joinProject first.')
|
||||
}
|
||||
|
||||
return Object.assign({}, projectData, projectUpdateData)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const [projectSnapshot] = useState(() => new ProjectSnapshot(projectId))
|
||||
|
||||
const tags = useMemo(
|
||||
() =>
|
||||
@@ -56,41 +75,17 @@ export const ProjectProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
[]
|
||||
)
|
||||
|
||||
const value = useMemo(() => {
|
||||
return {
|
||||
_id,
|
||||
compiler,
|
||||
imageName,
|
||||
name,
|
||||
rootDocId,
|
||||
members,
|
||||
invites,
|
||||
features,
|
||||
publicAccessLevel,
|
||||
owner,
|
||||
tags,
|
||||
trackChangesState,
|
||||
mainBibliographyDocId,
|
||||
projectSnapshot,
|
||||
joinedOnce,
|
||||
}
|
||||
}, [
|
||||
_id,
|
||||
compiler,
|
||||
imageName,
|
||||
name,
|
||||
rootDocId,
|
||||
members,
|
||||
invites,
|
||||
features,
|
||||
publicAccessLevel,
|
||||
owner,
|
||||
tags,
|
||||
trackChangesState,
|
||||
mainBibliographyDocId,
|
||||
projectSnapshot,
|
||||
const value = {
|
||||
projectId,
|
||||
project,
|
||||
joinProject,
|
||||
updateProject,
|
||||
joinedOnce,
|
||||
])
|
||||
projectSnapshot,
|
||||
tags,
|
||||
features,
|
||||
name,
|
||||
}
|
||||
|
||||
return (
|
||||
<ProjectContext.Provider value={value}>{children}</ProjectContext.Provider>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { UserId } from '../../../../../types/user'
|
||||
import { PublicAccessLevel } from '../../../../../types/public-access-level'
|
||||
import { ProjectSnapshot } from '@/infrastructure/project-snapshot'
|
||||
import { Tag } from '../../../../../app/src/Features/Tags/types'
|
||||
import { ProjectSettings } from '@/features/editor-left-menu/utils/api'
|
||||
import { Folder } from '../../../../../types/folder'
|
||||
|
||||
export type ProjectContextMember = {
|
||||
export type ProjectMember = {
|
||||
_id: UserId
|
||||
privileges: 'readOnly' | 'readAndWrite' | 'review'
|
||||
email: string
|
||||
@@ -13,15 +13,11 @@ export type ProjectContextMember = {
|
||||
pendingReviewer?: boolean
|
||||
}
|
||||
|
||||
export type ProjectContextValue = {
|
||||
export interface ProjectMetadata extends ProjectSettings {
|
||||
_id: string
|
||||
name: string
|
||||
rootDocId?: string
|
||||
mainBibliographyDocId?: string
|
||||
compiler: string
|
||||
imageName: string
|
||||
members: ProjectContextMember[]
|
||||
invites: ProjectContextMember[]
|
||||
members: ProjectMember[]
|
||||
invites: ProjectMember[]
|
||||
features: {
|
||||
collaborators?: number
|
||||
compileGroup?: 'alpha' | 'standard' | 'priority'
|
||||
@@ -44,10 +40,8 @@ export type ProjectContextValue = {
|
||||
privileges: string
|
||||
signUpDate: string
|
||||
}
|
||||
tags: Tag[]
|
||||
rootFolder?: Folder[]
|
||||
trackChangesState: boolean | Record<UserId | '__guests__', boolean>
|
||||
projectSnapshot: ProjectSnapshot
|
||||
joinedOnce: boolean
|
||||
}
|
||||
|
||||
export type ProjectContextUpdateValue = Partial<ProjectContextValue>
|
||||
export type ProjectUpdate = Partial<ProjectMetadata>
|
||||
@@ -10,7 +10,7 @@ type UseStopOnFirstErrorProps = {
|
||||
export function useStopOnFirstError(opts: UseStopOnFirstErrorProps = {}) {
|
||||
const { eventSource } = opts
|
||||
const { stopOnFirstError, setStopOnFirstError } = useCompileContext()
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { projectId } = useProjectContext()
|
||||
|
||||
type Opts = {
|
||||
projectId: string
|
||||
|
||||
@@ -4,11 +4,16 @@ import {
|
||||
OverallThemeMeta,
|
||||
SpellCheckLanguage,
|
||||
} from '../../../../types/project-settings'
|
||||
import { EditorProviders } from '../../helpers/editor-providers'
|
||||
import {
|
||||
EditorProviders,
|
||||
makeProjectProvider,
|
||||
} from '../../helpers/editor-providers'
|
||||
import { mockScope } from './scope'
|
||||
import { Folder } from '../../../../types/folder'
|
||||
import { docsInFolder } from '@/features/file-tree/util/docs-in-folder'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { mockProject } from '../../features/source-editor/helpers/mock-project'
|
||||
import { UserId } from '../../../../types/user'
|
||||
|
||||
describe('<EditorLeftMenu />', function () {
|
||||
beforeEach(function () {
|
||||
@@ -57,9 +62,14 @@ describe('<EditorLeftMenu />', function () {
|
||||
|
||||
it('render full menu', function () {
|
||||
const scope = mockScope()
|
||||
const project = mockProject()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders scope={scope} layoutContext={{ leftMenuShown: true }}>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
layoutContext={{ leftMenuShown: true }}
|
||||
providers={{ ProjectProvider: makeProjectProvider(project) }}
|
||||
>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
)
|
||||
@@ -209,12 +219,6 @@ describe('<EditorLeftMenu />', function () {
|
||||
})
|
||||
|
||||
const scope = mockScope({
|
||||
project: {
|
||||
members: [],
|
||||
owner: {
|
||||
_id: '123',
|
||||
},
|
||||
},
|
||||
user: {
|
||||
features: {
|
||||
dropbox: false,
|
||||
@@ -226,6 +230,14 @@ describe('<EditorLeftMenu />', function () {
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
layoutContext={{ leftMenuShown: true }}
|
||||
projectOwner={{
|
||||
_id: '123' as UserId,
|
||||
email: 'owner@example.com',
|
||||
first_name: 'Test',
|
||||
last_name: 'Owner',
|
||||
privileges: 'owner',
|
||||
signUpDate: new Date('2025-07-07').toISOString(),
|
||||
}}
|
||||
>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
@@ -236,21 +248,25 @@ describe('<EditorLeftMenu />', function () {
|
||||
})
|
||||
|
||||
it('shows git modal correctly', function () {
|
||||
const scope = mockScope({
|
||||
project: {
|
||||
owner: {
|
||||
_id: '123',
|
||||
},
|
||||
features: {
|
||||
gitBridge: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
layoutContext={{ leftMenuShown: true }}
|
||||
projectOwner={{
|
||||
_id: '123' as UserId,
|
||||
email: 'owner@example.com',
|
||||
first_name: 'Test',
|
||||
last_name: 'Owner',
|
||||
privileges: 'owner',
|
||||
signUpDate: new Date('2025-07-07').toISOString(),
|
||||
}}
|
||||
projectFeatures={
|
||||
{
|
||||
gitBridge: true,
|
||||
} as any
|
||||
}
|
||||
>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
@@ -262,21 +278,25 @@ describe('<EditorLeftMenu />', function () {
|
||||
})
|
||||
|
||||
it('shows git modal paywall correctly', function () {
|
||||
const scope = mockScope({
|
||||
project: {
|
||||
owner: {
|
||||
_id: '123',
|
||||
},
|
||||
features: {
|
||||
gitBridge: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
const scope = mockScope()
|
||||
|
||||
cy.mount(
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
layoutContext={{ leftMenuShown: true }}
|
||||
projectOwner={{
|
||||
_id: '123' as UserId,
|
||||
email: 'owner@example.com',
|
||||
first_name: 'Test',
|
||||
last_name: 'Owner',
|
||||
privileges: 'owner',
|
||||
signUpDate: new Date('2025-07-07').toISOString(),
|
||||
}}
|
||||
projectFeatures={
|
||||
{
|
||||
gitBridge: false,
|
||||
} as any
|
||||
}
|
||||
>
|
||||
<EditorLeftMenu />
|
||||
</EditorProviders>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '../../../../frontend/js/shared/context/layout-context'
|
||||
import { FC, PropsWithChildren, useEffect } from 'react'
|
||||
import { useLocalCompileContext } from '@/shared/context/local-compile-context'
|
||||
import { ProjectCompiler } from '../../../../types/project-settings'
|
||||
|
||||
const storeAndFireEvent = (win: typeof window, key: string, value: unknown) => {
|
||||
localStorage.setItem(key, value)
|
||||
@@ -215,7 +216,7 @@ describe('<PdfPreview/>', function () {
|
||||
cached: false,
|
||||
setup: () => {},
|
||||
props: {
|
||||
compiler: 'lualatex',
|
||||
compiler: 'lualatex' as ProjectCompiler,
|
||||
},
|
||||
},
|
||||
'ignores the compile from cache when draft mode changed': {
|
||||
|
||||
@@ -15,9 +15,9 @@ describe('<EditorCloneProjectModalWrapper />', function () {
|
||||
fetchMock.removeRoutes().clearHistory()
|
||||
})
|
||||
|
||||
const project = {
|
||||
_id: 'project-1',
|
||||
name: 'Test Project',
|
||||
const contextProps = {
|
||||
projectId: 'project-1',
|
||||
projectName: 'Test Project',
|
||||
}
|
||||
|
||||
it('renders the translated modal title', async function () {
|
||||
@@ -30,7 +30,7 @@ describe('<EditorCloneProjectModalWrapper />', function () {
|
||||
openProject={openProject}
|
||||
show
|
||||
/>,
|
||||
{ scope: { project } }
|
||||
contextProps
|
||||
)
|
||||
|
||||
await screen.findByText('Copy project')
|
||||
@@ -55,7 +55,7 @@ describe('<EditorCloneProjectModalWrapper />', function () {
|
||||
openProject={openProject}
|
||||
show
|
||||
/>,
|
||||
{ scope: { project } }
|
||||
contextProps
|
||||
)
|
||||
|
||||
const cancelButton = await screen.findByRole('button', { name: 'Cancel' })
|
||||
@@ -123,7 +123,7 @@ describe('<EditorCloneProjectModalWrapper />', function () {
|
||||
openProject={openProject}
|
||||
show
|
||||
/>,
|
||||
{ scope: { project } }
|
||||
contextProps
|
||||
)
|
||||
|
||||
const button = await screen.findByRole('button', { name: 'Copy' })
|
||||
@@ -160,7 +160,7 @@ describe('<EditorCloneProjectModalWrapper />', function () {
|
||||
openProject={openProject}
|
||||
show
|
||||
/>,
|
||||
{ scope: { project } }
|
||||
contextProps
|
||||
)
|
||||
|
||||
const button = await screen.findByRole('button', { name: 'Copy' })
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import CodeMirrorEditor from '../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import {
|
||||
EditorProviders,
|
||||
makeProjectProvider,
|
||||
USER_EMAIL,
|
||||
USER_ID,
|
||||
} from '../../helpers/editor-providers'
|
||||
import { mockScope } from '../source-editor/helpers/mock-scope'
|
||||
import { TestContainer } from '../source-editor/helpers/test-container'
|
||||
import { docId } from '../source-editor/helpers/mock-doc'
|
||||
import { mockProject } from '../source-editor/helpers/mock-project'
|
||||
|
||||
const userData = {
|
||||
avatar_text: 'User',
|
||||
@@ -181,14 +183,22 @@ describe('<ReviewPanel />', function () {
|
||||
removeChangeIds,
|
||||
},
|
||||
},
|
||||
projectFeatures: { trackChangesVisible: true },
|
||||
})
|
||||
const project = mockProject({
|
||||
projectOwner: {
|
||||
_id: USER_ID,
|
||||
},
|
||||
projectFeatures: { trackChanges: false, trackChangesVisible: true },
|
||||
})
|
||||
|
||||
cy.wrap(scope).as('scope')
|
||||
|
||||
cy.mount(
|
||||
<TestContainer className="rp-size-expanded">
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={{ ProjectProvider: makeProjectProvider(project) }}
|
||||
>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
@@ -653,6 +663,10 @@ describe('<ReviewPanel /> in mini mode', function () {
|
||||
projectFeatures: { trackChangesVisible: true },
|
||||
})
|
||||
|
||||
const project = mockProject({
|
||||
projectFeatures: { trackChangesVisible: true },
|
||||
})
|
||||
|
||||
cy.intercept('GET', '/project/*/ranges', [
|
||||
{
|
||||
id: docId,
|
||||
@@ -672,7 +686,10 @@ describe('<ReviewPanel /> in mini mode', function () {
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={{ ProjectProvider: makeProjectProvider(project) }}
|
||||
>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
@@ -717,7 +734,7 @@ describe('<ReviewPanel /> in mini mode', function () {
|
||||
cy.get('.review-panel-mini').should('not.exist')
|
||||
})
|
||||
|
||||
it("doesn't render mini when a resolved comments is present in document", function () {
|
||||
it("doesn't render mini when a resolved comment is present in document", function () {
|
||||
render({
|
||||
comments: [
|
||||
{
|
||||
@@ -747,7 +764,7 @@ describe('<ReviewPanel /> in mini mode', function () {
|
||||
cy.get('.review-panel-mini').should('not.exist')
|
||||
})
|
||||
|
||||
it('renders mini when a unresolved comments is present in document', function () {
|
||||
it('renders mini when an unresolved comment is present in document', function () {
|
||||
render({
|
||||
comments: [
|
||||
{
|
||||
@@ -796,6 +813,8 @@ describe('<ReviewPanel /> for free users', function () {
|
||||
function mountEditor(ownerId = USER_ID) {
|
||||
const scope = mockScope(undefined, {
|
||||
permissions: { write: true, trackedWrite: false, comment: true },
|
||||
})
|
||||
const project = mockProject({
|
||||
projectFeatures: { trackChanges: false, trackChangesVisible: true },
|
||||
projectOwner: {
|
||||
_id: ownerId,
|
||||
@@ -806,7 +825,10 @@ describe('<ReviewPanel /> for free users', function () {
|
||||
|
||||
cy.mount(
|
||||
<TestContainer className="rp-size-expanded">
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={{ ProjectProvider: makeProjectProvider(project) }}
|
||||
>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import { screen, fireEvent, render, waitFor } from '@testing-library/react'
|
||||
import { screen, fireEvent, waitFor } from '@testing-library/react'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import ShareProjectModal from '../../../../../frontend/js/features/share-project-modal/components/share-project-modal'
|
||||
import { renderWithEditorContext } from '../../../helpers/render-with-context'
|
||||
import {
|
||||
EditorProviders,
|
||||
makeProjectProvider,
|
||||
projectDefaults,
|
||||
USER_EMAIL,
|
||||
USER_ID,
|
||||
} from '../../../helpers/editor-providers'
|
||||
import { location } from '@/shared/components/location'
|
||||
import useScopeValue from '@/shared/hooks/use-scope-value'
|
||||
import { useProjectContext } from '@/shared/context/project-context'
|
||||
|
||||
async function changePrivilegeLevel(screen, { current, next }) {
|
||||
const select = screen.getByDisplayValue(current)
|
||||
@@ -23,22 +24,25 @@ async function changePrivilegeLevel(screen, { current, next }) {
|
||||
fireEvent.click(option)
|
||||
}
|
||||
|
||||
describe('<ShareProjectModal/>', function () {
|
||||
const project = {
|
||||
_id: 'test-project',
|
||||
name: 'Test Project',
|
||||
features: {
|
||||
collaborators: 10,
|
||||
compileGroup: 'standard',
|
||||
},
|
||||
owner: {
|
||||
_id: USER_ID,
|
||||
email: USER_EMAIL,
|
||||
},
|
||||
members: [],
|
||||
invites: [],
|
||||
}
|
||||
const shareModalProjectDefaults = Object.assign({}, projectDefaults, {
|
||||
_id: 'test-project',
|
||||
name: 'Test Project',
|
||||
features: {
|
||||
collaborators: 10,
|
||||
compileGroup: 'standard',
|
||||
},
|
||||
owner: {
|
||||
_id: USER_ID,
|
||||
email: USER_EMAIL,
|
||||
},
|
||||
})
|
||||
|
||||
function createContextProps(projectOverrides) {
|
||||
const project = Object.assign({}, shareModalProjectDefaults, projectOverrides)
|
||||
return { providers: { ProjectProvider: makeProjectProvider(project) } }
|
||||
}
|
||||
|
||||
describe('<ShareProjectModal/>', function () {
|
||||
const contacts = [
|
||||
// user with edited name
|
||||
{
|
||||
@@ -101,9 +105,10 @@ describe('<ShareProjectModal/>', function () {
|
||||
})
|
||||
|
||||
it('renders the modal', async function () {
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: { project },
|
||||
})
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal {...modalProps} />,
|
||||
createContextProps()
|
||||
)
|
||||
|
||||
await screen.findByText('Share Project')
|
||||
})
|
||||
@@ -113,7 +118,7 @@ describe('<ShareProjectModal/>', function () {
|
||||
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal {...modalProps} handleHide={handleHide} />,
|
||||
{ scope: { project } }
|
||||
createContextProps()
|
||||
)
|
||||
|
||||
const [headerCloseButton, footerCloseButton] = await screen.findAllByRole(
|
||||
@@ -128,9 +133,10 @@ describe('<ShareProjectModal/>', function () {
|
||||
})
|
||||
|
||||
it('handles access level "private"', async function () {
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: { project: { ...project, publicAccesLevel: 'private' } },
|
||||
})
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal {...modalProps} />,
|
||||
createContextProps({ publicAccessLevel: 'private' })
|
||||
)
|
||||
|
||||
await screen.findByText('Link sharing is off')
|
||||
await screen.findByRole('button', { name: 'Turn on link sharing' })
|
||||
@@ -149,10 +155,11 @@ describe('<ShareProjectModal/>', function () {
|
||||
readAndWriteHashPrefix: 'taEVki',
|
||||
readOnlyHashPrefix: 'j2xYbL',
|
||||
}
|
||||
fetchMock.get(`/project/${project._id}/tokens`, tokens)
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: { project: { ...project, publicAccesLevel: 'tokenBased' } },
|
||||
})
|
||||
fetchMock.get(`/project/${shareModalProjectDefaults._id}/tokens`, tokens)
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal {...modalProps} />,
|
||||
createContextProps({ publicAccessLevel: 'tokenBased' })
|
||||
)
|
||||
|
||||
await screen.findByText('Link sharing is on')
|
||||
await screen.findByRole('button', { name: 'Turn off link sharing' })
|
||||
@@ -171,9 +178,10 @@ describe('<ShareProjectModal/>', function () {
|
||||
})
|
||||
|
||||
it('handles legacy access level "readAndWrite"', async function () {
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: { project: { ...project, publicAccesLevel: 'readAndWrite' } },
|
||||
})
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal {...modalProps} />,
|
||||
createContextProps({ publicAccessLevel: 'readAndWrite' })
|
||||
)
|
||||
|
||||
await screen.findByText(
|
||||
'This project is public and can be edited by anyone with the URL.'
|
||||
@@ -182,9 +190,10 @@ describe('<ShareProjectModal/>', function () {
|
||||
})
|
||||
|
||||
it('handles legacy access level "readOnly"', async function () {
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: { project: { ...project, publicAccesLevel: 'readOnly' } },
|
||||
})
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal {...modalProps} />,
|
||||
createContextProps({ publicAccessLevel: 'readOnly' })
|
||||
)
|
||||
|
||||
await screen.findByText(
|
||||
'This project is public and can be viewed but not edited by anyone with the URL'
|
||||
@@ -193,7 +202,7 @@ describe('<ShareProjectModal/>', function () {
|
||||
})
|
||||
|
||||
it('displays actions for project-owners', async function () {
|
||||
fetchMock.get(`/project/${project._id}/tokens`, {})
|
||||
fetchMock.get(`/project/${shareModalProjectDefaults._id}/tokens`, {})
|
||||
|
||||
const invites = [
|
||||
{
|
||||
@@ -204,18 +213,9 @@ describe('<ShareProjectModal/>', function () {
|
||||
]
|
||||
|
||||
// render as project owner: actions should be present
|
||||
render(
|
||||
<EditorProviders
|
||||
scope={{
|
||||
project: {
|
||||
...project,
|
||||
invites,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ShareProjectModal {...modalProps} />
|
||||
</EditorProviders>
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal {...modalProps} />,
|
||||
createContextProps({ publicAccessLevel: 'tokenBased', invites })
|
||||
)
|
||||
|
||||
await screen.findByRole('button', { name: 'Turn off link sharing' })
|
||||
@@ -231,23 +231,13 @@ describe('<ShareProjectModal/>', function () {
|
||||
},
|
||||
]
|
||||
|
||||
render(
|
||||
<EditorProviders
|
||||
scope={{
|
||||
project: {
|
||||
...project,
|
||||
invites,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
},
|
||||
}}
|
||||
user={{
|
||||
id: 'non-project-owner',
|
||||
email: 'non-project-owner@example.com',
|
||||
}}
|
||||
>
|
||||
<ShareProjectModal {...modalProps} />
|
||||
</EditorProviders>
|
||||
)
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
...createContextProps({ publicAccessLevel: 'tokenBased', invites }),
|
||||
user: {
|
||||
id: 'non-project-owner',
|
||||
email: 'non-project-owner@example.com',
|
||||
},
|
||||
})
|
||||
|
||||
await screen.findByText(
|
||||
'To change access permissions, please ask the project owner'
|
||||
@@ -269,23 +259,13 @@ describe('<ShareProjectModal/>', function () {
|
||||
},
|
||||
]
|
||||
|
||||
render(
|
||||
<EditorProviders
|
||||
scope={{
|
||||
project: {
|
||||
...project,
|
||||
invites,
|
||||
publicAccesLevel: 'private',
|
||||
},
|
||||
}}
|
||||
user={{
|
||||
id: 'non-project-owner',
|
||||
email: 'non-project-owner@example.com',
|
||||
}}
|
||||
>
|
||||
<ShareProjectModal {...modalProps} />
|
||||
</EditorProviders>
|
||||
)
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
...createContextProps({ publicAccessLevel: 'private', invites }),
|
||||
user: {
|
||||
id: 'non-project-owner',
|
||||
email: 'non-project-owner@example.com',
|
||||
},
|
||||
})
|
||||
|
||||
await screen.findByText(
|
||||
'To add more collaborators or turn on link sharing, please ask the project owner'
|
||||
@@ -300,11 +280,11 @@ describe('<ShareProjectModal/>', function () {
|
||||
|
||||
it('only shows read-only token link to restricted token members', async function () {
|
||||
window.metaAttributesCache.set('ol-isRestrictedTokenMember', true)
|
||||
fetchMock.get(`/project/${project._id}/tokens`, {})
|
||||
fetchMock.get(`/project/${shareModalProjectDefaults._id}/tokens`, {})
|
||||
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
...createContextProps({ publicAccessLevel: 'private' }),
|
||||
isRestrictedTokenMember: true,
|
||||
scope: { project: { ...project, publicAccesLevel: 'tokenBased' } },
|
||||
})
|
||||
|
||||
// no buttons
|
||||
@@ -346,18 +326,12 @@ describe('<ShareProjectModal/>', function () {
|
||||
},
|
||||
]
|
||||
|
||||
fetchMock.get(`/project/${project._id}/tokens`, {})
|
||||
fetchMock.get(`/project/${shareModalProjectDefaults._id}/tokens`, {})
|
||||
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: {
|
||||
project: {
|
||||
...project,
|
||||
members,
|
||||
invites,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
},
|
||||
},
|
||||
})
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal {...modalProps} />,
|
||||
createContextProps({ publicAccessLevel: 'tokenBased', members, invites })
|
||||
)
|
||||
|
||||
const projectOwnerEmail = USER_EMAIL
|
||||
|
||||
@@ -381,7 +355,7 @@ describe('<ShareProjectModal/>', function () {
|
||||
})
|
||||
|
||||
it('resends an invite', async function () {
|
||||
fetchMock.get(`/project/${project._id}/tokens`, {})
|
||||
fetchMock.get(`/project/${shareModalProjectDefaults._id}/tokens`, {})
|
||||
fetchMock.postOnce(
|
||||
'express:/project/:projectId/invite/:inviteId/resend',
|
||||
204
|
||||
@@ -395,15 +369,10 @@ describe('<ShareProjectModal/>', function () {
|
||||
},
|
||||
]
|
||||
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: {
|
||||
project: {
|
||||
...project,
|
||||
invites,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
},
|
||||
},
|
||||
})
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal {...modalProps} />,
|
||||
createContextProps({ publicAccessLevel: 'tokenBased', invites })
|
||||
)
|
||||
|
||||
const [, closeButton] = screen.getAllByRole('button', {
|
||||
name: 'Close',
|
||||
@@ -419,7 +388,7 @@ describe('<ShareProjectModal/>', function () {
|
||||
})
|
||||
|
||||
it('revokes an invite', async function () {
|
||||
fetchMock.get(`/project/${project._id}/tokens`, {})
|
||||
fetchMock.get(`/project/${shareModalProjectDefaults._id}/tokens`, {})
|
||||
fetchMock.deleteOnce('express:/project/:projectId/invite/:inviteId', 204)
|
||||
|
||||
const invites = [
|
||||
@@ -430,15 +399,10 @@ describe('<ShareProjectModal/>', function () {
|
||||
},
|
||||
]
|
||||
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: {
|
||||
project: {
|
||||
...project,
|
||||
invites,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
},
|
||||
},
|
||||
})
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal {...modalProps} />,
|
||||
createContextProps({ publicAccessLevel: 'tokenBased', invites })
|
||||
)
|
||||
|
||||
const [, closeButton] = screen.getAllByRole('button', {
|
||||
name: 'Close',
|
||||
@@ -453,7 +417,7 @@ describe('<ShareProjectModal/>', function () {
|
||||
})
|
||||
|
||||
it('changes member privileges to read + write', async function () {
|
||||
fetchMock.get(`/project/${project._id}/tokens`, {})
|
||||
fetchMock.get(`/project/${shareModalProjectDefaults._id}/tokens`, {})
|
||||
fetchMock.putOnce('express:/project/:projectId/users/:userId', 204)
|
||||
|
||||
const members = [
|
||||
@@ -464,17 +428,12 @@ describe('<ShareProjectModal/>', function () {
|
||||
},
|
||||
]
|
||||
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: {
|
||||
project: {
|
||||
...project,
|
||||
members,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
},
|
||||
},
|
||||
})
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal {...modalProps} />,
|
||||
createContextProps({ publicAccessLevel: 'tokenBased', members })
|
||||
)
|
||||
|
||||
const [, closeButton] = await screen.getAllByRole('button', {
|
||||
const [, closeButton] = screen.getAllByRole('button', {
|
||||
name: 'Close',
|
||||
})
|
||||
|
||||
@@ -494,7 +453,7 @@ describe('<ShareProjectModal/>', function () {
|
||||
})
|
||||
|
||||
it('removes a member from the project', async function () {
|
||||
fetchMock.get(`/project/${project._id}/tokens`, {})
|
||||
fetchMock.get(`/project/${shareModalProjectDefaults._id}/tokens`, {})
|
||||
fetchMock.deleteOnce('express:/project/:projectId/users/:userId', 204)
|
||||
|
||||
const members = [
|
||||
@@ -505,15 +464,10 @@ describe('<ShareProjectModal/>', function () {
|
||||
},
|
||||
]
|
||||
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: {
|
||||
project: {
|
||||
...project,
|
||||
members,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
},
|
||||
},
|
||||
})
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal {...modalProps} />,
|
||||
createContextProps({ publicAccessLevel: 'tokenBased', members })
|
||||
)
|
||||
|
||||
expect(
|
||||
await screen.findAllByText('member-viewer@example.com')
|
||||
@@ -537,7 +491,7 @@ describe('<ShareProjectModal/>', function () {
|
||||
})
|
||||
|
||||
it('changes member privileges to owner with confirmation', async function () {
|
||||
fetchMock.get(`/project/${project._id}/tokens`, {})
|
||||
fetchMock.get(`/project/${shareModalProjectDefaults._id}/tokens`, {})
|
||||
fetchMock.postOnce('express:/project/:projectId/transfer-ownership', 204)
|
||||
|
||||
const members = [
|
||||
@@ -548,15 +502,10 @@ describe('<ShareProjectModal/>', function () {
|
||||
},
|
||||
]
|
||||
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: {
|
||||
project: {
|
||||
...project,
|
||||
members,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
},
|
||||
},
|
||||
})
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal {...modalProps} />,
|
||||
createContextProps({ publicAccessLevel: 'tokenBased', members })
|
||||
)
|
||||
|
||||
await screen.findByText('member-viewer@example.com')
|
||||
expect(screen.queryAllByText('member-viewer@example.com')).to.have.length(1)
|
||||
@@ -586,16 +535,12 @@ describe('<ShareProjectModal/>', function () {
|
||||
})
|
||||
|
||||
it('sends invites to input email addresses', async function () {
|
||||
fetchMock.get(`/project/${project._id}/tokens`, {})
|
||||
fetchMock.get(`/project/${shareModalProjectDefaults._id}/tokens`, {})
|
||||
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: {
|
||||
project: {
|
||||
...project,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
},
|
||||
},
|
||||
})
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal {...modalProps} />,
|
||||
createContextProps({ publicAccessLevel: 'tokenBased' })
|
||||
)
|
||||
|
||||
const [inputElement] = await screen.findAllByLabelText('Add people')
|
||||
|
||||
@@ -680,26 +625,24 @@ describe('<ShareProjectModal/>', function () {
|
||||
})
|
||||
|
||||
it('displays a message when the collaborator limit is reached', async function () {
|
||||
fetchMock.get(`/project/${project._id}/tokens`, {})
|
||||
fetchMock.get(`/project/${shareModalProjectDefaults._id}/tokens`, {})
|
||||
fetchMock.post(
|
||||
'/event/paywall-prompt',
|
||||
{},
|
||||
{ body: { 'paywall-type': 'project-sharing' } }
|
||||
)
|
||||
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: {
|
||||
project: {
|
||||
...project,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
features: {
|
||||
collaborators: 0,
|
||||
compileGroup: 'standard',
|
||||
trackChangesVisible: true,
|
||||
},
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal {...modalProps} />,
|
||||
createContextProps({
|
||||
publicAccessLevel: 'tokenBased',
|
||||
features: {
|
||||
collaborators: 0,
|
||||
compileGroup: 'standard',
|
||||
trackChangesVisible: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
await screen.findByText('Add more collaborators')
|
||||
|
||||
@@ -719,24 +662,22 @@ describe('<ShareProjectModal/>', function () {
|
||||
})
|
||||
|
||||
it('counts reviewers towards the collaborator limit', async function () {
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: {
|
||||
project: {
|
||||
...project,
|
||||
features: {
|
||||
collaborators: 1,
|
||||
trackChangesVisible: true,
|
||||
},
|
||||
members: [
|
||||
{
|
||||
_id: 'reviewer-id',
|
||||
email: 'reviewer@example.com',
|
||||
privileges: 'review',
|
||||
},
|
||||
],
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal {...modalProps} />,
|
||||
createContextProps({
|
||||
features: {
|
||||
collaborators: 1,
|
||||
trackChangesVisible: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
members: [
|
||||
{
|
||||
_id: 'reviewer-id',
|
||||
email: 'reviewer@example.com',
|
||||
privileges: 'review',
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
await screen.findByText('Add more collaborators')
|
||||
|
||||
@@ -757,16 +698,14 @@ describe('<ShareProjectModal/>', function () {
|
||||
})
|
||||
|
||||
it('handles server error responses', async function () {
|
||||
fetchMock.get(`/project/${project._id}/tokens`, {})
|
||||
fetchMock.get(`/project/${shareModalProjectDefaults._id}/tokens`, {})
|
||||
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: {
|
||||
project: {
|
||||
...project,
|
||||
publicAccesLevel: 'tokenBased',
|
||||
},
|
||||
},
|
||||
})
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal {...modalProps} />,
|
||||
createContextProps({
|
||||
publicAccessLevel: 'tokenBased',
|
||||
})
|
||||
)
|
||||
|
||||
// loading contacts
|
||||
await waitFor(() => {
|
||||
@@ -818,21 +757,25 @@ describe('<ShareProjectModal/>', function () {
|
||||
})
|
||||
|
||||
it('handles switching between access levels', async function () {
|
||||
fetchMock.get(`/project/${project._id}/tokens`, {})
|
||||
fetchMock.get(`/project/${shareModalProjectDefaults._id}/tokens`, {})
|
||||
fetchMock.post('express:/project/:projectId/settings/admin', 204)
|
||||
|
||||
let setPublicAccessLevel = function () {}
|
||||
|
||||
function WrappedModal() {
|
||||
setPublicAccessLevel = useScopeValue('project.publicAccesLevel')[1]
|
||||
const { updateProject } = useProjectContext()
|
||||
setPublicAccessLevel = publicAccessLevel => {
|
||||
updateProject({ publicAccessLevel })
|
||||
}
|
||||
return <ShareProjectModal {...modalProps} />
|
||||
}
|
||||
|
||||
renderWithEditorContext(<WrappedModal />, {
|
||||
scope: {
|
||||
project: { ...project, publicAccesLevel: 'private' },
|
||||
},
|
||||
})
|
||||
renderWithEditorContext(
|
||||
<WrappedModal />,
|
||||
createContextProps({
|
||||
publicAccessLevel: 'private',
|
||||
})
|
||||
)
|
||||
|
||||
await screen.findByText('Link sharing is off')
|
||||
|
||||
@@ -848,8 +791,8 @@ describe('<ShareProjectModal/>', function () {
|
||||
})
|
||||
|
||||
// NOTE: the project data is usually updated via the websocket connection
|
||||
// but we can't do that so we're doing it via the scope value store (this
|
||||
// will be via the project context when this value has been migrated)
|
||||
// but we can't do that so we're doing it via the project context, which is
|
||||
// exposed in a hacky way here
|
||||
setPublicAccessLevel('tokenBased')
|
||||
|
||||
await screen.findByText('Link sharing is on')
|
||||
@@ -870,9 +813,10 @@ describe('<ShareProjectModal/>', function () {
|
||||
})
|
||||
|
||||
it('avoids selecting unmatched contact', async function () {
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: { project },
|
||||
})
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal {...modalProps} />,
|
||||
createContextProps()
|
||||
)
|
||||
|
||||
const [inputElement] = await screen.findAllByLabelText('Add people')
|
||||
|
||||
@@ -924,9 +868,10 @@ describe('<ShareProjectModal/>', function () {
|
||||
})
|
||||
|
||||
it('selects contact by typing the entire email and blurring the input', async function () {
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: { project },
|
||||
})
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal {...modalProps} />,
|
||||
createContextProps()
|
||||
)
|
||||
|
||||
const [inputElement] = await screen.findAllByLabelText('Add people')
|
||||
|
||||
@@ -959,9 +904,10 @@ describe('<ShareProjectModal/>', function () {
|
||||
})
|
||||
|
||||
it('selects contact by typing a partial email and selecting the suggestion', async function () {
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: { project },
|
||||
})
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal {...modalProps} />,
|
||||
createContextProps()
|
||||
)
|
||||
|
||||
const [inputElement] = await screen.findAllByLabelText('Add people')
|
||||
|
||||
@@ -993,9 +939,10 @@ describe('<ShareProjectModal/>', function () {
|
||||
})
|
||||
|
||||
it('allows an email address to be selected, removed, then re-added', async function () {
|
||||
renderWithEditorContext(<ShareProjectModal {...modalProps} />, {
|
||||
scope: { project },
|
||||
})
|
||||
renderWithEditorContext(
|
||||
<ShareProjectModal {...modalProps} />,
|
||||
createContextProps()
|
||||
)
|
||||
|
||||
const [inputElement] = await screen.findAllByLabelText('Add people')
|
||||
|
||||
|
||||
@@ -64,7 +64,6 @@ describe('autocomplete', { scrollBehavior: false }, function () {
|
||||
]
|
||||
|
||||
const scope = mockScope()
|
||||
scope.project.rootFolder = rootFolder
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
@@ -446,7 +445,6 @@ describe('autocomplete', { scrollBehavior: false }, function () {
|
||||
]
|
||||
|
||||
const scope = mockScope()
|
||||
scope.project.rootFolder = rootFolder
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
@@ -910,7 +908,6 @@ describe('autocomplete', { scrollBehavior: false }, function () {
|
||||
]
|
||||
|
||||
const scope = mockScope()
|
||||
scope.project.rootFolder = rootFolder
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
|
||||
@@ -2,12 +2,15 @@ import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/
|
||||
import {
|
||||
EditorProviders,
|
||||
makeEditorPropertiesProvider,
|
||||
makeProjectProvider,
|
||||
USER_ID,
|
||||
} from '../../../helpers/editor-providers'
|
||||
import { mockScope, rootFolderId } from '../helpers/mock-scope'
|
||||
import { FC } from 'react'
|
||||
import { FileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { mockProject } from '../helpers/mock-project'
|
||||
|
||||
const clickToolbarButton = (text: string) => {
|
||||
cy.findByLabelText(text).click()
|
||||
@@ -44,6 +47,11 @@ describe('<FigureModal />', function () {
|
||||
function mount() {
|
||||
const content = ''
|
||||
const scope = mockScope(content)
|
||||
const project = mockProject({
|
||||
projectOwner: {
|
||||
_id: USER_ID,
|
||||
},
|
||||
})
|
||||
|
||||
const FileTreePathProvider: FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
@@ -69,6 +77,7 @@ describe('<FigureModal />', function () {
|
||||
scope={scope}
|
||||
providers={{
|
||||
FileTreePathProvider,
|
||||
ProjectProvider: makeProjectProvider(project),
|
||||
EditorPropertiesProvider: makeEditorPropertiesProvider({
|
||||
showVisual: true,
|
||||
showSymbolPalette: false,
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import { EditorProviders } from '../../../helpers/editor-providers'
|
||||
import {
|
||||
EditorProviders,
|
||||
makeProjectProvider,
|
||||
} from '../../../helpers/editor-providers'
|
||||
import CodeMirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
import forEach from 'mocha-each'
|
||||
import PackageVersions from '../../../../../app/src/infrastructure/PackageVersions'
|
||||
import { mockProject } from '../helpers/mock-project'
|
||||
|
||||
const languages = [
|
||||
{ code: 'af', dic: 'af_ZA', name: 'Afrikaans' },
|
||||
@@ -125,11 +129,14 @@ forEach(Object.keys(suggestions)).describe(
|
||||
cy.interceptEvents()
|
||||
|
||||
const scope = mockScope(content)
|
||||
scope.project.spellCheckLanguage = spellCheckLanguage
|
||||
const project = mockProject({ spellCheckLanguage })
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders scope={scope}>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={{ ProjectProvider: makeProjectProvider(project) }}
|
||||
>
|
||||
<CodeMirrorEditor />
|
||||
</EditorProviders>
|
||||
</TestContainer>
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import {
|
||||
EditorProviders,
|
||||
makeEditorPropertiesProvider,
|
||||
makeProjectProvider,
|
||||
} from '../../../helpers/editor-providers'
|
||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
import { mockProject } from '../helpers/mock-project'
|
||||
|
||||
const mountEditor = (content: string) => {
|
||||
const scope = mockScope(content)
|
||||
|
||||
const project = mockProject()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={{
|
||||
ProjectProvider: makeProjectProvider(project),
|
||||
EditorPropertiesProvider: makeEditorPropertiesProvider({
|
||||
showVisual: true,
|
||||
showSymbolPalette: false,
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import {
|
||||
EditorProviders,
|
||||
makeEditorPropertiesProvider,
|
||||
makeProjectProvider,
|
||||
} from '../../../helpers/editor-providers'
|
||||
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
|
||||
import { mockScope } from '../helpers/mock-scope'
|
||||
import { TestContainer } from '../helpers/test-container'
|
||||
import { isMac } from '@/shared/utils/os'
|
||||
import { mockProject } from '../helpers/mock-project'
|
||||
|
||||
const mountEditor = (content: string) => {
|
||||
const scope = mockScope(content)
|
||||
const project = mockProject()
|
||||
|
||||
cy.mount(
|
||||
<TestContainer>
|
||||
<EditorProviders
|
||||
scope={scope}
|
||||
providers={{
|
||||
ProjectProvider: makeProjectProvider(project),
|
||||
EditorPropertiesProvider: makeEditorPropertiesProvider({
|
||||
showVisual: true,
|
||||
showSymbolPalette: false,
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { docId } from './mock-doc'
|
||||
import { Folder } from '../../../../../types/folder'
|
||||
import { UserId } from '../../../../../types/user'
|
||||
import { ProjectCompiler } from '../../../../../types/project-settings'
|
||||
|
||||
export const rootFolderId = '012345678901234567890123'
|
||||
export const figuresFolderId = '123456789012345678901234'
|
||||
export const figureId = '234567890123456789012345'
|
||||
export const mockProject = ({
|
||||
projectFeatures = {},
|
||||
projectOwner = undefined,
|
||||
spellCheckLanguage = 'en',
|
||||
rootFolder = null,
|
||||
}: any = {}) => {
|
||||
return {
|
||||
_id: 'test-project',
|
||||
name: 'Test Project',
|
||||
spellCheckLanguage,
|
||||
rootDocId: '_root_doc_id',
|
||||
rootFolder:
|
||||
rootFolder ||
|
||||
([
|
||||
{
|
||||
_id: rootFolderId,
|
||||
name: 'rootFolder',
|
||||
docs: [
|
||||
{
|
||||
_id: docId,
|
||||
name: 'test.tex',
|
||||
},
|
||||
],
|
||||
folders: [
|
||||
{
|
||||
_id: figuresFolderId,
|
||||
name: 'figures',
|
||||
docs: [
|
||||
{
|
||||
_id: 'fake-nested-doc-id',
|
||||
name: 'foo.tex',
|
||||
},
|
||||
],
|
||||
folders: [],
|
||||
fileRefs: [
|
||||
{
|
||||
_id: figureId,
|
||||
name: 'frog.jpg',
|
||||
hash: '42',
|
||||
},
|
||||
{
|
||||
_id: 'fake-figure-id',
|
||||
name: 'unicorn.png',
|
||||
hash: '43',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
fileRefs: [],
|
||||
},
|
||||
] as Folder[]),
|
||||
features: {
|
||||
trackChanges: true,
|
||||
...projectFeatures,
|
||||
},
|
||||
compiler: 'pdflatex' as ProjectCompiler,
|
||||
imageName: 'texlive-full:2024.1',
|
||||
trackChangesState: false,
|
||||
invites: [],
|
||||
members: [],
|
||||
owner: projectOwner || {
|
||||
_id: '124abd' as UserId,
|
||||
email: 'owner@example.com',
|
||||
first_name: 'Test',
|
||||
last_name: 'Owner',
|
||||
privileges: 'owner',
|
||||
signUpDate: new Date('2025-07-07').toISOString(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,10 @@
|
||||
import { docId, mockDoc } from './mock-doc'
|
||||
import { sleep } from '../../../helpers/sleep'
|
||||
import { Folder } from '../../../../../types/folder'
|
||||
|
||||
export const rootFolderId = '012345678901234567890123'
|
||||
export const figuresFolderId = '123456789012345678901234'
|
||||
export const figureId = '234567890123456789012345'
|
||||
export const mockScope = (
|
||||
content?: string,
|
||||
{
|
||||
docOptions = {},
|
||||
projectFeatures = {},
|
||||
permissions = {},
|
||||
projectOwner = undefined,
|
||||
}: any = {}
|
||||
{ docOptions = {}, permissions = {} }: any = {}
|
||||
) => {
|
||||
return {
|
||||
editor: {
|
||||
@@ -21,55 +13,8 @@ export const mockScope = (
|
||||
currentDocumentId: docId,
|
||||
wantTrackChanges: false,
|
||||
},
|
||||
project: {
|
||||
_id: 'test-project',
|
||||
name: 'Test Project',
|
||||
spellCheckLanguage: 'en',
|
||||
rootFolder: [
|
||||
{
|
||||
_id: rootFolderId,
|
||||
name: 'rootFolder',
|
||||
docs: [
|
||||
{
|
||||
_id: docId,
|
||||
name: 'test.tex',
|
||||
},
|
||||
],
|
||||
folders: [
|
||||
{
|
||||
_id: figuresFolderId,
|
||||
name: 'figures',
|
||||
docs: [
|
||||
{
|
||||
_id: 'fake-nested-doc-id',
|
||||
name: 'foo.tex',
|
||||
},
|
||||
],
|
||||
folders: [],
|
||||
fileRefs: [
|
||||
{
|
||||
_id: figureId,
|
||||
name: 'frog.jpg',
|
||||
hash: '42',
|
||||
},
|
||||
{
|
||||
_id: 'fake-figure-id',
|
||||
name: 'unicorn.png',
|
||||
hash: '43',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
fileRefs: [],
|
||||
},
|
||||
] as Folder[],
|
||||
features: {
|
||||
trackChanges: true,
|
||||
...projectFeatures,
|
||||
},
|
||||
trackChangesState: {},
|
||||
members: [],
|
||||
owner: projectOwner,
|
||||
pdf: {
|
||||
logEntryAnnotations: {},
|
||||
},
|
||||
permissions: {
|
||||
comment: true,
|
||||
|
||||
@@ -23,10 +23,12 @@ import {
|
||||
EditorOpenDocContext,
|
||||
type EditorOpenDocContextState,
|
||||
} from '@/features/ide-react/context/editor-open-doc-context'
|
||||
import { ProjectContext } from '@/shared/context/project-context'
|
||||
import { ReactContextRoot } from '@/features/ide-react/context/react-context-root'
|
||||
import useEventListener from '@/shared/hooks/use-event-listener'
|
||||
import useDetachLayout from '@/shared/hooks/use-detach-layout'
|
||||
import useExposedState from '@/shared/hooks/use-exposed-state'
|
||||
import { ProjectSnapshot } from '@/infrastructure/project-snapshot'
|
||||
import {
|
||||
EditorPropertiesContext,
|
||||
EditorPropertiesContextValue,
|
||||
@@ -42,6 +44,12 @@ import type { PermissionsLevel } from '@/features/ide-react/types/permissions'
|
||||
import type { Folder } from '../../../types/folder'
|
||||
import type { SocketDebuggingInfo } from '@/features/ide-react/connection/types/connection-state'
|
||||
import type { DocumentContainer } from '@/features/ide-react/editor/document-container'
|
||||
import {
|
||||
ProjectMetadata,
|
||||
ProjectUpdate,
|
||||
} from '@/shared/context/types/project-metadata'
|
||||
import { UserId } from '../../../types/user'
|
||||
import { ProjectCompiler } from '../../../types/project-settings'
|
||||
|
||||
// these constants can be imported in tests instead of
|
||||
// using magic strings
|
||||
@@ -68,10 +76,11 @@ const defaultUserSettings = {
|
||||
export type EditorProvidersProps = {
|
||||
user?: { id: string; email: string }
|
||||
projectId?: string
|
||||
projectOwner?: { _id: string; email: string }
|
||||
projectName?: string
|
||||
projectOwner?: ProjectMetadata['owner']
|
||||
rootDocId?: string
|
||||
imageName?: string
|
||||
compiler?: string
|
||||
compiler?: ProjectCompiler
|
||||
socket?: Socket
|
||||
isRestrictedTokenMember?: boolean
|
||||
scope?: Record<string, any>
|
||||
@@ -85,6 +94,46 @@ export type EditorProvidersProps = {
|
||||
providers?: Record<string, React.FC<React.PropsWithChildren<any>>>
|
||||
}
|
||||
|
||||
export const projectDefaults = {
|
||||
_id: PROJECT_ID,
|
||||
name: PROJECT_NAME,
|
||||
owner: {
|
||||
_id: '124abd' as UserId,
|
||||
email: 'owner@example.com',
|
||||
first_name: 'Test',
|
||||
last_name: 'Owner',
|
||||
privileges: 'owner',
|
||||
signUpDate: new Date('2025-07-07').toISOString(),
|
||||
},
|
||||
features: {
|
||||
referencesSearch: true,
|
||||
gitBridge: false,
|
||||
},
|
||||
rootDocId: '_root_doc_id',
|
||||
rootFolder: [
|
||||
{
|
||||
_id: 'root-folder-id',
|
||||
name: 'rootFolder',
|
||||
docs: [
|
||||
{
|
||||
_id: '_root_doc_id',
|
||||
name: 'main.tex',
|
||||
},
|
||||
],
|
||||
folders: [],
|
||||
fileRefs: [],
|
||||
},
|
||||
],
|
||||
imageName: 'texlive-full:2024.1',
|
||||
compiler: 'pdflatex' as ProjectCompiler,
|
||||
members: [],
|
||||
invites: [],
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {import('@/shared/context/layout-context').LayoutContextValue} LayoutContextValue
|
||||
* @type Partial<LayoutContextValue>
|
||||
*/
|
||||
const layoutContextDefault = {
|
||||
view: 'editor',
|
||||
openFile: null,
|
||||
@@ -99,37 +148,23 @@ const layoutContextDefault = {
|
||||
|
||||
export function EditorProviders({
|
||||
user = { id: USER_ID, email: USER_EMAIL },
|
||||
projectId = PROJECT_ID,
|
||||
projectOwner = {
|
||||
_id: '124abd',
|
||||
email: 'owner@example.com',
|
||||
},
|
||||
rootDocId = '_root_doc_id',
|
||||
imageName = 'texlive-full:2024.1',
|
||||
compiler = 'pdflatex',
|
||||
projectId = projectDefaults._id,
|
||||
projectName = projectDefaults.name,
|
||||
projectOwner = projectDefaults.owner,
|
||||
rootDocId = projectDefaults.rootDocId,
|
||||
imageName = projectDefaults.imageName,
|
||||
compiler = projectDefaults.compiler,
|
||||
socket = new SocketIOMock() as any as Socket,
|
||||
isRestrictedTokenMember = false,
|
||||
scope: defaultScope = {},
|
||||
features = {
|
||||
referencesSearch: true,
|
||||
gitBridge: false,
|
||||
},
|
||||
projectFeatures = features,
|
||||
permissionsLevel = 'owner',
|
||||
children,
|
||||
rootFolder = [
|
||||
{
|
||||
_id: 'root-folder-id',
|
||||
name: 'rootFolder',
|
||||
docs: [
|
||||
{
|
||||
_id: '_root_doc_id',
|
||||
name: 'main.tex',
|
||||
},
|
||||
],
|
||||
folders: [],
|
||||
fileRefs: [],
|
||||
},
|
||||
],
|
||||
rootFolder = projectDefaults.rootFolder,
|
||||
/** @type {Partial<LayoutContext>} */
|
||||
layoutContext = layoutContextDefault,
|
||||
userSettings = {},
|
||||
@@ -166,21 +201,26 @@ export function EditorProviders({
|
||||
currentDocumentId: null,
|
||||
wantTrackChanges: false,
|
||||
},
|
||||
project: {
|
||||
_id: projectId,
|
||||
name: PROJECT_NAME,
|
||||
owner: projectOwner,
|
||||
features: projectFeatures,
|
||||
rootDocId,
|
||||
rootFolder,
|
||||
imageName,
|
||||
compiler,
|
||||
},
|
||||
permissionsLevel,
|
||||
},
|
||||
defaultScope
|
||||
)
|
||||
|
||||
const project = {
|
||||
_id: projectId,
|
||||
name: projectName,
|
||||
owner: projectOwner,
|
||||
features: projectFeatures,
|
||||
rootDocId,
|
||||
rootFolder,
|
||||
imageName,
|
||||
compiler,
|
||||
members: [],
|
||||
invites: [],
|
||||
trackChangesState: false,
|
||||
spellCheckLanguage: 'en',
|
||||
}
|
||||
|
||||
// Add details for useUserContext
|
||||
window.metaAttributesCache.set('ol-user', { ...user, features })
|
||||
window.metaAttributesCache.set('ol-project_id', projectId)
|
||||
@@ -199,6 +239,7 @@ export function EditorProviders({
|
||||
wantTrackChanges: scope.editor.wantTrackChanges,
|
||||
}),
|
||||
LayoutProvider: makeLayoutProvider(layoutContext),
|
||||
ProjectProvider: makeProjectProvider(project),
|
||||
...providers,
|
||||
}}
|
||||
>
|
||||
@@ -503,3 +544,35 @@ export function makeEditorPropertiesProvider(
|
||||
|
||||
return EditorPropertiesProvider
|
||||
}
|
||||
|
||||
export function makeProjectProvider(initialProject: ProjectMetadata) {
|
||||
const ProjectProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
const [project, setProject] = useState(initialProject)
|
||||
|
||||
const updateProject = useCallback((projectUpdateData: ProjectUpdate) => {
|
||||
setProject(projectData =>
|
||||
Object.assign({}, projectData, projectUpdateData)
|
||||
)
|
||||
}, [])
|
||||
|
||||
const value = {
|
||||
projectId: project._id,
|
||||
project,
|
||||
joinProject: () => {},
|
||||
updateProject,
|
||||
joinedOnce: true,
|
||||
projectSnapshot: new ProjectSnapshot(project._id),
|
||||
tags: [],
|
||||
features: project.features,
|
||||
name: project.name,
|
||||
}
|
||||
|
||||
return (
|
||||
<ProjectContext.Provider value={value}>
|
||||
{children}
|
||||
</ProjectContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
return ProjectProvider
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user