mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #12962 from overleaf/td-history-loading-state-fix
History migration: Split updates and file diff loading states, move file restore load state out of context GitOrigin-RevId: a994a1bdc8168043f045aaa3b7708bfce994d505
This commit is contained in:
parent
333b54c237
commit
5078f8ab43
7 changed files with 53 additions and 63 deletions
|
@ -6,19 +6,16 @@ import { OwnerPaywallPrompt } from './owner-paywall-prompt'
|
||||||
import { NonOwnerPaywallPrompt } from './non-owner-paywall-prompt'
|
import { NonOwnerPaywallPrompt } from './non-owner-paywall-prompt'
|
||||||
|
|
||||||
function AllHistoryList() {
|
function AllHistoryList() {
|
||||||
const {
|
const { updatesInfo, fetchNextBatchOfUpdates, currentUserIsOwner } =
|
||||||
updatesInfo,
|
useHistoryContext()
|
||||||
loadingState,
|
const updatesLoadingState = updatesInfo.loadingState
|
||||||
fetchNextBatchOfUpdates,
|
|
||||||
currentUserIsOwner,
|
|
||||||
} = useHistoryContext()
|
|
||||||
const { visibleUpdateCount, updates, atEnd } = updatesInfo
|
const { visibleUpdateCount, updates, atEnd } = updatesInfo
|
||||||
const scrollerRef = useRef<HTMLDivElement>(null)
|
const scrollerRef = useRef<HTMLDivElement>(null)
|
||||||
const bottomRef = useRef<HTMLDivElement>(null)
|
const bottomRef = useRef<HTMLDivElement>(null)
|
||||||
const intersectionObserverRef = useRef<IntersectionObserver | null>(null)
|
const intersectionObserverRef = useRef<IntersectionObserver | null>(null)
|
||||||
const [bottomVisible, setBottomVisible] = useState(false)
|
const [bottomVisible, setBottomVisible] = useState(false)
|
||||||
const showPaywall =
|
const showPaywall =
|
||||||
loadingState === 'ready' && updatesInfo.freeHistoryLimitHit
|
updatesLoadingState === 'ready' && updatesInfo.freeHistoryLimitHit
|
||||||
const showOwnerPaywall = showPaywall && currentUserIsOwner
|
const showOwnerPaywall = showPaywall && currentUserIsOwner
|
||||||
const showNonOwnerPaywall = showPaywall && !currentUserIsOwner
|
const showNonOwnerPaywall = showPaywall && !currentUserIsOwner
|
||||||
const visibleUpdates =
|
const visibleUpdates =
|
||||||
|
@ -27,7 +24,7 @@ function AllHistoryList() {
|
||||||
// Create an intersection observer that watches for any part of an element
|
// Create an intersection observer that watches for any part of an element
|
||||||
// positioned at the bottom of the list to be visible
|
// positioned at the bottom of the list to be visible
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loadingState === 'ready' && !intersectionObserverRef.current) {
|
if (updatesLoadingState === 'ready' && !intersectionObserverRef.current) {
|
||||||
const scroller = scrollerRef.current
|
const scroller = scrollerRef.current
|
||||||
const bottom = bottomRef.current
|
const bottom = bottomRef.current
|
||||||
|
|
||||||
|
@ -48,13 +45,13 @@ function AllHistoryList() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [loadingState])
|
}, [updatesLoadingState])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!atEnd && loadingState === 'ready' && bottomVisible) {
|
if (!atEnd && updatesLoadingState === 'ready' && bottomVisible) {
|
||||||
fetchNextBatchOfUpdates()
|
fetchNextBatchOfUpdates()
|
||||||
}
|
}
|
||||||
}, [atEnd, bottomVisible, fetchNextBatchOfUpdates, loadingState])
|
}, [atEnd, bottomVisible, fetchNextBatchOfUpdates, updatesLoadingState])
|
||||||
|
|
||||||
// While updates are loading, remove the intersection observer and set
|
// While updates are loading, remove the intersection observer and set
|
||||||
// bottomVisible to false. This is to avoid loading more updates immediately
|
// bottomVisible to false. This is to avoid loading more updates immediately
|
||||||
|
@ -62,14 +59,14 @@ function AllHistoryList() {
|
||||||
// the intersection observer is asynchronous and won't have noticed that the
|
// the intersection observer is asynchronous and won't have noticed that the
|
||||||
// bottom is no longer visible
|
// bottom is no longer visible
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loadingState !== 'ready' && intersectionObserverRef.current) {
|
if (updatesLoadingState !== 'ready' && intersectionObserverRef.current) {
|
||||||
setBottomVisible(false)
|
setBottomVisible(false)
|
||||||
if (intersectionObserverRef.current) {
|
if (intersectionObserverRef.current) {
|
||||||
intersectionObserverRef.current.disconnect()
|
intersectionObserverRef.current.disconnect()
|
||||||
intersectionObserverRef.current = null
|
intersectionObserverRef.current = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [loadingState])
|
}, [updatesLoadingState])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={scrollerRef} className="history-all-versions-scroller">
|
<div ref={scrollerRef} className="history-all-versions-scroller">
|
||||||
|
@ -92,8 +89,8 @@ function AllHistoryList() {
|
||||||
</div>
|
</div>
|
||||||
{showOwnerPaywall ? <OwnerPaywallPrompt /> : null}
|
{showOwnerPaywall ? <OwnerPaywallPrompt /> : null}
|
||||||
{showNonOwnerPaywall ? <NonOwnerPaywallPrompt /> : null}
|
{showNonOwnerPaywall ? <NonOwnerPaywallPrompt /> : null}
|
||||||
{loadingState === 'loadingInitial' ||
|
{updatesLoadingState === 'loadingInitial' ||
|
||||||
loadingState === 'loadingUpdates' ? (
|
updatesLoadingState === 'loadingUpdates' ? (
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,8 +9,7 @@ import useAsync from '../../../../shared/hooks/use-async'
|
||||||
import ErrorMessage from '../error-message'
|
import ErrorMessage from '../error-message'
|
||||||
|
|
||||||
function DiffView() {
|
function DiffView() {
|
||||||
const { selection, projectId, loadingState } = useHistoryContext()
|
const { selection, projectId, loadingFileDiffs } = useHistoryContext()
|
||||||
const loadingFileDiffs = loadingState === 'loadingFileDiffs'
|
|
||||||
const { isLoading, data, runAsync, error } = useAsync<DocDiffResponse>()
|
const { isLoading, data, runAsync, error } = useAsync<DocDiffResponse>()
|
||||||
const { updateRange, selectedFile } = selection
|
const { updateRange, selectedFile } = selection
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Button } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useHistoryContext } from '../../../context/history-context'
|
|
||||||
import { useRestoreDeletedFile } from '../../../context/hooks/use-restore-deleted-file'
|
import { useRestoreDeletedFile } from '../../../context/hooks/use-restore-deleted-file'
|
||||||
import type { HistoryContextValue } from '../../../context/types/history-context-value'
|
import type { HistoryContextValue } from '../../../context/types/history-context-value'
|
||||||
|
|
||||||
|
@ -12,21 +11,18 @@ export default function ToolbarRestoreFileButton({
|
||||||
selection,
|
selection,
|
||||||
}: ToolbarRestoreFileButtonProps) {
|
}: ToolbarRestoreFileButtonProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { loadingState } = useHistoryContext()
|
|
||||||
|
|
||||||
const onRestoreFile = useRestoreDeletedFile()
|
const { restoreDeletedFile, isLoading } = useRestoreDeletedFile()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className="btn-secondary history-react-toolbar-restore-file-button"
|
className="btn-secondary history-react-toolbar-restore-file-button"
|
||||||
bsSize="xs"
|
bsSize="xs"
|
||||||
bsStyle={null}
|
bsStyle={null}
|
||||||
onClick={() => onRestoreFile(selection)}
|
onClick={() => restoreDeletedFile(selection)}
|
||||||
disabled={loadingState === 'restoringFile'}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
{loadingState === 'restoringFile'
|
{isLoading ? `${t('restoring')}…` : t('restore_file')}
|
||||||
? `${t('restoring')}…`
|
|
||||||
: t('restore_file')}
|
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,10 @@ import ErrorMessage from './error-message'
|
||||||
const fileTreeContainer = document.getElementById('history-file-tree')
|
const fileTreeContainer = document.getElementById('history-file-tree')
|
||||||
|
|
||||||
function Main() {
|
function Main() {
|
||||||
const { loadingState, error } = useHistoryContext()
|
const { updatesInfo, error } = useHistoryContext()
|
||||||
|
|
||||||
let content = null
|
let content = null
|
||||||
if (loadingState === 'loadingInitial') {
|
if (updatesInfo.loadingState === 'loadingInitial') {
|
||||||
content = <LoadingSpinner />
|
content = <LoadingSpinner />
|
||||||
} else if (error) {
|
} else if (error) {
|
||||||
content = <ErrorMessage />
|
content = <ErrorMessage />
|
||||||
|
|
|
@ -85,17 +85,19 @@ function useHistory() {
|
||||||
atEnd: false,
|
atEnd: false,
|
||||||
freeHistoryLimitHit: false,
|
freeHistoryLimitHit: false,
|
||||||
nextBeforeTimestamp: undefined,
|
nextBeforeTimestamp: undefined,
|
||||||
|
loadingState: 'loadingInitial',
|
||||||
})
|
})
|
||||||
const [labels, setLabels] = useState<HistoryContextValue['labels']>(null)
|
const [labels, setLabels] = useState<HistoryContextValue['labels']>(null)
|
||||||
const [labelsOnly, setLabelsOnly] = usePersistedState(
|
const [labelsOnly, setLabelsOnly] = usePersistedState(
|
||||||
`history.userPrefs.showOnlyLabels.${projectId}`,
|
`history.userPrefs.showOnlyLabels.${projectId}`,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
const [loadingState, setLoadingState] =
|
const [loadingFileDiffs, setLoadingFileDiffs] = useState(false)
|
||||||
useState<HistoryContextValue['loadingState']>('loadingInitial')
|
|
||||||
const [error, setError] = useState<HistoryContextValue['error']>(null)
|
const [error, setError] = useState<HistoryContextValue['error']>(null)
|
||||||
|
|
||||||
const fetchNextBatchOfUpdates = useCallback(() => {
|
const fetchNextBatchOfUpdates = useCallback(() => {
|
||||||
|
const updatesLoadingState = updatesInfo.loadingState
|
||||||
|
|
||||||
const loadUpdates = (updatesData: Update[]) => {
|
const loadUpdates = (updatesData: Update[]) => {
|
||||||
const dateTimeNow = new Date()
|
const dateTimeNow = new Date()
|
||||||
const timestamp24hoursAgo = dateTimeNow.setDate(dateTimeNow.getDate() - 1)
|
const timestamp24hoursAgo = dateTimeNow.setDate(dateTimeNow.getDate() - 1)
|
||||||
|
@ -140,7 +142,10 @@ function useHistory() {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
updatesInfo.atEnd ||
|
updatesInfo.atEnd ||
|
||||||
!(loadingState === 'loadingInitial' || loadingState === 'ready')
|
!(
|
||||||
|
updatesLoadingState === 'loadingInitial' ||
|
||||||
|
updatesLoadingState === 'ready'
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -150,9 +155,11 @@ function useHistory() {
|
||||||
)
|
)
|
||||||
const labelsPromise = labels == null ? fetchLabels(projectId) : null
|
const labelsPromise = labels == null ? fetchLabels(projectId) : null
|
||||||
|
|
||||||
setLoadingState(
|
setUpdatesInfo({
|
||||||
loadingState === 'ready' ? 'loadingUpdates' : 'loadingInitial'
|
...updatesInfo,
|
||||||
)
|
loadingState:
|
||||||
|
updatesLoadingState === 'ready' ? 'loadingUpdates' : 'loadingInitial',
|
||||||
|
})
|
||||||
Promise.all([updatesPromise, labelsPromise])
|
Promise.all([updatesPromise, labelsPromise])
|
||||||
.then(([{ updates: updatesData, nextBeforeTimestamp }, labels]) => {
|
.then(([{ updates: updatesData, nextBeforeTimestamp }, labels]) => {
|
||||||
const lastUpdateToV = updatesData.length ? updatesData[0].toV : null
|
const lastUpdateToV = updatesData.length ? updatesData[0].toV : null
|
||||||
|
@ -173,16 +180,14 @@ function useHistory() {
|
||||||
freeHistoryLimitHit,
|
freeHistoryLimitHit,
|
||||||
atEnd,
|
atEnd,
|
||||||
nextBeforeTimestamp,
|
nextBeforeTimestamp,
|
||||||
|
loadingState: 'ready',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
setError(error)
|
setError(error)
|
||||||
setUpdatesInfo({ ...updatesInfo, atEnd: true })
|
setUpdatesInfo({ ...updatesInfo, atEnd: true, loadingState: 'ready' })
|
||||||
})
|
})
|
||||||
.finally(() => {
|
}, [updatesInfo, projectId, labels, userHasFullFeature])
|
||||||
setLoadingState('ready')
|
|
||||||
})
|
|
||||||
}, [loadingState, labels, projectId, userHasFullFeature, updatesInfo])
|
|
||||||
|
|
||||||
const resetSelection = useCallback(() => {
|
const resetSelection = useCallback(() => {
|
||||||
setSelection(selectionInitialState)
|
setSelection(selectionInitialState)
|
||||||
|
@ -203,12 +208,12 @@ function useHistory() {
|
||||||
|
|
||||||
// Load files when the update selection changes
|
// Load files when the update selection changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!updateRange || loadingState !== 'ready' || !filesEmpty) {
|
if (!updateRange || updatesInfo.loadingState !== 'ready' || !filesEmpty) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const { fromV, toV } = updateRange
|
const { fromV, toV } = updateRange
|
||||||
|
|
||||||
setLoadingState('loadingFileDiffs')
|
setLoadingFileDiffs(true)
|
||||||
|
|
||||||
diffFiles(projectId, fromV, toV)
|
diffFiles(projectId, fromV, toV)
|
||||||
.then(({ diff: files }) => {
|
.then(({ diff: files }) => {
|
||||||
|
@ -236,7 +241,7 @@ function useHistory() {
|
||||||
setError(error)
|
setError(error)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoadingState('ready')
|
setLoadingFileDiffs(false)
|
||||||
})
|
})
|
||||||
}, [
|
}, [
|
||||||
updateRange,
|
updateRange,
|
||||||
|
@ -244,7 +249,7 @@ function useHistory() {
|
||||||
updates,
|
updates,
|
||||||
comparing,
|
comparing,
|
||||||
setError,
|
setError,
|
||||||
loadingState,
|
updatesInfo.loadingState,
|
||||||
filesEmpty,
|
filesEmpty,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@ -268,8 +273,8 @@ function useHistory() {
|
||||||
() => ({
|
() => ({
|
||||||
error,
|
error,
|
||||||
setError,
|
setError,
|
||||||
loadingState,
|
loadingFileDiffs,
|
||||||
setLoadingState,
|
setLoadingFileDiffs,
|
||||||
updatesInfo,
|
updatesInfo,
|
||||||
setUpdatesInfo,
|
setUpdatesInfo,
|
||||||
labels,
|
labels,
|
||||||
|
@ -287,8 +292,8 @@ function useHistory() {
|
||||||
[
|
[
|
||||||
error,
|
error,
|
||||||
setError,
|
setError,
|
||||||
loadingState,
|
loadingFileDiffs,
|
||||||
setLoadingState,
|
setLoadingFileDiffs,
|
||||||
updatesInfo,
|
updatesInfo,
|
||||||
setUpdatesInfo,
|
setUpdatesInfo,
|
||||||
labels,
|
labels,
|
||||||
|
|
|
@ -9,17 +9,18 @@ import { useHistoryContext } from '../history-context'
|
||||||
import type { HistoryContextValue } from '../types/history-context-value'
|
import type { HistoryContextValue } from '../types/history-context-value'
|
||||||
|
|
||||||
export function useRestoreDeletedFile() {
|
export function useRestoreDeletedFile() {
|
||||||
const { runAsync } = useAsync()
|
const { isLoading, runAsync } = useAsync()
|
||||||
const { setLoadingState, projectId, setError } = useHistoryContext()
|
const { projectId, setError } = useHistoryContext()
|
||||||
const ide = useIdeContext()
|
const ide = useIdeContext()
|
||||||
const { setView } = useLayoutContext()
|
const { setView } = useLayoutContext()
|
||||||
|
|
||||||
return async (selection: HistoryContextValue['selection']) => {
|
const restoreDeletedFile = async (
|
||||||
|
selection: HistoryContextValue['selection']
|
||||||
|
) => {
|
||||||
const { selectedFile } = selection
|
const { selectedFile } = selection
|
||||||
|
|
||||||
if (selectedFile && selectedFile.pathname && isFileRemoved(selectedFile)) {
|
if (selectedFile && selectedFile.pathname && isFileRemoved(selectedFile)) {
|
||||||
sendMB('history-v2-restore-deleted')
|
sendMB('history-v2-restore-deleted')
|
||||||
setLoadingState('restoringFile')
|
|
||||||
|
|
||||||
await runAsync(
|
await runAsync(
|
||||||
restoreFile(projectId, selectedFile)
|
restoreFile(projectId, selectedFile)
|
||||||
|
@ -40,10 +41,9 @@ export function useRestoreDeletedFile() {
|
||||||
setView('editor')
|
setView('editor')
|
||||||
})
|
})
|
||||||
.catch(error => setError(error))
|
.catch(error => setError(error))
|
||||||
.finally(() => {
|
|
||||||
setLoadingState('ready')
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { restoreDeletedFile, isLoading }
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,7 @@ import { LoadedUpdate } from '../../services/types/update'
|
||||||
import { LoadedLabel } from '../../services/types/label'
|
import { LoadedLabel } from '../../services/types/label'
|
||||||
import { Selection } from '../../services/types/selection'
|
import { Selection } from '../../services/types/selection'
|
||||||
|
|
||||||
type LoadingState =
|
type UpdatesLoadingState = 'loadingInitial' | 'loadingUpdates' | 'ready'
|
||||||
| 'loadingInitial'
|
|
||||||
| 'loadingUpdates'
|
|
||||||
| 'loadingFileDiffs'
|
|
||||||
| 'restoringFile'
|
|
||||||
| 'ready'
|
|
||||||
|
|
||||||
export type HistoryContextValue = {
|
export type HistoryContextValue = {
|
||||||
updatesInfo: {
|
updatesInfo: {
|
||||||
|
@ -17,16 +12,14 @@ export type HistoryContextValue = {
|
||||||
atEnd: boolean
|
atEnd: boolean
|
||||||
nextBeforeTimestamp: number | undefined
|
nextBeforeTimestamp: number | undefined
|
||||||
freeHistoryLimitHit: boolean
|
freeHistoryLimitHit: boolean
|
||||||
|
loadingState: UpdatesLoadingState
|
||||||
}
|
}
|
||||||
setUpdatesInfo: React.Dispatch<
|
setUpdatesInfo: React.Dispatch<
|
||||||
React.SetStateAction<HistoryContextValue['updatesInfo']>
|
React.SetStateAction<HistoryContextValue['updatesInfo']>
|
||||||
>
|
>
|
||||||
userHasFullFeature: boolean
|
userHasFullFeature: boolean
|
||||||
currentUserIsOwner: boolean
|
currentUserIsOwner: boolean
|
||||||
loadingState: LoadingState
|
loadingFileDiffs: boolean
|
||||||
setLoadingState: React.Dispatch<
|
|
||||||
React.SetStateAction<HistoryContextValue['loadingState']>
|
|
||||||
>
|
|
||||||
error: Nullable<unknown>
|
error: Nullable<unknown>
|
||||||
setError: React.Dispatch<React.SetStateAction<HistoryContextValue['error']>>
|
setError: React.Dispatch<React.SetStateAction<HistoryContextValue['error']>>
|
||||||
labels: Nullable<LoadedLabel[]>
|
labels: Nullable<LoadedLabel[]>
|
||||||
|
|
Loading…
Reference in a new issue