Merge pull request #20057 from overleaf/jpa-readonly-tuning

[web] tuning for readonly mirror on client

GitOrigin-RevId: c0ecc8923cdec6c515da17750133632ebc430e8d
This commit is contained in:
Jakob Ackermann 2024-08-22 17:00:07 +02:00 committed by Copybot
parent 4997b7f1ee
commit 1182049d89
14 changed files with 83 additions and 39 deletions

View file

@ -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

View file

@ -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() {

View file

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

View file

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

View file

@ -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">

View file

@ -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 = {

View file

@ -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()) {

View file

@ -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(

View file

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

View file

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

View file

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

View file

@ -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} />
)}
&nbsp;

View file

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

View file

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