Просмотр исходного кода

Merge pull request #13133 from overleaf/mj-wait-upload

[cm6] Wait for file to be ready before inserting code from figure modal

GitOrigin-RevId: 5c4e8f243518bacc7b6ef4272eaf44995192efbc
Mathias Jakobsen 3 лет назад
Родитель
Сommit
480672bf7a

+ 5 - 0
services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-other-project-source.tsx

@@ -20,6 +20,8 @@ import { postJSON } from '../../../../../infrastructure/fetch-json'
 import { useProjectContext } from '../../../../../shared/context/project-context'
 import { FileRelocator } from '../file-relocator'
 import { useTranslation } from 'react-i18next'
+import { waitForFileTreeUpdate } from '../../../extensions/figure-modal'
+import { useCodeMirrorViewContext } from '../../codemirror-editor'
 
 function suggestName(path: string) {
   const parts = path.split('/')
@@ -28,6 +30,7 @@ function suggestName(path: string) {
 
 export const FigureModalOtherProjectSource: FC = () => {
   const { t } = useTranslation()
+  const view = useCodeMirrorViewContext()
   const { dispatch } = useFigureModalContext()
   const { _id: projectId } = useProjectContext()
   const { loading: projectsLoading, data: projects, error } = useUserProjects()
@@ -108,9 +111,11 @@ export const FigureModalOtherProjectSource: FC = () => {
 
     dispatch({
       getPath: async () => {
+        const fileTreeUpdate = waitForFileTreeUpdate(view)
         await postJSON(`/project/${projectId}/linked_file`, {
           body,
         })
+        await fileTreeUpdate.withTimeout(500)
         return targetFolder.path === '' && targetFolder.name === 'rootFolder'
           ? `${newName}`
           : `${targetFolder.path ? targetFolder.path + '/' : ''}${

+ 6 - 1
services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-upload-source.tsx

@@ -14,6 +14,8 @@ import classNames from 'classnames'
 import { Button } from 'react-bootstrap'
 import { FileRelocator } from '../file-relocator'
 import { useTranslation } from 'react-i18next'
+import { useCodeMirrorViewContext } from '../../codemirror-editor'
+import { waitForFileTreeUpdate } from '../../../extensions/figure-modal'
 
 const maxFileSize = window.ExposedSettings.maxUploadSize
 
@@ -28,6 +30,7 @@ export enum FileUploadStatus {
 
 export const FigureModalUploadFileSource: FC = () => {
   const { t } = useTranslation()
+  const view = useCodeMirrorViewContext()
   const { dispatch } = useFigureModalContext()
   const { _id: projectId } = useProjectContext()
   const [, rootFolder] = useCurrentProjectFolders()
@@ -68,7 +71,9 @@ export const FigureModalUploadFileSource: FC = () => {
       }
       dispatch({
         getPath: async () => {
+          const fileTreeUpdate = waitForFileTreeUpdate(view)
           const uploadResult = await uppy.upload()
+          await fileTreeUpdate.withTimeout(500)
           if (!uploadResult.successful) {
             throw new Error('Upload failed')
           }
@@ -81,7 +86,7 @@ export const FigureModalUploadFileSource: FC = () => {
         },
       })
     },
-    [dispatch, rootFolder, uppy]
+    [dispatch, rootFolder, uppy, view]
   )
 
   useEffect(() => {

+ 11 - 2
services/web/frontend/js/features/source-editor/components/figure-modal/file-sources/figure-modal-url-source.tsx

@@ -6,14 +6,19 @@ import { File } from '../../../utils/file'
 import { useCurrentProjectFolders } from '../../../hooks/useCurrentProjectFolders'
 import { FileRelocator } from '../file-relocator'
 import { useTranslation } from 'react-i18next'
+import { useCodeMirrorViewContext } from '../../codemirror-editor'
+import { EditorView } from '@codemirror/view'
+import { waitForFileTreeUpdate } from '../../../extensions/figure-modal'
 
 function generateLinkedFileFetcher(
   projectId: string,
   url: string,
   name: string,
-  folder: File
+  folder: File,
+  view: EditorView
 ) {
   return async () => {
+    const fileTreeUpdate = waitForFileTreeUpdate(view)
     await postJSON(`/project/${projectId}/linked_file`, {
       body: {
         parent_folder_id: folder.id,
@@ -24,6 +29,8 @@ function generateLinkedFileFetcher(
         },
       },
     })
+    await fileTreeUpdate.withTimeout(500)
+
     return folder.path === '' && folder.name === 'rootFolder'
       ? `${name}`
       : `${folder.path ? folder.path + '/' : ''}${folder.name}/${name}`
@@ -31,6 +38,7 @@ function generateLinkedFileFetcher(
 }
 
 export const FigureModalUrlSource: FC = () => {
+  const view = useCodeMirrorViewContext()
   const { t } = useTranslation()
   const [url, setUrl] = useState<string>('')
   const [nameDirty, setNameDirty] = useState<boolean>(false)
@@ -53,7 +61,8 @@ export const FigureModalUrlSource: FC = () => {
           projectId,
           newUrl,
           newName,
-          folder ?? rootFile
+          folder ?? rootFile,
+          view
         ),
       })
     } else if (getPath) {

+ 73 - 0
services/web/frontend/js/features/source-editor/extensions/effect-listeners.ts

@@ -0,0 +1,73 @@
+import { StateEffect, StateEffectType, StateField } from '@codemirror/state'
+import { EditorView } from '@codemirror/view'
+
+type EffectListenerOptions = {
+  once: boolean
+}
+
+type EffectListener = {
+  effect: StateEffectType<any>
+  callback: (value: any) => any
+  options?: EffectListenerOptions
+}
+
+const addEffectListenerEffect = StateEffect.define<EffectListener>()
+const removeEffectListenerEffect = StateEffect.define<EffectListener>()
+
+export const effectListeners = () => [effectListenersField]
+
+const effectListenersField = StateField.define<EffectListener[]>({
+  create: () => [],
+  update(value, transaction) {
+    for (const effect of transaction.effects) {
+      if (effect.is(addEffectListenerEffect)) {
+        value.push(effect.value)
+      }
+      if (effect.is(removeEffectListenerEffect)) {
+        value = value.filter(
+          listener =>
+            !(
+              listener.effect === effect.value.effect &&
+              listener.callback === effect.value.callback
+            )
+        )
+      }
+      for (let i = 0; i < value.length; ++i) {
+        const listener = value[i]
+        if (effect.is(listener.effect)) {
+          // Invoke the callback after the transaction
+          setTimeout(() => listener.callback(effect.value))
+          if (listener.options?.once) {
+            // Remove the effectListener
+            value.splice(i, 1)
+            // Keep index the same for the next iteration, since we've removed
+            // an element
+            --i
+          }
+        }
+      }
+    }
+    return value
+  },
+})
+
+export const addEffectListener = <T>(
+  view: EditorView,
+  effect: StateEffectType<T>,
+  callback: (value: T) => any,
+  options?: EffectListenerOptions
+) => {
+  view.dispatch({
+    effects: addEffectListenerEffect.of({ effect, callback, options }),
+  })
+}
+
+export const removeEffectListener = <T>(
+  view: EditorView,
+  effect: StateEffectType<T>,
+  callback: (value: T) => any
+) => {
+  view.dispatch({
+    effects: removeEffectListenerEffect.of({ effect, callback }),
+  })
+}

+ 31 - 0
services/web/frontend/js/features/source-editor/extensions/figure-modal.ts

@@ -4,6 +4,9 @@ import {
   StateEffect,
   StateField,
 } from '@codemirror/state'
+import { EditorView } from '@codemirror/view'
+import { addEffectListener, removeEffectListener } from './effect-listeners'
+import { setMetadataEffect } from './language'
 
 type NestedReadonly<T> = {
   readonly [P in keyof T]: NestedReadonly<T[P]>
@@ -127,3 +130,31 @@ export const editFigureData = StateField.define<FigureData | null>({
 })
 
 export const figureModal = (): Extension => [editFigureData]
+
+export function waitForFileTreeUpdate(view: EditorView) {
+  const abortController = new AbortController()
+  const promise = new Promise<void>(resolve => {
+    const abort = () => {
+      console.warn('Aborting wait for file tree update')
+      removeEffectListener(view, setMetadataEffect, listener)
+      resolve()
+    }
+    function listener() {
+      if (abortController.signal.aborted) {
+        // We've already handled this
+        return
+      }
+      abortController.signal.removeEventListener('abort', abort)
+      resolve()
+    }
+    abortController.signal.addEventListener('abort', abort, { once: true })
+    addEffectListener(view, setMetadataEffect, listener, { once: true })
+  })
+  return {
+    withTimeout(afterMs = 500) {
+      setTimeout(() => abortController.abort(), afterMs)
+      return promise
+    },
+    promise,
+  }
+}

+ 2 - 0
services/web/frontend/js/features/source-editor/extensions/index.ts

@@ -44,6 +44,7 @@ import { indentationMarkers } from './indentation-markers'
 import { codemirrorDevTools } from '../languages/latex/codemirror-dev-tools'
 import { keymaps } from './keymaps'
 import { shortcuts } from './shortcuts'
+import { effectListeners } from './effect-listeners'
 
 const moduleExtensions: Array<() => Extension> = importOverleafModules(
   'sourceEditorExtensions'
@@ -117,4 +118,5 @@ export const createExtensions = (options: Record<string, any>): Extension[] => [
   exceptionLogger(),
   moduleExtensions.map(extension => extension()),
   thirdPartyExtensions(),
+  effectListeners(),
 ]