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:
Tim Down
2025-07-10 08:06:31 +00:00
committed by Copybot
parent 9237d8227b
commit 905cc5d45f
79 changed files with 806 additions and 703 deletions
@@ -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))