From 13f246a85e0ad148616ef2ac69fe0f6b0e15131d Mon Sep 17 00:00:00 2001 From: Tim Down <158919+timdown@users.noreply.github.com> Date: Thu, 26 Oct 2023 11:01:08 +0100 Subject: [PATCH] Merge pull request #15342 from overleaf/td-remove-file-tree-manager-in-react Remove use of FileTreeManager in React code GitOrigin-RevId: f15bc9b4f84e0f65709b9850ed8cc5d3637efa7f --- .../file-tree/contexts/file-tree-path.tsx | 81 ++++++++++ .../features/file-tree/util/mutate-in-tree.js | 2 +- .../js/features/file-tree/util/path.ts | 138 ++++++++++++++++++ .../context/hooks/use-restore-deleted-file.ts | 111 ++++++++++---- .../components/pdf-synctex-controls.jsx | 22 +-- .../features/pdf-preview/util/output-files.js | 13 +- .../extensions/visual/atomic-decorations.ts | 15 +- .../visual/visual-widgets/graphics.ts | 7 +- .../source-editor/extensions/visual/visual.ts | 7 +- .../hooks/use-codemirror-scope.ts | 10 +- .../js/ide/binary-files/BinaryFilesManager.js | 7 + .../shared/context/local-compile-context.jsx | 29 +++- .../js/shared/context/root-context.jsx | 29 ++-- .../web/frontend/stories/decorators/scope.tsx | 71 ++++++--- .../source-editor/source-editor.stories.tsx | 51 ++++--- .../pdf-preview/pdf-logs-entries.spec.tsx | 61 +++++--- ...codemirror-editor-visual-readonly.spec.tsx | 38 +++-- .../codemirror-editor-visual.spec.tsx | 19 ++- .../components/figure-modal.spec.tsx | 19 ++- .../frontend/helpers/editor-providers.jsx | 109 ++++++++------ services/web/types/file-tree-entity.ts | 5 + services/web/types/preview-path.ts | 4 + 22 files changed, 643 insertions(+), 205 deletions(-) create mode 100644 services/web/frontend/js/features/file-tree/contexts/file-tree-path.tsx create mode 100644 services/web/frontend/js/features/file-tree/util/path.ts create mode 100644 services/web/types/file-tree-entity.ts create mode 100644 services/web/types/preview-path.ts diff --git a/services/web/frontend/js/features/file-tree/contexts/file-tree-path.tsx b/services/web/frontend/js/features/file-tree/contexts/file-tree-path.tsx new file mode 100644 index 0000000000..7967bd57f5 --- /dev/null +++ b/services/web/frontend/js/features/file-tree/contexts/file-tree-path.tsx @@ -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( + () => ({ + pathInFolder: pathInFileTree, + findEntityByPath: findEntityByPathInFileTree, + previewByPath: previewByPathInFileTree, + dirname: dirnameInFileTree, + }), + [ + pathInFileTree, + findEntityByPathInFileTree, + previewByPathInFileTree, + dirnameInFileTree, + ] + ) + + return ( + + {children} + + ) +} + +export function useFileTreePathContext(): FileTreePathContextValue { + const context = useContext(FileTreePathContext) + + if (!context) { + throw new Error( + 'useFileTreePathContext is only available inside FileTreePathProvider' + ) + } + + return context +} diff --git a/services/web/frontend/js/features/file-tree/util/mutate-in-tree.js b/services/web/frontend/js/features/file-tree/util/mutate-in-tree.js index c1541685a6..7e3318c4d4 100644 --- a/services/web/frontend/js/features/file-tree/util/mutate-in-tree.js +++ b/services/web/frontend/js/features/file-tree/util/mutate-in-tree.js @@ -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 diff --git a/services/web/frontend/js/features/file-tree/util/path.ts b/services/web/frontend/js/features/file-tree/util/path.ts new file mode 100644 index 0000000000..7e124c20e4 --- /dev/null +++ b/services/web/frontend/js/features/file-tree/util/path.ts @@ -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 +} diff --git a/services/web/frontend/js/features/history/context/hooks/use-restore-deleted-file.ts b/services/web/frontend/js/features/history/context/hooks/use-restore-deleted-file.ts index 983f82f671..72aba4f174 100644 --- a/services/web/frontend/js/features/history/context/hooks/use-restore-deleted-file.ts +++ b/services/web/frontend/js/features/history/context/hooks/use-restore-deleted-file.ts @@ -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('idle') + const [restoredFileMetadata, setRestoredFileMetadata] = + useState(null) - const restoreDeletedFile = async ( - selection: HistoryContextValue['selection'] - ) => { - const { selectedFile } = selection + const isLoading = state === 'restoring' || state === 'waitingForFileTree' - 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) - } - - setView('editor') - }) - .catch(handleError) - ) + 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) + ) { + sendMB('history-v2-restore-deleted') + + setState('restoring') + restoreFile(projectId, selectedFile).then( + (data: RestoreFileResponse) => { + setRestoredFileMetadata(data) + setState('waitingForFileTree') + }, + error => { + setState('error') + handleError(error) + } + ) + } + }, + [handleError, projectId] + ) return { restoreDeletedFile, isLoading } } diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-synctex-controls.jsx b/services/web/frontend/js/features/pdf-preview/components/pdf-synctex-controls.jsx index 145adf12eb..bbde33dd10 100644 --- a/services/web/frontend/js/features/pdf-preview/components/pdf-synctex-controls.jsx +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-synctex-controls.jsx @@ -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( diff --git a/services/web/frontend/js/features/pdf-preview/util/output-files.js b/services/web/frontend/js/features/pdf-preview/util/output-files.js index fa25ce1aab..13dd049c81 100644 --- a/services/web/frontend/js/features/pdf-preview/util/output-files.js +++ b/services/web/frontend/js/features/pdf-preview/util/output-files.js @@ -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, diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts b/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts index 98dd6dcd24..abd7ffaa03 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts +++ b/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts @@ -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 ), diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/visual-widgets/graphics.ts b/services/web/frontend/js/features/source-editor/extensions/visual/visual-widgets/graphics.ts index bf639fbf59..7321f9e02b 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/visual-widgets/graphics.ts +++ b/services/web/frontend/js/features/source-editor/extensions/visual/visual-widgets/graphics.ts @@ -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() diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts b/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts index 9ef5a8f451..c397af13b8 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts +++ b/services/web/frontend/js/features/source-editor/extensions/visual/visual.ts @@ -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() diff --git a/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts b/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts index 6b585b4a1b..7618206900 100644 --- a/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts +++ b/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts @@ -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 diff --git a/services/web/frontend/js/ide/binary-files/BinaryFilesManager.js b/services/web/frontend/js/ide/binary-files/BinaryFilesManager.js index 41d619ed7e..0c0afe627b 100644 --- a/services/web/frontend/js/ide/binary-files/BinaryFilesManager.js +++ b/services/web/frontend/js/ide/binary-files/BinaryFilesManager.js @@ -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( () => { diff --git a/services/web/frontend/js/shared/context/local-compile-context.jsx b/services/web/frontend/js/shared/context/local-compile-context.jsx index 058396bfde..b01a317d2c 100644 --- a/services/web/frontend/js/shared/context/local-compile-context.jsx +++ b/services/web/frontend/js/shared/context/local-compile-context.jsx @@ -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 diff --git a/services/web/frontend/js/shared/context/root-context.jsx b/services/web/frontend/js/shared/context/root-context.jsx index aac1dccc0d..cb71c1e6b7 100644 --- a/services/web/frontend/js/shared/context/root-context.jsx +++ b/services/web/frontend/js/shared/context/root-context.jsx @@ -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,19 +22,21 @@ export function ContextRoot({ children, ide }) { - - - - - - - {children} - - - - - - + + + + + + + + {children} + + + + + + + diff --git a/services/web/frontend/stories/decorators/scope.tsx b/services/web/frontend/stories/decorators/scope.tsx index 7b19f9d2d1..2dd5773f01 100644 --- a/services/web/frontend/stories/decorators/scope.tsx +++ b/services/web/frontend/stories/decorators/scope.tsx @@ -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 } 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 ( - - - + + + + + + + + + + + + + + + + + + + + + + + + + ) } diff --git a/services/web/frontend/stories/source-editor/source-editor.stories.tsx b/services/web/frontend/stories/source-editor/source-editor.stories.tsx index 942d089d8e..07346a5a5e 100644 --- a/services/web/frontend/stories/source-editor/source-editor.stories.tsx +++ b/services/web/frontend/stories/source-editor/source-editor.stories.tsx @@ -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 }) => ( + 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} + +) export default { title: 'Editor / Source Editor', component: SourceEditor, decorators: [ - ScopeDecorator, + (Story: any) => + ScopeDecorator(Story, { + mockCompileOnLoad: true, + providers: { FileTreePathProvider }, + }), (Story: any) => (
@@ -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 diff --git a/services/web/test/frontend/components/pdf-preview/pdf-logs-entries.spec.tsx b/services/web/test/frontend/components/pdf-preview/pdf-logs-entries.spec.tsx index b4945bc661..364b18cf0b 100644 --- a/services/web/test/frontend/components/pdf-preview/pdf-logs-entries.spec.tsx +++ b/services/web/test/frontend/components/pdf-preview/pdf-logs-entries.spec.tsx @@ -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('', function () { - const fakeEntity = { type: 'doc' } + const fakeFindEntityResult: FindResult = { + type: 'doc', + entity: { _id: '123', name: '123 Doc' }, + } + + const FileTreePathProvider: FC = ({ children }) => ( + + {children} + + ) const logEntries = [ { @@ -23,11 +45,8 @@ describe('', 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('', function () { it('opens doc on click', function () { cy.mount( - + ) @@ -57,10 +76,14 @@ describe('', function () { }).click() cy.get('@findEntityByPath').should('have.been.calledOnce') - cy.get('@openDoc').should('have.been.calledOnceWith', fakeEntity, { - gotoLine: 9, - gotoColumn: 8, - }) + 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('', function () { }) cy.mount( - + ).then(() => { @@ -89,10 +112,14 @@ describe('', function () { }) cy.get('@findEntityByPath').should('have.been.calledOnce') - cy.get('@openDoc').should('have.been.calledOnceWith', fakeEntity, { - gotoLine: 7, - gotoColumn: 6, - }) + 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('', function () { }) cy.mount( - + ) @@ -113,7 +140,7 @@ describe('', 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', diff --git a/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-readonly.spec.tsx b/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-readonly.spec.tsx index 2509374e42..331cc898d6 100644 --- a/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-readonly.spec.tsx +++ b/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual-readonly.spec.tsx @@ -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 }) => (
{children}
) -const mountEditor = (content: string, ...args: any[]) => { +const mountEditor = ( + content: string, + props?: Omit, 'children' | 'scope'> +) => { const scope = mockScope(content) scope.permissionsLevel = 'readOnly' scope.editor.showVisual = true cy.mount( - + @@ -81,15 +85,21 @@ describe(' in Visual mode with read-only permission', functio }) it('does not display the figure edit button', function () { - const fileTreeManager = { - findEntityById: cy.stub(), - findEntityByPath: cy.stub(), - getEntityPath: cy.stub(), - getRootDocDirname: cy.stub(), - getPreviewByPath: cy - .stub() - .returns({ url: '/images/frog.jpg', extension: 'jpg' }), - } + const FileTreePathProvider: FC = ({ children }) => ( + + {children} + + ) cy.intercept('/images/frog.jpg', { fixture: 'images/gradient.png' }) @@ -100,7 +110,9 @@ describe(' 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) diff --git a/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual.spec.tsx b/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual.spec.tsx index b3aedf5fbf..e732c7f6ef 100644 --- a/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual.spec.tsx +++ b/services/web/test/frontend/features/source-editor/components/codemirror-editor-visual.spec.tsx @@ -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 }) => (
{children}
@@ -23,9 +24,25 @@ describe(' in Visual mode', function () { const scope = mockScope(content) scope.editor.showVisual = true + const FileTreePathProvider: FC = ({ children }) => ( + ({ url: path, extension: 'png' })), + }} + > + {children} + + ) + cy.mount( - + diff --git a/services/web/test/frontend/features/source-editor/components/figure-modal.spec.tsx b/services/web/test/frontend/features/source-editor/components/figure-modal.spec.tsx index 3e3f1f4eb0..9603329279 100644 --- a/services/web/test/frontend/features/source-editor/components/figure-modal.spec.tsx +++ b/services/web/test/frontend/features/source-editor/components/figure-modal.spec.tsx @@ -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 }) => (
{children}
@@ -50,9 +51,25 @@ describe('', function () { const scope = mockScope(content) scope.editor.showVisual = true + const FileTreePathProvider: FC = ({ children }) => ( + + {children} + + ) + cy.mount( - + diff --git a/services/web/test/frontend/helpers/editor-providers.jsx b/services/web/test/frontend/helpers/editor-providers.jsx index cfa019cf1f..2bda201b7c 100644 --- a/services/web/test/frontend/helpers/editor-providers.jsx +++ b/services/web/test/frontend/helpers/editor-providers.jsx @@ -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,33 +71,36 @@ 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 = { - user: window.user, - project: { - _id: window.project_id, - name: PROJECT_NAME, - owner: projectOwner, - features, - rootDoc_id: rootDocId, - rootFolder, + const $scope = merge( + { + user: window.user, + project: { + _id: window.project_id, + name: PROJECT_NAME, + owner: projectOwner, + features, + rootDoc_id: rootDocId, + rootFolder, + }, + ui, + $watch: (path, callback) => { + callback(get($scope, path)) + return () => null + }, + $on: sinon.stub(), + $applyAsync: sinon.stub(), + toggleHistory: sinon.stub(), + permissionsLevel, }, - ui, - $watch: (path, callback) => { - callback(get($scope, path)) - return () => null - }, - $on: sinon.stub(), - $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 ( - - - - - - - - - - - - {children} - - - - - - - - - - - + + + + + + + + + + + + + {children} + + + + + + + + + + + + ) } diff --git a/services/web/types/file-tree-entity.ts b/services/web/types/file-tree-entity.ts new file mode 100644 index 0000000000..d2db6c48cf --- /dev/null +++ b/services/web/types/file-tree-entity.ts @@ -0,0 +1,5 @@ +import { Folder } from './folder' +import { Doc } from './doc' +import { FileRef } from './file-ref' + +export type FileTreeEntity = Folder | Doc | FileRef diff --git a/services/web/types/preview-path.ts b/services/web/types/preview-path.ts new file mode 100644 index 0000000000..c0eb9aa9e4 --- /dev/null +++ b/services/web/types/preview-path.ts @@ -0,0 +1,4 @@ +export type PreviewPath = { + url: string + extension: string +}