Merge pull request #22178 from overleaf/jpa-file-view-hash-2
[web] migrate file-view to download from history-v1 (via web) 2/2 GitOrigin-RevId: 93172cbbd2d19a55bb27f5b0ca0b494f815a3632
This commit is contained in:
@@ -33,7 +33,6 @@ import {
|
|||||||
} from '../errors'
|
} from '../errors'
|
||||||
import { Folder } from '../../../../../types/folder'
|
import { Folder } from '../../../../../types/folder'
|
||||||
import { useReferencesContext } from '@/features/ide-react/context/references-context'
|
import { useReferencesContext } from '@/features/ide-react/context/references-context'
|
||||||
import { useSnapshotContext } from '@/features/ide-react/context/snapshot-context'
|
|
||||||
|
|
||||||
type DroppedFile = File & {
|
type DroppedFile = File & {
|
||||||
relativePath?: string
|
relativePath?: string
|
||||||
@@ -221,7 +220,6 @@ export const FileTreeActionableProvider: FC = ({ children }) => {
|
|||||||
const { _id: projectId } = useProjectContext()
|
const { _id: projectId } = useProjectContext()
|
||||||
const { fileTreeReadOnly } = useFileTreeData()
|
const { fileTreeReadOnly } = useFileTreeData()
|
||||||
const { indexAllReferences } = useReferencesContext()
|
const { indexAllReferences } = useReferencesContext()
|
||||||
const { fileTreeFromHistory } = useSnapshotContext()
|
|
||||||
|
|
||||||
const [state, dispatch] = useReducer(
|
const [state, dispatch] = useReducer(
|
||||||
fileTreeReadOnly
|
fileTreeReadOnly
|
||||||
@@ -494,17 +492,14 @@ export const FileTreeActionableProvider: FC = ({ children }) => {
|
|||||||
const selectedEntity = findInTree(fileTreeData, selectedEntityId)
|
const selectedEntity = findInTree(fileTreeData, selectedEntityId)
|
||||||
|
|
||||||
if (selectedEntity?.type === 'fileRef') {
|
if (selectedEntity?.type === 'fileRef') {
|
||||||
if (fileTreeFromHistory) {
|
return `/project/${projectId}/blob/${selectedEntity.entity.hash}?fallback=${selectedEntityId}`
|
||||||
return `/project/${projectId}/blob/${selectedEntity.entity.hash}`
|
|
||||||
}
|
|
||||||
return `/project/${projectId}/file/${selectedEntityId}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedEntity?.type === 'doc') {
|
if (selectedEntity?.type === 'doc') {
|
||||||
return `/project/${projectId}/doc/${selectedEntityId}/download`
|
return `/project/${projectId}/doc/${selectedEntityId}/download`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [fileTreeData, projectId, selectedEntityIds, fileTreeFromHistory])
|
}, [fileTreeData, projectId, selectedEntityIds])
|
||||||
|
|
||||||
// TODO: wrap in useMemo
|
// TODO: wrap in useMemo
|
||||||
const value = {
|
const value = {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
pathInFolder,
|
pathInFolder,
|
||||||
} from '@/features/file-tree/util/path'
|
} from '@/features/file-tree/util/path'
|
||||||
import { PreviewPath } from '../../../../../types/preview-path'
|
import { PreviewPath } from '../../../../../types/preview-path'
|
||||||
import { useSnapshotContext } from '@/features/ide-react/context/snapshot-context'
|
|
||||||
|
|
||||||
type FileTreePathContextValue = {
|
type FileTreePathContextValue = {
|
||||||
pathInFolder: (id: string) => string | null
|
pathInFolder: (id: string) => string | null
|
||||||
@@ -25,7 +24,6 @@ export const FileTreePathContext = createContext<
|
|||||||
|
|
||||||
export const FileTreePathProvider: FC = ({ children }) => {
|
export const FileTreePathProvider: FC = ({ children }) => {
|
||||||
const { fileTreeData }: { fileTreeData: Folder } = useFileTreeData()
|
const { fileTreeData }: { fileTreeData: Folder } = useFileTreeData()
|
||||||
const { fileTreeFromHistory } = useSnapshotContext()
|
|
||||||
const projectId = getMeta('ol-project_id')
|
const projectId = getMeta('ol-project_id')
|
||||||
|
|
||||||
const pathInFileTree = useCallback(
|
const pathInFileTree = useCallback(
|
||||||
@@ -39,9 +37,8 @@ export const FileTreePathProvider: FC = ({ children }) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const previewByPathInFileTree = useCallback(
|
const previewByPathInFileTree = useCallback(
|
||||||
(path: string) =>
|
(path: string) => previewByPath(fileTreeData, projectId, path),
|
||||||
previewByPath(fileTreeData, projectId, path, fileTreeFromHistory),
|
[fileTreeData, projectId]
|
||||||
[fileTreeData, projectId, fileTreeFromHistory]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const dirnameInFileTree = useCallback(
|
const dirnameInFileTree = useCallback(
|
||||||
|
|||||||
@@ -106,8 +106,7 @@ export function findEntityByPath(
|
|||||||
export function previewByPath(
|
export function previewByPath(
|
||||||
folder: Folder,
|
folder: Folder,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
path: string,
|
path: string
|
||||||
fileTreeFromHistory: boolean
|
|
||||||
): PreviewPath | null {
|
): PreviewPath | null {
|
||||||
for (const suffix of [
|
for (const suffix of [
|
||||||
'',
|
'',
|
||||||
@@ -125,9 +124,7 @@ export function previewByPath(
|
|||||||
if (result?.type === 'fileRef') {
|
if (result?.type === 'fileRef') {
|
||||||
const { name, _id: id, hash } = result.entity
|
const { name, _id: id, hash } = result.entity
|
||||||
return {
|
return {
|
||||||
url: fileTreeFromHistory
|
url: `/project/${projectId}/blob/${hash}?fallback=${id}`,
|
||||||
? `/project/${projectId}/blob/${hash}`
|
|
||||||
: `/project/${projectId}/file/${id}`,
|
|
||||||
extension: name.slice(name.lastIndexOf('.') + 1),
|
extension: name.slice(name.lastIndexOf('.') + 1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { LinkedFileIcon } from './file-view-icons'
|
|||||||
import { BinaryFile, hasProvider, LinkedFile } from '../types/binary-file'
|
import { BinaryFile, hasProvider, LinkedFile } from '../types/binary-file'
|
||||||
import FileViewRefreshButton from './file-view-refresh-button'
|
import FileViewRefreshButton from './file-view-refresh-button'
|
||||||
import FileViewRefreshError from './file-view-refresh-error'
|
import FileViewRefreshError from './file-view-refresh-error'
|
||||||
import { useSnapshotContext } from '@/features/ide-react/context/snapshot-context'
|
|
||||||
import MaterialIcon from '@/shared/components/material-icon'
|
import MaterialIcon from '@/shared/components/material-icon'
|
||||||
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
|
||||||
import OLButton from '@/features/ui/components/ol/ol-button'
|
import OLButton from '@/features/ui/components/ol/ol-button'
|
||||||
@@ -53,7 +52,6 @@ type FileViewHeaderProps = {
|
|||||||
export default function FileViewHeader({ file }: FileViewHeaderProps) {
|
export default function FileViewHeader({ file }: FileViewHeaderProps) {
|
||||||
const { _id: projectId } = useProjectContext()
|
const { _id: projectId } = useProjectContext()
|
||||||
const { fileTreeReadOnly } = useFileTreeData()
|
const { fileTreeReadOnly } = useFileTreeData()
|
||||||
const { fileTreeFromHistory } = useSnapshotContext()
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const [refreshError, setRefreshError] = useState<Nullable<string>>(null)
|
const [refreshError, setRefreshError] = useState<Nullable<string>>(null)
|
||||||
@@ -86,11 +84,7 @@ export default function FileViewHeader({ file }: FileViewHeaderProps) {
|
|||||||
<OLButton
|
<OLButton
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
download={file.name}
|
download={file.name}
|
||||||
href={
|
href={`/project/${projectId}/blob/${file.hash}?fallback=${file.id}`}
|
||||||
fileTreeFromHistory
|
|
||||||
? `/project/${projectId}/blob/${file.hash}`
|
|
||||||
: `/project/${projectId}/file/${file.id}`
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<BootstrapVersionSwitcher
|
<BootstrapVersionSwitcher
|
||||||
bs3={<Icon type="download" fw />}
|
bs3={<Icon type="download" fw />}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useProjectContext } from '../../../shared/context/project-context'
|
import { useProjectContext } from '../../../shared/context/project-context'
|
||||||
import { useSnapshotContext } from '@/features/ide-react/context/snapshot-context'
|
|
||||||
import { BinaryFile } from '@/features/file-view/types/binary-file'
|
import { BinaryFile } from '@/features/file-view/types/binary-file'
|
||||||
|
|
||||||
export default function FileViewImage({
|
export default function FileViewImage({
|
||||||
@@ -12,15 +11,10 @@ export default function FileViewImage({
|
|||||||
onError: () => void
|
onError: () => void
|
||||||
}) {
|
}) {
|
||||||
const { _id: projectId } = useProjectContext()
|
const { _id: projectId } = useProjectContext()
|
||||||
const { fileTreeFromHistory } = useSnapshotContext()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
src={
|
src={`/project/${projectId}/blob/${file.hash}?fallback=${file.id}`}
|
||||||
fileTreeFromHistory
|
|
||||||
? `/project/${projectId}/blob/${file.hash}`
|
|
||||||
: `/project/${projectId}/file/${file.id}`
|
|
||||||
}
|
|
||||||
onLoad={onLoad}
|
onLoad={onLoad}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
alt={file.name}
|
alt={file.name}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useProjectContext } from '../../../shared/context/project-context'
|
|||||||
import { debugConsole } from '@/utils/debugging'
|
import { debugConsole } from '@/utils/debugging'
|
||||||
import useAbortController from '../../../shared/hooks/use-abort-controller'
|
import useAbortController from '../../../shared/hooks/use-abort-controller'
|
||||||
import { BinaryFile } from '@/features/file-view/types/binary-file'
|
import { BinaryFile } from '@/features/file-view/types/binary-file'
|
||||||
import { useSnapshotContext } from '@/features/ide-react/context/snapshot-context'
|
|
||||||
|
|
||||||
const MAX_FILE_SIZE = 2 * 1024 * 1024
|
const MAX_FILE_SIZE = 2 * 1024 * 1024
|
||||||
|
|
||||||
@@ -17,7 +16,6 @@ export default function FileViewText({
|
|||||||
onError: () => void
|
onError: () => void
|
||||||
}) {
|
}) {
|
||||||
const { _id: projectId } = useProjectContext()
|
const { _id: projectId } = useProjectContext()
|
||||||
const { fileTreeFromHistory } = useSnapshotContext()
|
|
||||||
|
|
||||||
const [textPreview, setTextPreview] = useState('')
|
const [textPreview, setTextPreview] = useState('')
|
||||||
const [shouldShowDots, setShouldShowDots] = useState(false)
|
const [shouldShowDots, setShouldShowDots] = useState(false)
|
||||||
@@ -30,9 +28,7 @@ export default function FileViewText({
|
|||||||
if (inFlight) {
|
if (inFlight) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let path = fileTreeFromHistory
|
const path = `/project/${projectId}/blob/${file.hash}?fallback=${file.id}`
|
||||||
? `/project/${projectId}/blob/${file.hash}`
|
|
||||||
: `/project/${projectId}/file/${file.id}`
|
|
||||||
const fetchContentLengthTimeout = setTimeout(
|
const fetchContentLengthTimeout = setTimeout(
|
||||||
() => fetchContentLengthController.abort(),
|
() => fetchContentLengthController.abort(),
|
||||||
10000
|
10000
|
||||||
@@ -45,32 +41,27 @@ export default function FileViewText({
|
|||||||
})
|
})
|
||||||
.then(fileSize => {
|
.then(fileSize => {
|
||||||
let truncated = false
|
let truncated = false
|
||||||
let maxSize = null
|
const headers = new Headers()
|
||||||
if (fileSize && Number(fileSize) > MAX_FILE_SIZE) {
|
if (fileSize && Number(fileSize) > MAX_FILE_SIZE) {
|
||||||
truncated = true
|
truncated = true
|
||||||
maxSize = MAX_FILE_SIZE
|
headers.set('Range', `bytes=0-${MAX_FILE_SIZE}`)
|
||||||
}
|
|
||||||
|
|
||||||
if (maxSize != null) {
|
|
||||||
path += `?range=0-${maxSize}`
|
|
||||||
}
|
}
|
||||||
fetchDataTimeout = window.setTimeout(
|
fetchDataTimeout = window.setTimeout(
|
||||||
() => fetchDataController.abort(),
|
() => fetchDataController.abort(),
|
||||||
60000
|
60000
|
||||||
)
|
)
|
||||||
return fetch(path, { signal: fetchDataController.signal }).then(
|
const signal = fetchDataController.signal
|
||||||
response => {
|
return fetch(path, { signal, headers }).then(response => {
|
||||||
return response.text().then(text => {
|
return response.text().then(text => {
|
||||||
if (truncated) {
|
if (truncated) {
|
||||||
text = text.replace(/\n.*$/, '')
|
text = text.replace(/\n.*$/, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
setTextPreview(text)
|
setTextPreview(text)
|
||||||
onLoad()
|
onLoad()
|
||||||
setShouldShowDots(truncated)
|
setShouldShowDots(truncated)
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
debugConsole.error('Error fetching file contents', err)
|
debugConsole.error('Error fetching file contents', err)
|
||||||
@@ -82,7 +73,6 @@ export default function FileViewText({
|
|||||||
clearTimeout(fetchDataTimeout)
|
clearTimeout(fetchDataTimeout)
|
||||||
})
|
})
|
||||||
}, [
|
}, [
|
||||||
fileTreeFromHistory,
|
|
||||||
projectId,
|
projectId,
|
||||||
file.id,
|
file.id,
|
||||||
file.hash,
|
file.hash,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ extra parameters or packages included.
|
|||||||
|
|
||||||
const setupFetchMock = fetchMock => {
|
const setupFetchMock = fetchMock => {
|
||||||
return fetchMock
|
return fetchMock
|
||||||
.head('express:/project/:project_id/file/:file_id', {
|
.head('express:/project/:project_id/blob/:hash', {
|
||||||
status: 201,
|
status: 201,
|
||||||
headers: { 'Content-Length': 10000 },
|
headers: { 'Content-Length': 10000 },
|
||||||
})
|
})
|
||||||
@@ -41,10 +41,9 @@ const fileData = {
|
|||||||
|
|
||||||
export const FileFromUrl = args => {
|
export const FileFromUrl = args => {
|
||||||
useFetchMock(fetchMock =>
|
useFetchMock(fetchMock =>
|
||||||
setupFetchMock(fetchMock).get(
|
setupFetchMock(fetchMock).get('express:/project/:project_id/blob/:hash', {
|
||||||
'express:/project/:project_id/file/:file_id',
|
body: bodies.latex,
|
||||||
{ body: bodies.latex }
|
})
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return <FileView {...args} />
|
return <FileView {...args} />
|
||||||
@@ -61,10 +60,9 @@ FileFromUrl.args = {
|
|||||||
|
|
||||||
export const FileFromProjectWithLinkableProjectId = args => {
|
export const FileFromProjectWithLinkableProjectId = args => {
|
||||||
useFetchMock(fetchMock =>
|
useFetchMock(fetchMock =>
|
||||||
setupFetchMock(fetchMock).get(
|
setupFetchMock(fetchMock).get('express:/project/:project_id/blob/:hash', {
|
||||||
'express:/project/:project_id/file/:file_id',
|
body: bodies.latex,
|
||||||
{ body: bodies.latex }
|
})
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return <FileView {...args} />
|
return <FileView {...args} />
|
||||||
@@ -82,10 +80,9 @@ FileFromProjectWithLinkableProjectId.args = {
|
|||||||
|
|
||||||
export const FileFromProjectWithoutLinkableProjectId = args => {
|
export const FileFromProjectWithoutLinkableProjectId = args => {
|
||||||
useFetchMock(fetchMock =>
|
useFetchMock(fetchMock =>
|
||||||
setupFetchMock(fetchMock).get(
|
setupFetchMock(fetchMock).get('express:/project/:project_id/blob/:hash', {
|
||||||
'express:/project/:project_id/file/:file_id',
|
body: bodies.latex,
|
||||||
{ body: bodies.latex }
|
})
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return <FileView {...args} />
|
return <FileView {...args} />
|
||||||
@@ -103,10 +100,9 @@ FileFromProjectWithoutLinkableProjectId.args = {
|
|||||||
|
|
||||||
export const FileFromProjectOutputWithLinkableProject = args => {
|
export const FileFromProjectOutputWithLinkableProject = args => {
|
||||||
useFetchMock(fetchMock =>
|
useFetchMock(fetchMock =>
|
||||||
setupFetchMock(fetchMock).get(
|
setupFetchMock(fetchMock).get('express:/project/:project_id/blob/:hash', {
|
||||||
'express:/project/:project_id/file/:file_id',
|
body: bodies.latex,
|
||||||
{ body: bodies.latex }
|
})
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return <FileView {...args} />
|
return <FileView {...args} />
|
||||||
@@ -124,10 +120,9 @@ FileFromProjectOutputWithLinkableProject.args = {
|
|||||||
|
|
||||||
export const FileFromProjectOutputWithoutLinkableProjectId = args => {
|
export const FileFromProjectOutputWithoutLinkableProjectId = args => {
|
||||||
useFetchMock(fetchMock =>
|
useFetchMock(fetchMock =>
|
||||||
setupFetchMock(fetchMock).get(
|
setupFetchMock(fetchMock).get('express:/project/:project_id/blob/:hash', {
|
||||||
'express:/project/:project_id/file/:file_id',
|
body: bodies.latex,
|
||||||
{ body: bodies.latex }
|
})
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return <FileView {...args} />
|
return <FileView {...args} />
|
||||||
@@ -165,7 +160,7 @@ ImageFile.args = {
|
|||||||
export const TextFile = args => {
|
export const TextFile = args => {
|
||||||
useFetchMock(fetchMock =>
|
useFetchMock(fetchMock =>
|
||||||
setupFetchMock(fetchMock).get(
|
setupFetchMock(fetchMock).get(
|
||||||
'express:/project/:project_id/file/:file_id',
|
'express:/project/:project_id/blob/:hash',
|
||||||
{ body: bodies.text },
|
{ body: bodies.text },
|
||||||
{ overwriteRoutes: true }
|
{ overwriteRoutes: true }
|
||||||
)
|
)
|
||||||
@@ -187,7 +182,7 @@ TextFile.args = {
|
|||||||
export const UploadedFile = args => {
|
export const UploadedFile = args => {
|
||||||
useFetchMock(fetchMock =>
|
useFetchMock(fetchMock =>
|
||||||
setupFetchMock(fetchMock).head(
|
setupFetchMock(fetchMock).head(
|
||||||
'express:/project/:project_id/file/:file_id',
|
'express:/project/:project_id/blob/:hash',
|
||||||
{ status: 500 },
|
{ status: 500 },
|
||||||
{ overwriteRoutes: true }
|
{ overwriteRoutes: true }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -151,23 +151,10 @@ describe('Path utils', function () {
|
|||||||
const preview = previewByPath(
|
const preview = previewByPath(
|
||||||
rootFolder,
|
rootFolder,
|
||||||
'test-project-id',
|
'test-project-id',
|
||||||
'test-folder/example.png',
|
'test-folder/example.png'
|
||||||
false
|
|
||||||
)
|
)
|
||||||
expect(preview).to.deep.equal({
|
expect(preview).to.deep.equal({
|
||||||
url: '/project/test-project-id/file/test-file-in-folder',
|
url: '/project/test-project-id/blob/42?fallback=test-file-in-folder',
|
||||||
extension: 'png',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
it('returns handles history file-tree', function () {
|
|
||||||
const preview = previewByPath(
|
|
||||||
rootFolder,
|
|
||||||
'test-project-id',
|
|
||||||
'test-folder/example.png',
|
|
||||||
true
|
|
||||||
)
|
|
||||||
expect(preview).to.deep.equal({
|
|
||||||
url: '/project/test-project-id/blob/42',
|
|
||||||
extension: 'png',
|
extension: 'png',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ describe('<FileViewText/>', function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('renders a text view', async function () {
|
it('renders a text view', async function () {
|
||||||
fetchMock.head('express:/project/:project_id/file/:file_id', {
|
fetchMock.head('express:/project/:project_id/blob/:hash', {
|
||||||
status: 201,
|
status: 201,
|
||||||
headers: { 'Content-Length': 10000 },
|
headers: { 'Content-Length': 10000 },
|
||||||
})
|
})
|
||||||
fetchMock.get(
|
fetchMock.get(
|
||||||
'express:/project/:project_id/file/:file_id',
|
'express:/project/:project_id/blob/:hash',
|
||||||
'Text file content'
|
'Text file content'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ describe('<FileView/>', function () {
|
|||||||
|
|
||||||
describe('for a text file', function () {
|
describe('for a text file', function () {
|
||||||
it('shows a loading indicator while the file is loading', async function () {
|
it('shows a loading indicator while the file is loading', async function () {
|
||||||
fetchMock.head('express:/project/:project_id/file/:file_id', {
|
fetchMock.head('express:/project/:project_id/blob/:hash', {
|
||||||
status: 201,
|
status: 201,
|
||||||
headers: { 'Content-Length': 10000 },
|
headers: { 'Content-Length': 10000 },
|
||||||
})
|
})
|
||||||
fetchMock.get(
|
fetchMock.get(
|
||||||
'express:/project/:project_id/file/:file_id',
|
'express:/project/:project_id/blob/:hash',
|
||||||
'Text file content'
|
'Text file content'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user