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
This commit is contained in:
Mathias Jakobsen 2023-05-19 09:29:48 +01:00 committed by Copybot
parent 5bbe427ed0
commit 480672bf7a
6 changed files with 128 additions and 3 deletions

View file

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

View file

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

View file

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

View file

@ -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 }),
})
}

View file

@ -4,6 +4,9 @@ import {
StateEffect, StateEffect,
StateField, StateField,
} from '@codemirror/state' } from '@codemirror/state'
import { EditorView } from '@codemirror/view'
import { addEffectListener, removeEffectListener } from './effect-listeners'
import { setMetadataEffect } from './language'
type NestedReadonly<T> = { type NestedReadonly<T> = {
readonly [P in keyof T]: NestedReadonly<T[P]> 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 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,
}
}

View file

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