Merge pull request #15342 from overleaf/td-remove-file-tree-manager-in-react

Remove use of FileTreeManager in React code

GitOrigin-RevId: f15bc9b4f84e0f65709b9850ed8cc5d3637efa7f
This commit is contained in:
Tim Down 2023-10-26 11:01:08 +01:00 committed by Copybot
parent 01439641ca
commit 13f246a85e
22 changed files with 643 additions and 205 deletions

View file

@ -0,0 +1,81 @@
import { createContext, FC, useCallback, useContext, useMemo } from 'react'
import { Folder } from '../../../../../types/folder'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import getMeta from '@/utils/meta'
import {
findEntityByPath,
previewByPath,
dirname,
FindResult,
pathInFolder,
} from '@/features/file-tree/util/path'
import { PreviewPath } from '../../../../../types/preview-path'
type FileTreePathContextValue = {
pathInFolder: (id: string) => string | null
findEntityByPath: (path: string) => FindResult | null
previewByPath: (path: string) => PreviewPath | null
dirname: (id: string) => string | null
}
export const FileTreePathContext = createContext<
FileTreePathContextValue | undefined
>(undefined)
export const FileTreePathProvider: FC = ({ children }) => {
const { fileTreeData }: { fileTreeData: Folder } = useFileTreeData()
const projectId = getMeta('ol-project_id') as string
const pathInFileTree = useCallback(
(id: string) => pathInFolder(fileTreeData, id),
[fileTreeData]
)
const findEntityByPathInFileTree = useCallback(
(path: string) => findEntityByPath(fileTreeData, path),
[fileTreeData]
)
const previewByPathInFileTree = useCallback(
(path: string) => previewByPath(fileTreeData, projectId, path),
[fileTreeData, projectId]
)
const dirnameInFileTree = useCallback(
(id: string) => dirname(fileTreeData, id),
[fileTreeData]
)
const value = useMemo<FileTreePathContextValue>(
() => ({
pathInFolder: pathInFileTree,
findEntityByPath: findEntityByPathInFileTree,
previewByPath: previewByPathInFileTree,
dirname: dirnameInFileTree,
}),
[
pathInFileTree,
findEntityByPathInFileTree,
previewByPathInFileTree,
dirnameInFileTree,
]
)
return (
<FileTreePathContext.Provider value={value}>
{children}
</FileTreePathContext.Provider>
)
}
export function useFileTreePathContext(): FileTreePathContextValue {
const context = useContext(FileTreePathContext)
if (!context) {
throw new Error(
'useFileTreePathContext is only available inside FileTreePathProvider'
)
}
return context
}

View file

