Merge pull request #25752 from overleaf/dp-delete-multiple-files

Add bulk delete button to file tree toolbar

GitOrigin-RevId: c857d8f5027eddb29b1ca324efe1a0e94ef4c28b
This commit is contained in:
David
2025-05-21 09:47:25 +01:00
committed by Copybot
parent c45bca6ce9
commit 061f10a059
10 changed files with 180 additions and 153 deletions

View File

@@ -8,6 +8,7 @@ export default /** @type {const} */ ([
'brush',
'code',
'create_new_folder',
'delete',
'description',
'experiment',
'forum',

View File

@@ -6,7 +6,7 @@ import FileTreeContext from './file-tree-context'
import FileTreeDraggablePreviewLayer from './file-tree-draggable-preview-layer'
import FileTreeFolderList from './file-tree-folder-list'
import FileTreeToolbar from './file-tree-toolbar'
import FileTreeToolbarNew from '@/features/ide-redesign/components/file-tree-toolbar'
import FileTreeToolbarNew from '@/features/ide-redesign/components/file-tree/file-tree-toolbar'
import FileTreeModalDelete from './modals/file-tree-modal-delete'
import FileTreeModalCreateFolder from './modals/file-tree-modal-create-folder'
import FileTreeModalError from './modals/file-tree-modal-error'

View File

@@ -57,6 +57,7 @@ const FileTreeActionableContext = createContext<
newFileCreateMode: any | null
error: any | null
canDelete: boolean
canBulkDelete: boolean
canRename: boolean
canCreate: boolean
parentFolderId: string
@@ -509,6 +510,8 @@ export const FileTreeActionableProvider: FC<React.PropsWithChildren> = ({
const value = useMemo(
() => ({
canDelete: write && selectedEntityIds.size > 0 && !isRootFolderSelected,
canBulkDelete:
write && selectedEntityIds.size > 1 && !isRootFolderSelected,
canRename: write && selectedEntityIds.size === 1 && !isRootFolderSelected,
canCreate: write && selectedEntityIds.size < 2,
...state,
@@ -545,8 +548,8 @@ export const FileTreeActionableProvider: FC<React.PropsWithChildren> = ({
isDuplicate,
isRootFolderSelected,
parentFolderId,
selectedEntityIds.size,
selectedFileName,
selectedEntityIds.size,
startCreatingDocOrFile,
startCreatingFile,
startCreatingFolder,

View File

@@ -1,149 +0,0 @@
import { useTranslation } from 'react-i18next'
import * as eventTracking from '../../../infrastructure/event-tracking'
import { useFileTreeActionable } from '@/features/file-tree/contexts/file-tree-actionable'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import MaterialIcon, {
AvailableUnfilledIcon,
} from '@/shared/components/material-icon'
import React from 'react'
import useCollapsibleFileTree from '../hooks/use-collapsible-file-tree'
import { useCommandProvider } from '@/features/ide-react/hooks/use-command-provider'
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
function FileTreeToolbar() {
const { t } = useTranslation()
const { fileTreeExpanded, toggleFileTreeExpanded } = useCollapsibleFileTree()
return (
<div className="file-tree-toolbar">
<button
className="file-tree-expand-collapse-button"
onClick={toggleFileTreeExpanded}
aria-label={
fileTreeExpanded ? t('hide_file_tree') : t('show_file_tree')
}
>
<MaterialIcon
type={
fileTreeExpanded ? 'keyboard_arrow_down' : 'keyboard_arrow_right'
}
/>
<h4>{t('file_tree')}</h4>
</button>
<FileTreeActionButtons />
</div>
)
}
function FileTreeActionButtons() {
const { t } = useTranslation()
const { fileTreeReadOnly } = useFileTreeData()
const { write } = usePermissionsContext()
const {
canCreate,
startCreatingFolder,
startCreatingDocOrFile,
startUploadingDocOrFile,
} = useFileTreeActionable()
useCommandProvider(() => {
if (!canCreate || fileTreeReadOnly || !write) return
return [
{
label: t('new_file'),
id: 'new_file',
handler: ({ location }) => {
eventTracking.sendMB('new-file-click', { location })
startCreatingDocOrFile()
},
},
{
label: t('new_folder'),
id: 'new_folder',
handler: startCreatingFolder,
},
{
label: t('upload_file'),
id: 'upload_file',
handler: ({ location }) => {
eventTracking.sendMB('upload-click', { location })
startUploadingDocOrFile()
},
},
]
}, [
canCreate,
fileTreeReadOnly,
startCreatingDocOrFile,
t,
startCreatingFolder,
startUploadingDocOrFile,
write,
])
if (!canCreate || fileTreeReadOnly) return null
const createWithAnalytics = () => {
eventTracking.sendMB('new-file-click', { location: 'toolbar' })
startCreatingDocOrFile()
}
const uploadWithAnalytics = () => {
eventTracking.sendMB('upload-click', { location: 'toolbar' })
startUploadingDocOrFile()
}
return (
<div className="file-tree-toolbar-action-buttons">
<FileTreeActionButton
id="new-file"
description={t('new_file')}
onClick={createWithAnalytics}
iconType="note_add"
/>
<FileTreeActionButton
id="new-folder"
description={t('new_folder')}
onClick={startCreatingFolder}
iconType="create_new_folder"
/>
<FileTreeActionButton
id="upload"
description={t('upload')}
onClick={uploadWithAnalytics}
iconType="upload_file"
/>
</div>
)
}
function FileTreeActionButton({
id,
description,
onClick,
iconType,
}: {
id: string
description: string
onClick: () => void
iconType: AvailableUnfilledIcon
}) {
return (
<OLTooltip
id={id}
description={description}
overlayProps={{ placement: 'bottom' }}
>
<button className="btn file-tree-toolbar-action-button" onClick={onClick}>
<MaterialIcon
unfilled
type={iconType}
accessibilityLabel={description}
/>
</button>
</OLTooltip>
)
}
export default FileTreeToolbar

View File

@@ -0,0 +1,33 @@
import OLTooltip from '@/features/ui/components/ol/ol-tooltip'
import MaterialIcon, {
AvailableUnfilledIcon,
} from '@/shared/components/material-icon'
import React from 'react'
export default function FileTreeActionButton({
id,
description,
onClick,
iconType,
}: {
id: string
description: string
onClick: () => void
iconType: AvailableUnfilledIcon
}) {
return (
<OLTooltip
id={id}
description={description}
overlayProps={{ placement: 'bottom' }}
>
<button className="btn file-tree-toolbar-action-button" onClick={onClick}>
<MaterialIcon
unfilled
type={iconType}
accessibilityLabel={description}
/>
</button>
</OLTooltip>
)
}

View File

@@ -0,0 +1,107 @@
import { useTranslation } from 'react-i18next'
import * as eventTracking from '../../../../infrastructure/event-tracking'
import { useFileTreeActionable } from '@/features/file-tree/contexts/file-tree-actionable'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import React from 'react'
import { useCommandProvider } from '@/features/ide-react/hooks/use-command-provider'
import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
import FileTreeActionButton from './file-tree-action-button'
export default function FileTreeActionButtons() {
const { t } = useTranslation()
const { fileTreeReadOnly } = useFileTreeData()
const { write } = usePermissionsContext()
const {
canCreate,
canBulkDelete,
startDeleting,
startCreatingFolder,
startCreatingDocOrFile,
startUploadingDocOrFile,
} = useFileTreeActionable()
useCommandProvider(() => {
if (!canCreate || fileTreeReadOnly || !write) return
return [
{
label: t('new_file'),
id: 'new_file',
handler: ({ location }) => {
eventTracking.sendMB('new-file-click', { location })
startCreatingDocOrFile()
},
},
{
label: t('new_folder'),
id: 'new_folder',
handler: startCreatingFolder,
},
{
label: t('upload_file'),
id: 'upload_file',
handler: ({ location }) => {
eventTracking.sendMB('upload-click', { location })
startUploadingDocOrFile()
},
},
]
}, [
canCreate,
fileTreeReadOnly,
startCreatingDocOrFile,
t,
startCreatingFolder,
startUploadingDocOrFile,
write,
])
if (fileTreeReadOnly) return null
const createWithAnalytics = () => {
eventTracking.sendMB('new-file-click', { location: 'toolbar' })
startCreatingDocOrFile()
}
const uploadWithAnalytics = () => {
eventTracking.sendMB('upload-click', { location: 'toolbar' })
startUploadingDocOrFile()
}
return (
<div className="file-tree-toolbar-action-buttons">
{canCreate && (
<FileTreeActionButton
id="new-file"
description={t('new_file')}
onClick={createWithAnalytics}
iconType="note_add"
/>
)}
{canCreate && (
<FileTreeActionButton
id="new-folder"
description={t('new_folder')}
onClick={startCreatingFolder}
iconType="create_new_folder"
/>
)}
{canCreate && (
<FileTreeActionButton
id="upload"
description={t('upload')}
onClick={uploadWithAnalytics}
iconType="upload_file"
/>
)}
{canBulkDelete && (
<FileTreeActionButton
id="delete"
description={t('delete')}
onClick={startDeleting}
iconType="delete"
/>
)}
</div>
)
}

View File

@@ -3,7 +3,7 @@ import { FileTree } from '@/features/ide-react/components/file-tree'
import { OutlineContainer } from '@/features/outline/components/outline-container'
import { VerticalResizeHandle } from '@/features/ide-react/components/resize/vertical-resize-handle'
import { useOutlinePane } from '@/features/ide-react/hooks/use-outline-pane'
import useCollapsibleFileTree from '../hooks/use-collapsible-file-tree'
import useCollapsibleFileTree from '../../hooks/use-collapsible-file-tree'
import classNames from 'classnames'
function FileTreeOutlinePanel() {

View File

@@ -0,0 +1,32 @@
import { useTranslation } from 'react-i18next'
import MaterialIcon from '@/shared/components/material-icon'
import React from 'react'
import useCollapsibleFileTree from '../../hooks/use-collapsible-file-tree'
import FileTreeActionButtons from './file-tree-action-buttons'
function FileTreeToolbar() {
const { t } = useTranslation()
const { fileTreeExpanded, toggleFileTreeExpanded } = useCollapsibleFileTree()
return (
<div className="file-tree-toolbar">
<button
className="file-tree-expand-collapse-button"
onClick={toggleFileTreeExpanded}
aria-label={
fileTreeExpanded ? t('hide_file_tree') : t('show_file_tree')
}
>
<MaterialIcon
type={
fileTreeExpanded ? 'keyboard_arrow_down' : 'keyboard_arrow_right'
}
/>
<h4>{t('file_tree')}</h4>
</button>
<FileTreeActionButtons />
</div>
)
}
export default FileTreeToolbar

View File

@@ -12,7 +12,7 @@ import {
RailTabKey,
useRailContext,
} from '../contexts/rail-context'
import FileTreeOutlinePanel from './file-tree-outline-panel'
import FileTreeOutlinePanel from './file-tree/file-tree-outline-panel'
import { ChatIndicator, ChatPane } from './chat/chat'
import getMeta from '@/utils/meta'
import { HorizontalResizeHandle } from '@/features/ide-react/components/resize/horizontal-resize-handle'