mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
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:
parent
5bbe427ed0
commit
480672bf7a
6 changed files with 128 additions and 3 deletions
|
@ -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 + '/' : ''}${
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 }),
|
||||||
|
})
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in a new issue