@ -46,7 +46,7 @@ export function createEntityInTree(tree, parentFolderId, newEntityData) {
}
function mutateInTree(tree, id, mutationFunction) {
if (tree._id === id) {
if (!id || tree._id === id) {
// covers the root folder case: it has no parent so in order to use
// mutationFunction we pass an empty array as the parent and return the
// mutated tree directly

View file

@ -0,0 +1,138 @@
import { Folder } from '../../../../../types/folder'
import { FileTreeEntity } from '../../../../../types/file-tree-entity'
import { Doc } from '../../../../../types/doc'
import { FileRef } from '../../../../../types/file-ref'
import { PreviewPath } from '../../../../../types/preview-path'
type DocFindResult = {
entity: Doc
type: 'doc'
}
type FolderFindResult = {
entity: Folder
type: 'folder'
}
type FileRefFindResult = {
entity: FileRef
type: 'fileRef'
}
export type FindResult = DocFindResult | FolderFindResult | FileRefFindResult
// Finds the entity with a given ID in the tree represented by `folder` and
// returns a path to that entity, represented by an array of folders starting at
// the root plus the entity itself
function pathComponentsInFolder(
folder: Folder,
id: string,
ancestors: FileTreeEntity[] = []
): FileTreeEntity[] | null {
const docOrFileRef =
folder.docs.find(doc => doc._id === id) ||
folder.fileRefs.find(fileRef => fileRef._id === id)
if (docOrFileRef) {
return ancestors.concat([docOrFileRef])
}
for (const subfolder of folder.folders) {
if (subfolder._id === id) {
return ancestors.concat([subfolder])
} else {
const path = pathComponentsInFolder(
subfolder,
id,
ancestors.concat([subfolder])
)
if (path !== null) {
return path
}
}
}
return null
}
// Finds the entity with a given ID in the tree represented by `folder` and
// returns a path to that entity as a string
export function pathInFolder(folder: Folder, id: string): string | null {
return (
pathComponentsInFolder(folder, id)
?.map(entity => entity.name)
.join('/') || null
)
}
export function findEntityByPath(
folder: Folder,
path: string
): FindResult | null {
if (path === '') {
return { entity: folder, type: 'folder' }
}
const parts = path.split('/')
const name = parts.shift()
const rest = parts.join('/')
if (name === '.') {
return findEntityByPath(folder, rest)
}
const doc = folder.docs.find(doc => doc.name === name)
if (doc) {
return { entity: doc, type: 'doc' }
}
const fileRef = folder.fileRefs.find(fileRef => fileRef.name === name)
if (fileRef) {
return { entity: fileRef, type: 'fileRef' }
}
for (const subfolder of folder.folders) {
if (subfolder.name === name) {
if (rest === '') {
return { entity: subfolder, type: 'folder' }
} else {
return findEntityByPath(subfolder, rest)
}
}
}
return null
}
export function previewByPath(
folder: Folder,
projectId: string,
path: string
): PreviewPath | null {
for (const suffix of [
'',
'.png',
'.jpg',
'.jpeg',
'.pdf',
'.PNG',
'.JPG',
'.JPEG',
'.PDF',
]) {
const result = findEntityByPath(folder, path + suffix)
if (result) {
const { name, _id: id } = result.entity
return {
url: `/project/${projectId}/file/${id}`,
extension: name.slice(name.lastIndexOf('.')),
}
}
}
return null
}
export function dirname(fileTreeData: Folder, id: string) {
const path = pathInFolder(fileTreeData, id)
return path?.split('/').slice(0, -1).join('/') || null
}

View file

@ -1,51 +1,100 @@
import { sendMB } from '../../../../infrastructure/event-tracking'
import { useIdeContext } from '../../../../shared/context/ide-context'
import { useLayoutContext } from '../../../../shared/context/layout-context'
import useAsync from '../../../../shared/hooks/use-async'
import { restoreFile } from '../../services/api'
import { isFileRemoved } from '../../utils/file-diff'
import { waitFor } from '../../utils/wait-for'
import { useHistoryContext } from '../history-context'
import type { HistoryContextValue } from '../types/history-context-value'
import { useErrorHandler } from 'react-error-boundary'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import { findInTree } from '@/features/file-tree/util/find-in-tree'
import { useCallback, useEffect, useState } from 'react'
import { RestoreFileResponse } from '@/features/history/services/types/restore-file'
type RestorationState =
| 'idle'
| 'restoring'
| 'waitingForFileTree'
| 'complete'
| 'error'
| 'timedOut'
export function useRestoreDeletedFile() {
const { isLoading, runAsync } = useAsync()
const { projectId } = useHistoryContext()
const ide = useIdeContext()
const { setView } = useLayoutContext()
const handleError = useErrorHandler()
const { fileTreeData } = useFileTreeData()
const [state, setState] = useState<RestorationState>('idle')
const [restoredFileMetadata, setRestoredFileMetadata] =
useState<RestoreFileResponse | null>(null)
const restoreDeletedFile = async (
selection: HistoryContextValue['selection']
) => {
const isLoading = state === 'restoring' || state === 'waitingForFileTree'
useEffect(() => {
if (state === 'waitingForFileTree' && restoredFileMetadata) {
const result = findInTree(fileTreeData, restoredFileMetadata.id)
if (result) {
setState('complete')
const { _id: id } = result.entity
setView('editor')
if (restoredFileMetadata.type === 'doc') {
ide.editorManager.openDocId(id)
} else {
ide.binaryFilesManager.openFileWithId(id)
}
// Get the file tree to select the entity that has just been restored
window.dispatchEvent(new CustomEvent('editor.openDoc', { detail: id }))
}
}
}, [
state,
fileTreeData,
restoredFileMetadata,
ide.editorManager,
ide.binaryFilesManager,
setView,
])
useEffect(() => {
if (state === 'waitingForFileTree') {
const timer = window.setTimeout(() => {
setState('timedOut')
handleError(new Error('timed out'))
}, 3000)
return () => {
window.clearTimeout(timer)
}
}
}, [handleError, state])
const restoreDeletedFile = useCallback(
(selection: HistoryContextValue['selection']) => {
const { selectedFile } = selection
if (selectedFile && selectedFile.pathname && isFileRemoved(selectedFile)) {
if (
selectedFile &&
selectedFile.pathname &&
isFileRemoved(selectedFile)
) {
sendMB('history-v2-restore-deleted')
await runAsync(
restoreFile(projectId, selectedFile)
.then(async data => {
const { id, type } = data
const entity = await waitFor(
() => ide.fileTreeManager.findEntityById(id),
3000
)
if (type === 'doc') {
ide.editorManager.openDoc(entity)
} else {
ide.binaryFilesManager.openFile(entity)
setState('restoring')
restoreFile(projectId, selectedFile).then(
(data: RestoreFileResponse) => {
setRestoredFileMetadata(data)
setState('waitingForFileTree')
},
error => {
setState('error')
handleError(error)
}
setView('editor')
})
.catch(handleError)
)
}
}
},
[handleError, projectId]
)
return { restoreDeletedFile, isLoading }
}

View file

@ -20,6 +20,7 @@ import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
import useScopeEventListener from '../../../shared/hooks/use-scope-event-listener'
import * as eventTracking from '../../../infrastructure/event-tracking'
import { debugConsole } from '@/utils/debugging'
import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
function GoToCodeButton({
position,
@ -118,7 +119,7 @@ function GoToPdfButton({
function PdfSynctexControls() {
const ide = useIdeContext()
const { _id: projectId } = useProjectContext()
const { _id: projectId, rootDocId } = useProjectContext()
const { detachRole } = useLayoutContext()
@ -132,6 +133,7 @@ function PdfSynctexControls() {
} = useCompileContext()
const { selectedEntities } = useFileTreeData()
const { findEntityByPath, dirname, pathInFolder } = useFileTreePathContext()
const [cursorPosition, setCursorPosition] = useState(() => {
const position = localStorage.getItem(
@ -162,26 +164,28 @@ function PdfSynctexControls() {
const getCurrentFilePath = useCallback(() => {
const docId = ide.editorManager.getCurrentDocId()
const doc = ide.fileTreeManager.findEntityById(docId)
let path = ide.fileTreeManager.getEntityPath(doc)
let path = pathInFolder(docId)
// If the root file is folder/main.tex, then synctex sees the path as folder/./main.tex
const rootDocDirname = ide.fileTreeManager.getRootDocDirname()
const rootDocDirname = dirname(rootDocId)
if (rootDocDirname) {
path = path.replace(RegExp(`^${rootDocDirname}`), `${rootDocDirname}/.`)
}
return path
}, [ide])
}, [dirname, ide.editorManager, pathInFolder, rootDocId])
const goToCodeLine = useCallback(
(file, line) => {
if (file) {
const doc = ide.fileTreeManager.findEntityByPath(file)
const doc = findEntityByPath(file)?.entity
if (!doc) {
debugConsole.warn(`Document with path ${file} not found`)
return
}
ide.editorManager.openDoc(doc, {
ide.editorManager.openDocId(doc._id, {
gotoLine: line,
})
} else {
@ -194,7 +198,7 @@ function PdfSynctexControls() {
}, 4000)
}
},
[ide, isMounted, setSynctexError]
[findEntityByPath, ide.editorManager, isMounted, setSynctexError]
)
const goToPdfLocation = useCallback(

View file

@ -4,6 +4,7 @@ import BibLogParser from '../../../ide/log-parser/bib-log-parser'
import { v4 as uuid } from 'uuid'
import { enablePdfCaching } from './pdf-caching-flags'
import { debugConsole } from '@/utils/debugging'
import { dirname, findEntityByPath } from '@/features/file-tree/util/path'
// Warnings that may disappear after a second LaTeX pass
const TRANSIENT_WARNING_REGEX = /^(Reference|Citation).+undefined on input line/
@ -133,8 +134,8 @@ export const handleLogFiles = async (outputFiles, data, signal) => {
return result
}
export function buildLogEntryAnnotations(entries, fileTreeManager) {
const rootDocDirname = fileTreeManager.getRootDocDirname()
export function buildLogEntryAnnotations(entries, fileTreeData, rootDocId) {
const rootDocDirname = dirname(fileTreeData, rootDocId)
const logEntryAnnotations = {}
@ -142,14 +143,14 @@ export function buildLogEntryAnnotations(entries, fileTreeManager) {
if (entry.file) {
entry.file = normalizeFilePath(entry.file, rootDocDirname)
const entity = fileTreeManager.findEntityByPath(entry.file)
const entity = findEntityByPath(fileTreeData, entry.file)?.entity
if (entity) {
if (!(entity.id in logEntryAnnotations)) {
logEntryAnnotations[entity.id] = []
if (!(entity._id in logEntryAnnotations)) {
logEntryAnnotations[entity._id] = []
}
logEntryAnnotations[entity.id].push({
logEntryAnnotations[entity._id].push({
row: entry.line - 1,
type: entry.level === 'error' ? 'error' : 'warning',
text: entry.message,

View file

@ -66,13 +66,10 @@ import { skipPreambleWithCursor } from './skip-preamble-cursor'
import { TableRenderingErrorWidget } from './visual-widgets/table-rendering-error'
import { GraphicsWidget } from './visual-widgets/graphics'
import { InlineGraphicsWidget } from './visual-widgets/inline-graphics'
import { PreviewPath } from '../../../../../../types/preview-path'
type Options = {
fileTreeManager: {
getPreviewByPath: (
path: string
) => { url: string; extension: string } | null
}
previewByPath: (path: string) => PreviewPath | null
}
function shouldDecorate(
@ -135,9 +132,7 @@ const hasClosingBrace = (node: SyntaxNode) =>
* Decorations that span multiple lines must be contained in a StateField, not a ViewPlugin.
*/
export const atomicDecorations = (options: Options) => {
const getPreviewByPath = (path: string) =>
options.fileTreeManager.getPreviewByPath(path)
const { previewByPath } = options
const createDecorations = (
state: EditorState,
tree: Tree
@ -895,7 +890,7 @@ export const atomicDecorations = (options: Options) => {
Decoration.replace({
widget: new Widget(
filePath,
getPreviewByPath,
previewByPath,
centered,
figureData
),
@ -910,7 +905,7 @@ export const atomicDecorations = (options: Options) => {
Decoration.replace({
widget: new Widget(
filePath,
getPreviewByPath,
previewByPath,
centered,
figureData
),

View file

@ -3,6 +3,7 @@ import { placeSelectionInsideBlock } from '../selection'
import { isEqual } from 'lodash'
import { FigureData } from '../../figure-modal'
import { debugConsole } from '@/utils/debugging'
import { PreviewPath } from '../../../../../../../types/preview-path'
export class GraphicsWidget extends WidgetType {
destroyed = false
@ -10,9 +11,7 @@ export class GraphicsWidget extends WidgetType {
constructor(
public filePath: string,
public getPreviewByPath: (
filePath: string
) => { url: string; extension: string } | null,
public previewByPath: (path: string) => PreviewPath | null,
public centered: boolean,
public figureData: FigureData | null
) {
@ -82,7 +81,7 @@ export class GraphicsWidget extends WidgetType {
renderGraphic(element: HTMLElement, view: EditorView) {
element.textContent = '' // ensure the element is empty
const preview = this.getPreviewByPath(this.filePath)
const preview = this.previewByPath(this.filePath)
element.dataset.filepath = this.filePath
element.dataset.width = this.figureData?.width?.toString()

View file

@ -24,14 +24,11 @@ import { pasteHtml } from './paste-html'
import { commandTooltip } from '../command-tooltip'
import { tableGeneratorTheme } from './table-generator'
import { debugConsole } from '@/utils/debugging'
import { PreviewPath } from '../../../../../../types/preview-path'
type Options = {
visual: boolean
fileTreeManager: {
getPreviewByPath: (
path: string
) => { url: string; extension: string } | null
}
previewByPath: (path: string) => PreviewPath | null
}
const visualConf = new Compartment()

View file

@ -48,6 +48,7 @@ import { CurrentDoc } from '../../../../../types/current-doc'
import { useErrorHandler } from 'react-error-boundary'
import { setVisual } from '../extensions/visual/visual'
import getMeta from '../../../utils/meta'
import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
function useCodeMirrorScope(view: EditorView) {
const ide = useIdeContext()
@ -244,8 +245,10 @@ function useCodeMirrorScope(view: EditorView) {
const editableRef = useRef(permissionsLevel !== 'readOnly')
const { previewByPath } = useFileTreePathContext()
const visualRef = useRef({
fileTreeManager: ide.fileTreeManager,
previewByPath,
visual,
})
@ -312,6 +315,11 @@ function useCodeMirrorScope(view: EditorView) {
window.dispatchEvent(new Event('editor:visual-switch'))
}, [view, visual])
useEffect(() => {
visualRef.current.previewByPath = previewByPath
view.dispatch(setVisual(visualRef.current))
}, [view, previewByPath])
useEffect(() => {
editableRef.current = permissionsLevel !== 'readOnly'
view.dispatch(setEditable(editableRef.current)) // the editor needs to be locked when there's a problem saving data

View file

@ -50,6 +50,13 @@ export default BinaryFilesManager = class BinaryFilesManager {
)
}
openFileWithId(id) {
const entity = this.ide.fileTreeManager.findEntityById(id)
if (entity?.type === 'file') {
this.openFile(entity)
}
}
closeFile() {
return window.setTimeout(
() => {

View file

@ -29,6 +29,8 @@ import { useEditorContext } from './editor-context'
import { buildFileList } from '../../features/pdf-preview/util/file-list'
import { useLayoutContext } from './layout-context'
import { useUserContext } from './user-context'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
export const LocalCompileContext = createContext()
@ -95,6 +97,9 @@ export function LocalCompileProvider({ children }) {
const { features } = useUserContext()
const { fileTreeData } = useFileTreeData()
const { findEntityByPath } = useFileTreePathContext()
// whether a compile is in progress
const [compiling, setCompiling] = useState(false)
@ -245,6 +250,17 @@ export function LocalCompileProvider({ children }) {
compilingRef.current = compiling
}, [compiling])
const _buildLogEntryAnnotations = useCallback(
entries => buildLogEntryAnnotations(entries, fileTreeData, rootDocId),
[fileTreeData, rootDocId]
)
const buildLogEntryAnnotationsRef = useRef(_buildLogEntryAnnotations)
useEffect(() => {
buildLogEntryAnnotationsRef.current = _buildLogEntryAnnotations
}, [_buildLogEntryAnnotations])
// the document compiler
const [compiler] = useState(() => {
return new DocumentCompiler({
@ -349,10 +365,7 @@ export function LocalCompileProvider({ children }) {
setRawLog(result.log)
setLogEntries(result.logEntries)
setLogEntryAnnotations(
buildLogEntryAnnotations(
result.logEntries.all,
ide.fileTreeManager
)
buildLogEntryAnnotationsRef.current(result.logEntries.all)
)
// sample compile stats for real users
@ -521,16 +534,16 @@ export function LocalCompileProvider({ children }) {
const syncToEntry = useCallback(
entry => {
const entity = ide.fileTreeManager.findEntityByPath(entry.file)
const result = findEntityByPath(entry.file)
if (entity && entity.type === 'doc') {
ide.editorManager.openDoc(entity, {
if (result && result.type === 'doc') {
ide.editorManager.openDocId(result.entity._id, {
gotoLine: entry.line ?? undefined,
gotoColumn: entry.column ?? undefined,
})
}
},
[ide]
[findEntityByPath, ide.editorManager]
)
// clear the cache then run a compile, triggered by a menu item

View file

@ -13,6 +13,7 @@ import { ProjectProvider } from './project-context'
import { SplitTestProvider } from './split-test-context'
import { FileTreeDataProvider } from './file-tree-data-context'
import { ProjectSettingsProvider } from '@/features/editor-left-menu/context/project-settings-context'
import { FileTreePathProvider } from '@/features/file-tree/contexts/file-tree-path'
export function ContextRoot({ children, ide }) {
return (
@ -21,6 +22,7 @@ export function ContextRoot({ children, ide }) {
<UserProvider>
<ProjectProvider>
<FileTreeDataProvider>
<FileTreePathProvider>
<DetachProvider>
<EditorProvider>
<ProjectSettingsProvider>
@ -34,6 +36,7 @@ export function ContextRoot({ children, ide }) {
</ProjectSettingsProvider>
</EditorProvider>
</DetachProvider>
</FileTreePathProvider>
</FileTreeDataProvider>
</ProjectProvider>
</UserProvider>

View file

@ -1,6 +1,5 @@
import { useEffect, useMemo } from 'react'
import { get } from 'lodash'
import { ContextRoot } from '../../js/shared/context/root-context'
import { User } from '../../../types/user'
import { Project } from '../../../types/project'
import {
@ -10,6 +9,18 @@ import {
} from '../fixtures/compile'
import useFetchMock from '../hooks/use-fetch-mock'
import { useMeta } from '../hooks/use-meta'
import { SplitTestProvider } from '@/shared/context/split-test-context'
import { IdeAngularProvider } from '@/shared/context/ide-angular-provider'
import { UserProvider } from '@/shared/context/user-context'
import { ProjectProvider } from '@/shared/context/project-context'
import { FileTreeDataProvider } from '@/shared/context/file-tree-data-context'
import { EditorProvider } from '@/shared/context/editor-context'
import { DetachProvider } from '@/shared/context/detach-context'
import { LayoutProvider } from '@/shared/context/layout-context'
import { LocalCompileProvider } from '@/shared/context/local-compile-context'
import { DetachCompileProvider } from '@/shared/context/detach-compile-context'
import { ProjectSettingsProvider } from '@/features/editor-left-menu/context/project-settings-context'
import { FileTreePathProvider } from '@/features/file-tree/contexts/file-tree-path'
const scopeWatchers: [string, (value: any) => void][] = []
@ -97,19 +108,6 @@ const initialize = () => {
on: () => {},
removeListener: () => {},
},
fileTreeManager: {
findEntityById: () => null,
findEntityByPath: () => null,
getEntityPath: () => null,
getRootDocDirname: () => undefined,
getPreviewByPath: (path: string) =>
path === 'frog.jpg'
? {
extension: 'png',
url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAYAAAB5fY51AAABhWlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TpaJVETuIOGSogmBBVMRRq1CECqFWaNXB5NIvaNKQpLg4Cq4FBz8Wqw4uzro6uAqC4AeIq4uToouU+L+k0CLWg+N+vLv3uHsHCNUi06y2cUDTbTMRi4qp9KoYeIWAPnShB6Mys4w5SYqj5fi6h4+vdxGe1frcn6NbzVgM8InEs8wwbeIN4ulN2+C8TxxieVklPiceM+mCxI9cVzx+45xzWeCZITOZmCcOEYu5JlaamOVNjXiKOKxqOuULKY9VzluctWKZ1e/JXxjM6CvLXKc5hBgWsQQJIhSUUUARNiK06qRYSNB+tIV/0PVL5FLIVQAjxwJK0CC7fvA/+N2tlZ2c8JKCUaD9xXE+hoHALlCrOM73sePUTgD/M3ClN/ylKjDzSXqloYWPgN5t4OK6oSl7wOUOMPBkyKbsSn6aQjYLvJ/RN6WB/lugc83rrb6P0wcgSV3Fb4CDQ2AkR9nrLd7d0dzbv2fq/f0ARfNylZJUgMQAAAAGYktHRABuAP8AAGHZRr4AAAAJcEhZcwAALiMAAC4jAXilP3YAAAAHdElNRQfnAhELEhgyPeVkAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAAyVJREFUeNrt1rEJgDAURVGVNCmS2hS6PziCteIYWjuEbiEfOWeEV1xe35bt6QhjnlYjBJLzbYRABhMAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBSBYgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBbwX6m13QqB5HwbIZBSLyN4WACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBfCFVMtphUBKvYwQSBsPI3hYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIF8IUXjtUMuBMh1xAAAAAASUVORK5CYII=',
}
: null,
},
editorManager: {
getCurrentDocId: () => 'foo',
openDoc: (id: string, options: unknown) => {
@ -207,6 +205,7 @@ const initialize = () => {
type ScopeDecoratorOptions = {
mockCompileOnLoad: boolean
providers?: Record<string, any>
}
export const ScopeDecorator = (
@ -237,9 +236,47 @@ export const ScopeDecorator = (
// set values on window.metaAttributesCache (created in initialize, above)
useMeta(meta)
const Providers = {
DetachCompileProvider,
DetachProvider,
EditorProvider,
FileTreeDataProvider,
FileTreePathProvider,
IdeAngularProvider,
LayoutProvider,
LocalCompileProvider,
ProjectProvider,
ProjectSettingsProvider,
SplitTestProvider,
UserProvider,
...opts.providers,
}
return (
<ContextRoot ide={ide}>
<Providers.SplitTestProvider>
<Providers.IdeAngularProvider ide={ide}>
<Providers.UserProvider>
<Providers.ProjectProvider>
<Providers.FileTreeDataProvider>
<Providers.FileTreePathProvider>
<Providers.DetachProvider>
<Providers.EditorProvider>
<Providers.ProjectSettingsProvider>
<Providers.LayoutProvider>
<Providers.LocalCompileProvider>
<Providers.DetachCompileProvider>
<Story />
</ContextRoot>
</Providers.DetachCompileProvider>
</Providers.LocalCompileProvider>
</Providers.LayoutProvider>
</Providers.ProjectSettingsProvider>
</Providers.EditorProvider>
</Providers.DetachProvider>
</Providers.FileTreePathProvider>
</Providers.FileTreeDataProvider>
</Providers.ProjectProvider>
</Providers.UserProvider>
</Providers.IdeAngularProvider>
</Providers.SplitTestProvider>
)
}

View file

@ -2,12 +2,37 @@ import SourceEditor from '../../js/features/source-editor/components/source-edit
import { ScopeDecorator } from '../decorators/scope'
import { useScope } from '../hooks/use-scope'
import { useMeta } from '../hooks/use-meta'
import { FC } from 'react'
import { FileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
const FileTreePathProvider: FC = ({ children }) => (
<FileTreePathContext.Provider
value={{
dirname: () => null,
findEntityByPath: () => null,
pathInFolder: () => null,
previewByPath: (path: string) =>
path === 'frog.jpg'
? {
extension: 'png',
url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAYAAAB5fY51AAABhWlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9TpaJVETuIOGSogmBBVMRRq1CECqFWaNXB5NIvaNKQpLg4Cq4FBz8Wqw4uzro6uAqC4AeIq4uToouU+L+k0CLWg+N+vLv3uHsHCNUi06y2cUDTbTMRi4qp9KoYeIWAPnShB6Mys4w5SYqj5fi6h4+vdxGe1frcn6NbzVgM8InEs8wwbeIN4ulN2+C8TxxieVklPiceM+mCxI9cVzx+45xzWeCZITOZmCcOEYu5JlaamOVNjXiKOKxqOuULKY9VzluctWKZ1e/JXxjM6CvLXKc5hBgWsQQJIhSUUUARNiK06qRYSNB+tIV/0PVL5FLIVQAjxwJK0CC7fvA/+N2tlZ2c8JKCUaD9xXE+hoHALlCrOM73sePUTgD/M3ClN/ylKjDzSXqloYWPgN5t4OK6oSl7wOUOMPBkyKbsSn6aQjYLvJ/RN6WB/lugc83rrb6P0wcgSV3Fb4CDQ2AkR9nrLd7d0dzbv2fq/f0ARfNylZJUgMQAAAAGYktHRABuAP8AAGHZRr4AAAAJcEhZcwAALiMAAC4jAXilP3YAAAAHdElNRQfnAhELEhgyPeVkAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAAyVJREFUeNrt1rEJgDAURVGVNCmS2hS6PziCteIYWjuEbiEfOWeEV1xe35bt6QhjnlYjBJLzbYRABhMAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBSBYgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBbwX6m13QqB5HwbIZBSLyN4WACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBfCFVMtphUBKvYwQSBsPI3hYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIFIFgAggUIFoBgAQgWIFgAggUgWIBgAQgWgGABggUgWACCBQgWgGABCBYgWACCBQgWgGABCBYgWACCBSBYgGABCBaAYAGCBSBYAIIFCBaAYAEIFiBYAIIFIFiAYAEIFoBgAYIF8IUXjtUMuBMh1xAAAAAASUVORK5CYII=',
}
: null,
}}
>
{children}
</FileTreePathContext.Provider>
)
export default {
title: 'Editor / Source Editor',
component: SourceEditor,
decorators: [
ScopeDecorator,
(Story: any) =>
ScopeDecorator(Story, {
mockCompileOnLoad: true,
providers: { FileTreePathProvider },
}),
(Story: any) => (
<div style={{ height: '90vh' }}>
<Story />
@ -93,29 +118,6 @@ export const Visual = (args: any, { globals: { theme } }: any) => {
open_doc_name: 'example.tex',
showVisual: true,
},
rootFolder: {
name: 'rootFolder',
id: 'root-folder-id',
type: 'folder',
children: [
{
name: 'example.tex.tex',
id: 'example-doc-id',
type: 'doc',
selected: false,
$$hashKey: 'object:89',
},
{
name: 'frog.jpg',
id: 'frog-image-id',
type: 'file',
linkedFileData: null,
created: '2023-05-04T16:11:04.352Z',
$$hashKey: 'object:108',
},
],
selected: false,
},
settings: {
...settings,
overallTheme: theme === 'default-' ? '' : theme,
@ -127,6 +129,7 @@ export const Visual = (args: any, { globals: { theme } }: any) => {
'ol-completedTutorials': {
'table-generator-promotion': '2023-09-01T00:00:00.000Z',
},
'ol-project_id': '63e21c07946dd8c76505f85a',
})
return <SourceEditor />

View file

@ -1,9 +1,31 @@
import { EditorProviders } from '../../helpers/editor-providers'
import PdfLogsEntries from '../../../../frontend/js/features/pdf-preview/components/pdf-logs-entries'
import { detachChannel, testDetachChannel } from '../../helpers/detach-channel'
import { FileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
import { FindResult } from '@/features/file-tree/util/path'
import { FC } from 'react'
describe('<PdfLogsEntries/>', function () {
const fakeEntity = { type: 'doc' }
const fakeFindEntityResult: FindResult = {
type: 'doc',
entity: { _id: '123', name: '123 Doc' },
}
const FileTreePathProvider: FC = ({ children }) => (
<FileTreePathContext.Provider
value={{
dirname: cy.stub(),
findEntityByPath: cy
.stub()
.as('findEntityByPath')
.returns(fakeFindEntityResult),
pathInFolder: cy.stub(),
previewByPath: cy.stub(),
}}
>
{children}
</FileTreePathContext.Provider>
)
const logEntries = [
{
@ -23,11 +45,8 @@ describe('<PdfLogsEntries/>', function () {
beforeEach(function () {
props = {
fileTreeManager: {
findEntityByPath: cy.stub().as('findEntityByPath').returns(fakeEntity),
},
editorManager: {
openDoc: cy.spy().as('openDoc'),
openDocId: cy.spy().as('openDocId'),
},
}
@ -47,7 +66,7 @@ describe('<PdfLogsEntries/>', function () {
it('opens doc on click', function () {
cy.mount(
<EditorProviders {...props}>
<EditorProviders {...props} providers={{ FileTreePathProvider }}>
<PdfLogsEntries entries={logEntries} />
</EditorProviders>
)
@ -57,10 +76,14 @@ describe('<PdfLogsEntries/>', function () {
}).click()
cy.get('@findEntityByPath').should('have.been.calledOnce')
cy.get('@openDoc').should('have.been.calledOnceWith', fakeEntity, {
cy.get('@openDocId').should(
'have.been.calledOnceWith',
fakeFindEntityResult.entity._id,
{
gotoLine: 9,
gotoColumn: 8,
})
}
)
})
it('opens doc via detached action', function () {
@ -69,7 +92,7 @@ describe('<PdfLogsEntries/>', function () {
})
cy.mount(
<EditorProviders {...props}>
<EditorProviders {...props} providers={{ FileTreePathProvider }}>
<PdfLogsEntries entries={logEntries} />
</EditorProviders>
).then(() => {
@ -89,10 +112,14 @@ describe('<PdfLogsEntries/>', function () {
})
cy.get('@findEntityByPath').should('have.been.calledOnce')
cy.get('@openDoc').should('have.been.calledOnceWith', fakeEntity, {
cy.get('@openDocId').should(
'have.been.calledOnceWith',
fakeFindEntityResult.entity._id,
{
gotoLine: 7,
gotoColumn: 6,
})
}
)
})
it('sends open doc clicks via detached action', function () {
@ -101,7 +128,7 @@ describe('<PdfLogsEntries/>', function () {
})
cy.mount(
<EditorProviders {...props}>
<EditorProviders {...props} providers={{ FileTreePathProvider }}>
<PdfLogsEntries entries={logEntries} />
</EditorProviders>
)
@ -113,7 +140,7 @@ describe('<PdfLogsEntries/>', function () {
}).click()
cy.get('@findEntityByPath').should('not.have.been.called')
cy.get('@openDoc').should('not.have.been.called')
cy.get('@openDocId').should('not.have.been.called')
cy.get('@postDetachMessage').should('have.been.calledWith', {
role: 'detached',
event: 'action-sync-to-entry',

View file

@ -1,20 +1,24 @@
import { mockScope } from '../helpers/mock-scope'
import { EditorProviders } from '../../../helpers/editor-providers'
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
import { FC } from 'react'
import { FC, ComponentProps } from 'react'
import { FileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
const Container: FC = ({ children }) => (
<div style={{ width: 785, height: 785 }}>{children}</div>
)
const mountEditor = (content: string, ...args: any[]) => {
const mountEditor = (
content: string,
props?: Omit<ComponentProps<typeof EditorProviders>, 'children' | 'scope'>
) => {
const scope = mockScope(content)
scope.permissionsLevel = 'readOnly'
scope.editor.showVisual = true
cy.mount(
<Container>
<EditorProviders scope={scope} {...args}>
<EditorProviders scope={scope} {...props}>
<CodemirrorEditor />
</EditorProviders>
</Container>
@ -81,15 +85,21 @@ describe('<CodeMirrorEditor/> in Visual mode with read-only permission', functio
})
it('does not display the figure edit button', function () {
const fileTreeManager = {
findEntityById: cy.stub(),
const FileTreePathProvider: FC = ({ children }) => (
<FileTreePathContext.Provider
value={{
dirname: cy.stub(),
findEntityByPath: cy.stub(),
getEntityPath: cy.stub(),
getRootDocDirname: cy.stub(),
getPreviewByPath: cy
pathInFolder: cy.stub(),
previewByPath: cy
.stub()
.as('previewByPath')
.returns({ url: '/images/frog.jpg', extension: 'jpg' }),
}
}}
>
{children}
</FileTreePathContext.Provider>
)
cy.intercept('/images/frog.jpg', { fixture: 'images/gradient.png' })
@ -100,7 +110,9 @@ describe('<CodeMirrorEditor/> in Visual mode with read-only permission', functio
\\caption{My caption}
\\label{fig:my-label}
\\end{figure}`,
{ fileTreeManager }
{
providers: { FileTreePathProvider },
}
)
cy.get('img.ol-cm-graphics').should('have.length', 1)

View file

@ -5,6 +5,7 @@ import { EditorProviders } from '../../../helpers/editor-providers'
import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/components/codemirror-editor'
import { mockScope } from '../helpers/mock-scope'
import forEach from 'mocha-each'
import { FileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
const Container: FC = ({ children }) => (
<div style={{ width: 785, height: 785 }}>{children}</div>
@ -23,9 +24,25 @@ describe('<CodeMirrorEditor/> in Visual mode', function () {
const scope = mockScope(content)
scope.editor.showVisual = true
const FileTreePathProvider: FC = ({ children }) => (
<FileTreePathContext.Provider
value={{
dirname: cy.stub(),
findEntityByPath: cy.stub(),
pathInFolder: cy.stub(),
previewByPath: cy
.stub()
.as('previewByPath')
.callsFake(path => ({ url: path, extension: 'png' })),
}}
>
{children}
</FileTreePathContext.Provider>
)
cy.mount(
<Container>
<EditorProviders scope={scope}>
<EditorProviders scope={scope} providers={{ FileTreePathProvider }}>
<CodemirrorEditor />
</EditorProviders>
</Container>

View file

@ -2,6 +2,7 @@ import CodemirrorEditor from '../../../../../frontend/js/features/source-editor/
import { EditorProviders } 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'
const Container: FC = ({ children }) => (
<div style={{ width: 1500, height: 785 }}>{children}</div>
@ -50,9 +51,25 @@ describe('<FigureModal />', function () {
const scope = mockScope(content)
scope.editor.showVisual = true
const FileTreePathProvider: FC = ({ children }) => (
<FileTreePathContext.Provider
value={{
dirname: cy.stub(),
findEntityByPath: cy.stub(),
pathInFolder: cy.stub(),
previewByPath: cy
.stub()
.as('previewByPath')
.returns({ url: 'frog.jpg', extension: 'jpg' }),
}}
>
{children}
</FileTreePathContext.Provider>
)
cy.mount(
<Container>
<EditorProviders scope={scope}>
<EditorProviders scope={scope} providers={{ FileTreePathProvider }}>
<CodemirrorEditor />
</EditorProviders>
</Container>

View file

@ -1,7 +1,7 @@
// Disable prop type checks for test harnesses
/* eslint-disable react/prop-types */
import sinon from 'sinon'
import { get } from 'lodash'
import { get, merge } from 'lodash'
import { SplitTestProvider } from '@/shared/context/split-test-context'
import { IdeAngularProvider } from '@/shared/context/ide-angular-provider'
import { UserProvider } from '@/shared/context/user-context'
@ -13,6 +13,7 @@ import { LayoutProvider } from '@/shared/context/layout-context'
import { LocalCompileProvider } from '@/shared/context/local-compile-context'
import { DetachCompileProvider } from '@/shared/context/detach-compile-context'
import { ProjectSettingsProvider } from '@/features/editor-left-menu/context/project-settings-context'
import { FileTreePathProvider } from '@/features/file-tree/contexts/file-tree-path'
// these constants can be imported in tests instead of
// using magic strings
@ -70,13 +71,15 @@ export function EditorProviders({
},
},
},
providers = {},
}) {
window.user = user || window.user
window.gitBridgePublicBaseUrl = 'https://git.overleaf.test'
window.project_id = projectId != null ? projectId : window.project_id
window.isRestrictedTokenMember = isRestrictedTokenMember
const $scope = {
const $scope = merge(
{
user: window.user,
project: {
_id: window.project_id,
@ -95,8 +98,9 @@ export function EditorProviders({
$applyAsync: sinon.stub(),
toggleHistory: sinon.stub(),
permissionsLevel,
...scope,
}
},
scope
)
window._ide = {
$scope,
@ -109,30 +113,47 @@ export function EditorProviders({
// Add details for useUserContext
window.metaAttributesCache.set('ol-user', { ...user, features })
const Providers = {
DetachCompileProvider,
DetachProvider,
EditorProvider,
FileTreeDataProvider,
FileTreePathProvider,
IdeAngularProvider,
LayoutProvider,
LocalCompileProvider,
ProjectProvider,
ProjectSettingsProvider,
SplitTestProvider,
UserProvider,
...providers,
}
return (
<SplitTestProvider>
<IdeAngularProvider ide={window._ide}>
<UserProvider>
<ProjectProvider>
<FileTreeDataProvider>
<DetachProvider>
<EditorProvider>
<ProjectSettingsProvider>
<LayoutProvider>
<LocalCompileProvider>
<DetachCompileProvider>
<Providers.SplitTestProvider>
<Providers.IdeAngularProvider ide={window._ide}>
<Providers.UserProvider>
<Providers.ProjectProvider>
<Providers.FileTreeDataProvider>
<Providers.FileTreePathProvider>
<Providers.DetachProvider>
<Providers.EditorProvider>
<Providers.ProjectSettingsProvider>
<Providers.LayoutProvider>
<Providers.LocalCompileProvider>
<Providers.DetachCompileProvider>
{children}
</DetachCompileProvider>
</LocalCompileProvider>
</LayoutProvider>
</ProjectSettingsProvider>
</EditorProvider>
</DetachProvider>
</FileTreeDataProvider>
</ProjectProvider>
</UserProvider>
</IdeAngularProvider>
</SplitTestProvider>
</Providers.DetachCompileProvider>
</Providers.LocalCompileProvider>
</Providers.LayoutProvider>
</Providers.ProjectSettingsProvider>
</Providers.EditorProvider>
</Providers.DetachProvider>
</Providers.FileTreePathProvider>
</Providers.FileTreeDataProvider>
</Providers.ProjectProvider>
</Providers.UserProvider>
</Providers.IdeAngularProvider>
</Providers.SplitTestProvider>
)
}

View file

@ -0,0 +1,5 @@
import { Folder } from './folder'
import { Doc } from './doc'
import { FileRef } from './file-ref'
export type FileTreeEntity = Folder | Doc | FileRef

View file

@ -0,0 +1,4 @@
export type PreviewPath = {
url: string
extension: string
}