mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #20057 from overleaf/jpa-readonly-tuning
[web] tuning for readonly mirror on client GitOrigin-RevId: c0ecc8923cdec6c515da17750133632ebc430e8d
This commit is contained in:
parent
4997b7f1ee
commit
1182049d89
14 changed files with 83 additions and 39 deletions
|
@ -42,6 +42,7 @@ const TrackedChangeList = require('./lib/file_data/tracked_change_list')
|
|||
const TrackingProps = require('./lib/file_data/tracking_props')
|
||||
const Range = require('./lib/range')
|
||||
const CommentList = require('./lib/file_data/comment_list')
|
||||
const LazyStringFileData = require('./lib/file_data/lazy_string_file_data')
|
||||
|
||||
exports.AddCommentOperation = AddCommentOperation
|
||||
exports.Author = Author
|
||||
|
@ -56,6 +57,7 @@ exports.Comment = Comment
|
|||
exports.DeleteCommentOperation = DeleteCommentOperation
|
||||
exports.File = File
|
||||
exports.FileMap = FileMap
|
||||
exports.LazyStringFileData = LazyStringFileData
|
||||
exports.History = History
|
||||
exports.Label = Label
|
||||
exports.AddFileOperation = AddFileOperation
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import React, { useEffect, useRef } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { Dropdown } from 'react-bootstrap'
|
||||
import { useEditorContext } from '../../../shared/context/editor-context'
|
||||
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
|
||||
import { useFileTreeMainContext } from '../contexts/file-tree-main'
|
||||
|
||||
import FileTreeItemMenuItems from './file-tree-item/file-tree-item-menu-items'
|
||||
|
||||
function FileTreeContextMenu() {
|
||||
const { permissionsLevel } = useEditorContext()
|
||||
const { fileTreeReadOnly } = useFileTreeData()
|
||||
const { contextMenuCoords, setContextMenuCoords } = useFileTreeMainContext()
|
||||
const toggleButtonRef = useRef<HTMLButtonElement | null>(null)
|
||||
|
||||
|
@ -20,7 +20,7 @@ function FileTreeContextMenu() {
|
|||
}
|
||||
}, [contextMenuCoords])
|
||||
|
||||
if (!contextMenuCoords || permissionsLevel === 'readOnly') return null
|
||||
if (!contextMenuCoords || fileTreeReadOnly) return null
|
||||
|
||||
// A11y - Move the focus to the context menu when it opens
|
||||
function focusContextMenu() {
|
||||
|
|
|
@ -2,7 +2,7 @@ import { ReactNode, useEffect, useRef } from 'react'
|
|||
import classNames from 'classnames'
|
||||
import scrollIntoViewIfNeeded from 'scroll-into-view-if-needed'
|
||||
|
||||
import { useEditorContext } from '../../../../shared/context/editor-context'
|
||||
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
|
||||
import { useFileTreeMainContext } from '../../contexts/file-tree-main'
|
||||
import { useDraggable } from '../../contexts/file-tree-draggable'
|
||||
|
||||
|
@ -25,16 +25,14 @@ function FileTreeItemInner({
|
|||
isSelected: boolean
|
||||
icons?: ReactNode
|
||||
}) {
|
||||
const { permissionsLevel } = useEditorContext()
|
||||
const { fileTreeReadOnly } = useFileTreeData()
|
||||
const { setContextMenuCoords } = useFileTreeMainContext()
|
||||
const { isRenaming } = useFileTreeActionable()
|
||||
|
||||
const { selectedEntityIds } = useFileTreeSelectable()
|
||||
|
||||
const hasMenu =
|
||||
permissionsLevel !== 'readOnly' &&
|
||||
isSelected &&
|
||||
selectedEntityIds.size === 1
|
||||
!fileTreeReadOnly && isSelected && selectedEntityIds.size === 1
|
||||
|
||||
const { dragRef, setIsDraggable } = useDraggable(id)
|
||||
|
||||
|
|
|
@ -47,7 +47,11 @@ function FileTreeItemMenuItems() {
|
|||
<MenuItem onClick={startRenaming}>{t('rename')}</MenuItem>
|
||||
) : null}
|
||||
{downloadPath ? (
|
||||
<MenuItem href={downloadPath} onClick={downloadWithAnalytics} download>
|
||||
<MenuItem
|
||||
href={downloadPath}
|
||||
onClick={downloadWithAnalytics}
|
||||
download={selectedFileName}
|
||||
>
|
||||
{t('download')}
|
||||
</MenuItem>
|
||||
) : null}
|
||||
|
|
|
@ -5,13 +5,13 @@ import { Button } from 'react-bootstrap'
|
|||
import Tooltip from '../../../shared/components/tooltip'
|
||||
import Icon from '../../../shared/components/icon'
|
||||
|
||||
import { useEditorContext } from '../../../shared/context/editor-context'
|
||||
import { useFileTreeActionable } from '../contexts/file-tree-actionable'
|
||||
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
|
||||
|
||||
function FileTreeToolbar() {
|
||||
const { permissionsLevel } = useEditorContext()
|
||||
const { fileTreeReadOnly } = useFileTreeData()
|
||||
|
||||
if (permissionsLevel === 'readOnly') return null
|
||||
if (fileTreeReadOnly) return null
|
||||
|
||||
return (
|
||||
<div className="toolbar toolbar-filetree">
|
||||
|
|
|
@ -22,7 +22,6 @@ import { isNameUniqueInFolder } from '../util/is-name-unique-in-folder'
|
|||
import { isBlockedFilename, isCleanFilename } from '../util/safe-path'
|
||||
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import { useEditorContext } from '../../../shared/context/editor-context'
|
||||
import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
|
||||
import { useFileTreeSelectable } from './file-tree-selectable'
|
||||
|
||||
|
@ -34,6 +33,7 @@ import {
|
|||
} from '../errors'
|
||||
import { Folder } from '../../../../../types/folder'
|
||||
import { useReferencesContext } from '@/features/ide-react/context/references-context'
|
||||
import { useSnapshotContext } from '@/features/ide-react/context/snapshot-context'
|
||||
|
||||
type DroppedFile = File & {
|
||||
relativePath?: string
|
||||
|
@ -219,11 +219,12 @@ function fileTreeActionableReducer(state: State, action: Action) {
|
|||
|
||||
export const FileTreeActionableProvider: FC = ({ children }) => {
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { permissionsLevel } = useEditorContext()
|
||||
const { fileTreeReadOnly } = useFileTreeData()
|
||||
const { indexAllReferences } = useReferencesContext()
|
||||
const { fileTreeFromHistory } = useSnapshotContext()
|
||||
|
||||
const [state, dispatch] = useReducer(
|
||||
permissionsLevel === 'readOnly'
|
||||
fileTreeReadOnly
|
||||
? fileTreeActionableReadOnlyReducer
|
||||
: fileTreeActionableReducer,
|
||||
defaultState
|
||||
|
@ -493,6 +494,9 @@ export const FileTreeActionableProvider: FC = ({ children }) => {
|
|||
const selectedEntity = findInTree(fileTreeData, selectedEntityId)
|
||||
|
||||
if (selectedEntity?.type === 'fileRef') {
|
||||
if (fileTreeFromHistory) {
|
||||
return `/project/${projectId}/blob/${selectedEntity.entity.hash}`
|
||||
}
|
||||
return `/project/${projectId}/file/${selectedEntityId}`
|
||||
}
|
||||
|
||||
|
@ -500,7 +504,7 @@ export const FileTreeActionableProvider: FC = ({ children }) => {
|
|||
return `/project/${projectId}/doc/${selectedEntityId}/download`
|
||||
}
|
||||
}
|
||||
}, [fileTreeData, projectId, selectedEntityIds])
|
||||
}, [fileTreeData, projectId, selectedEntityIds, fileTreeFromHistory])
|
||||
|
||||
// TODO: wrap in useMemo
|
||||
const value = {
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
import { useFileTreeActionable } from './file-tree-actionable'
|
||||
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
|
||||
import { useFileTreeSelectable } from '../contexts/file-tree-selectable'
|
||||
import { useEditorContext } from '@/shared/context/editor-context'
|
||||
import { isAcceptableFile } from '@/features/file-tree/util/is-acceptable-file'
|
||||
|
||||
const DRAGGABLE_TYPE = 'ENTITY'
|
||||
|
@ -48,8 +47,7 @@ type DropResult = {
|
|||
export function useDraggable(draggedEntityId: string) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { permissionsLevel } = useEditorContext()
|
||||
const { fileTreeData } = useFileTreeData()
|
||||
const { fileTreeData, fileTreeReadOnly } = useFileTreeData()
|
||||
const { selectedEntityIds, isRootFolderSelected } = useFileTreeSelectable()
|
||||
const { finishMoving } = useFileTreeActionable()
|
||||
|
||||
|
@ -73,7 +71,7 @@ export function useDraggable(draggedEntityId: string) {
|
|||
}
|
||||
},
|
||||
canDrag() {
|
||||
return permissionsLevel !== 'readOnly' && isDraggable
|
||||
return !fileTreeReadOnly && isDraggable
|
||||
},
|
||||
end(item: DragObject, monitor: DragSourceMonitor<DragObject, DropResult>) {
|
||||
if (monitor.didDrop()) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
pathInFolder,
|
||||
} from '@/features/file-tree/util/path'
|
||||
import { PreviewPath } from '../../../../../types/preview-path'
|
||||
import { useSnapshotContext } from '@/features/ide-react/context/snapshot-context'
|
||||
|
||||
type FileTreePathContextValue = {
|
||||
pathInFolder: (id: string) => string | null
|
||||
|
@ -24,6 +25,7 @@ export const FileTreePathContext = createContext<
|
|||
|
||||
export const FileTreePathProvider: FC = ({ children }) => {
|
||||
const { fileTreeData }: { fileTreeData: Folder } = useFileTreeData()
|
||||
const { fileTreeFromHistory } = useSnapshotContext()
|
||||
const projectId = getMeta('ol-project_id')
|
||||
|
||||
const pathInFileTree = useCallback(
|
||||
|
@ -37,8 +39,9 @@ export const FileTreePathProvider: FC = ({ children }) => {
|
|||
)
|
||||
|
||||
const previewByPathInFileTree = useCallback(
|
||||
(path: string) => previewByPath(fileTreeData, projectId, path),
|
||||
[fileTreeData, projectId]
|
||||
(path: string) =>
|
||||
previewByPath(fileTreeData, projectId, path, fileTreeFromHistory),
|
||||
[fileTreeData, projectId, fileTreeFromHistory]
|
||||
)
|
||||
|
||||
const dirnameInFileTree = useCallback(
|
||||
|
|
|
@ -13,7 +13,6 @@ import _ from 'lodash'
|
|||
import { findInTree } from '../util/find-in-tree'
|
||||
import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
import { useEditorContext } from '../../../shared/context/editor-context'
|
||||
import { useLayoutContext } from '../../../shared/context/layout-context'
|
||||
import usePersistedState from '../../../shared/hooks/use-persisted-state'
|
||||
import usePreviousValue from '../../../shared/hooks/use-previous-value'
|
||||
|
@ -123,19 +122,19 @@ export const FileTreeSelectableProvider: FC<{
|
|||
onSelect: (value: FindResult[]) => void
|
||||
}> = ({ onSelect, children }) => {
|
||||
const { _id: projectId, rootDocId } = useProjectContext()
|
||||
const { permissionsLevel } = useEditorContext()
|
||||
|
||||
const [initialSelectedEntityId] = usePersistedState(
|
||||
`doc.open_id.${projectId}`,
|
||||
rootDocId
|
||||
)
|
||||
|
||||
const { fileTreeData, setSelectedEntities } = useFileTreeData()
|
||||
const { fileTreeData, setSelectedEntities, fileTreeReadOnly } =
|
||||
useFileTreeData()
|
||||
|
||||
const [isRootFolderSelected, setIsRootFolderSelected] = useState(false)
|
||||
|
||||
const [selectedEntityIds, dispatch] = useReducer(
|
||||
permissionsLevel === 'readOnly'
|
||||
fileTreeReadOnly
|
||||
? fileTreeSelectableReadOnlyReducer
|
||||
: fileTreeSelectableReadWriteReducer,
|
||||
null,
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
|
|||
import { useFileTreeSelectable } from '../contexts/file-tree-selectable'
|
||||
import { findInTree, findInTreeOrThrow } from '../util/find-in-tree'
|
||||
import { useIdeContext } from '@/shared/context/ide-context'
|
||||
import { useSnapshotContext } from '@/features/ide-react/context/snapshot-context'
|
||||
|
||||
export function useFileTreeSocketListener(onDelete: (entity: any) => void) {
|
||||
const user = useUserContext()
|
||||
|
@ -20,6 +21,7 @@ export function useFileTreeSocketListener(onDelete: (entity: any) => void) {
|
|||
const { selectedEntityIds, selectedEntityParentIds, select, unselect } =
|
||||
useFileTreeSelectable()
|
||||
const { socket } = useIdeContext()
|
||||
const { fileTreeFromHistory } = useSnapshotContext()
|
||||
|
||||
const selectEntityIfCreatedByUser = useCallback(
|
||||
// hack to automatically re-open refreshed linked files
|
||||
|
@ -38,6 +40,7 @@ export function useFileTreeSocketListener(onDelete: (entity: any) => void) {
|
|||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (fileTreeFromHistory) return
|
||||
function handleDispatchRename(entityId: string, name: string) {
|
||||
dispatchRename(entityId, name)
|
||||
}
|
||||
|
@ -46,9 +49,10 @@ export function useFileTreeSocketListener(onDelete: (entity: any) => void) {
|
|||
if (socket)
|
||||
socket.removeListener('reciveEntityRename', handleDispatchRename)
|
||||
}
|
||||
}, [socket, dispatchRename])
|
||||
}, [socket, dispatchRename, fileTreeFromHistory])
|
||||
|
||||
useEffect(() => {
|
||||
if (fileTreeFromHistory) return
|
||||
function handleDispatchDelete(entityId: string) {
|
||||
const entity = findInTree(fileTreeData, entityId)
|
||||
unselect(entityId)
|
||||
|
@ -82,9 +86,11 @@ export function useFileTreeSocketListener(onDelete: (entity: any) => void) {
|
|||
selectedEntityIds,
|
||||
selectedEntityParentIds,
|
||||
onDelete,
|
||||
fileTreeFromHistory,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (fileTreeFromHistory) return
|
||||
function handleDispatchMove(entityId: string, toFolderId: string) {
|
||||
dispatchMove(entityId, toFolderId)
|
||||
}
|
||||
|
@ -92,9 +98,10 @@ export function useFileTreeSocketListener(onDelete: (entity: any) => void) {
|
|||
return () => {
|
||||
if (socket) socket.removeListener('reciveEntityMove', handleDispatchMove)
|
||||
}
|
||||
}, [socket, dispatchMove])
|
||||
}, [socket, dispatchMove, fileTreeFromHistory])
|
||||
|
||||
useEffect(() => {
|
||||
if (fileTreeFromHistory) return
|
||||
function handleDispatchCreateFolder(parentFolderId: string, folder: any) {
|
||||
dispatchCreateFolder(parentFolderId, folder)
|
||||
}
|
||||
|
@ -103,9 +110,10 @@ export function useFileTreeSocketListener(onDelete: (entity: any) => void) {
|
|||
if (socket)
|
||||
socket.removeListener('reciveNewFolder', handleDispatchCreateFolder)
|
||||
}
|
||||
}, [socket, dispatchCreateFolder])
|
||||
}, [socket, dispatchCreateFolder, fileTreeFromHistory])
|
||||
|
||||
useEffect(() => {
|
||||
if (fileTreeFromHistory) return
|
||||
function handleDispatchCreateDoc(
|
||||
parentFolderId: string,
|
||||
doc: any,
|
||||
|
@ -117,9 +125,10 @@ export function useFileTreeSocketListener(onDelete: (entity: any) => void) {
|
|||
return () => {
|
||||
if (socket) socket.removeListener('reciveNewDoc', handleDispatchCreateDoc)
|
||||
}
|
||||
}, [socket, dispatchCreateDoc])
|
||||
}, [socket, dispatchCreateDoc, fileTreeFromHistory])
|
||||
|
||||
useEffect(() => {
|
||||
if (fileTreeFromHistory) return
|
||||
function handleDispatchCreateFile(
|
||||
parentFolderId: string,
|
||||
file: any,
|
||||
|
@ -137,5 +146,10 @@ export function useFileTreeSocketListener(onDelete: (entity: any) => void) {
|
|||
if (socket)
|
||||
socket.removeListener('reciveNewFile', handleDispatchCreateFile)
|
||||
}
|
||||
}, [socket, dispatchCreateFile, selectEntityIfCreatedByUser])
|
||||
}, [
|
||||
socket,
|
||||
dispatchCreateFile,
|
||||
selectEntityIfCreatedByUser,
|
||||
fileTreeFromHistory,
|
||||
])
|
||||
}
|
||||
|
|
|
@ -106,7 +106,8 @@ export function findEntityByPath(
|
|||
export function previewByPath(
|
||||
folder: Folder,
|
||||
projectId: string,
|
||||
path: string
|
||||
path: string,
|
||||
fileTreeFromHistory: boolean
|
||||
): PreviewPath | null {
|
||||
for (const suffix of [
|
||||
'',
|
||||
|
@ -121,10 +122,12 @@ export function previewByPath(
|
|||
]) {
|
||||
const result = findEntityByPath(folder, path + suffix)
|
||||
|
||||
if (result) {
|
||||
const { name, _id: id } = result.entity
|
||||
if (result?.type === 'fileRef') {
|
||||
const { name, _id: id, hash } = result.entity
|
||||
return {
|
||||
url: `/project/${projectId}/file/${id}`,
|
||||
url: fileTreeFromHistory
|
||||
? `/project/${projectId}/blob/${hash}`
|
||||
: `/project/${projectId}/file/${id}`,
|
||||
extension: name.slice(name.lastIndexOf('.') + 1),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Trans, useTranslation } from 'react-i18next'
|
|||
|
||||
import Icon from '../../../shared/components/icon'
|
||||
import { formatTime, relativeDate } from '../../utils/format-date'
|
||||
import { useEditorContext } from '../../../shared/context/editor-context'
|
||||
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
|
||||
import { useProjectContext } from '../../../shared/context/project-context'
|
||||
|
||||
import { Nullable } from '../../../../../types/utils'
|
||||
|
@ -49,7 +49,7 @@ type FileViewHeaderProps = {
|
|||
|
||||
export default function FileViewHeader({ file }: FileViewHeaderProps) {
|
||||
const { _id: projectId } = useProjectContext()
|
||||
const { permissionsLevel } = useEditorContext()
|
||||
const { fileTreeReadOnly } = useFileTreeData()
|
||||
const { fileTreeFromHistory } = useSnapshotContext()
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
@ -85,7 +85,7 @@ export default function FileViewHeader({ file }: FileViewHeaderProps) {
|
|||
tprFileViewInfo.map(({ import: { TPRFileViewInfo }, path }) => (
|
||||
<TPRFileViewInfo key={path} file={file} />
|
||||
))}
|
||||
{file.linkedFileData && permissionsLevel !== 'readOnly' && (
|
||||
{file.linkedFileData && !fileTreeReadOnly && (
|
||||
<FileViewRefreshButton file={file} setRefreshError={setRefreshError} />
|
||||
)}
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ const FileTreeDataContext = createContext<
|
|||
// by the file tree
|
||||
fileTreeData: Folder
|
||||
fileCount: { value: number; status: string; limit: number } | number
|
||||
fileTreeReadOnly: boolean
|
||||
hasFolders: boolean
|
||||
selectedEntities: FindResult[]
|
||||
setSelectedEntities: (selectedEntities: FindResult[]) => void
|
||||
|
@ -179,8 +180,11 @@ export const FileTreeDataProvider: FC = ({ children }) => {
|
|||
const [project] = useScopeValue<Project>('project')
|
||||
const [openDocId] = useScopeValue('editor.open_doc_id')
|
||||
const [, setOpenDocName] = useScopeValueSetterOnly('editor.open_doc_name')
|
||||
const [permissionsLevel] = useScopeValue('permissionsLevel')
|
||||
const { fileTreeFromHistory, snapshot, snapshotVersion } =
|
||||
useSnapshotContext()
|
||||
const fileTreeReadOnly =
|
||||
permissionsLevel === 'readOnly' || fileTreeFromHistory
|
||||
|
||||
const [rootFolder, setRootFolder] = useState(project?.rootFolder)
|
||||
|
||||
|
@ -288,6 +292,7 @@ export const FileTreeDataProvider: FC = ({ children }) => {
|
|||
dispatchRename,
|
||||
fileCount,
|
||||
fileTreeData,
|
||||
fileTreeReadOnly,
|
||||
hasFolders: fileTreeData?.folders.length > 0,
|
||||
selectedEntities,
|
||||
setSelectedEntities,
|
||||
|
@ -302,6 +307,7 @@ export const FileTreeDataProvider: FC = ({ children }) => {
|
|||
dispatchRename,
|
||||
fileCount,
|
||||
fileTreeData,
|
||||
fileTreeReadOnly,
|
||||
selectedEntities,
|
||||
setSelectedEntities,
|
||||
docs,
|
||||
|
|
|
@ -151,12 +151,25 @@ describe('Path utils', function () {
|
|||
const preview = previewByPath(
|
||||
rootFolder,
|
||||
'test-project-id',
|
||||
'test-folder/example.png'
|
||||
'test-folder/example.png',
|
||||
false
|
||||
)
|
||||
expect(preview).to.deep.equal({
|
||||
url: '/project/test-project-id/file/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',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue