feat: add batch delete functionality for files page (#9636)

* feat: add batch delete functionality for files page

- Add batch selection with checkboxes for individual files
- Implement batch delete operation with confirmation dialog
- Add select all/none functionality with indeterminate state
- Include safety check to prevent deleting files used in paintings
- Support multiple languages for batch operation UI text
- Exclude image files from batch operations per design requirements

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* style: make batch delete popconfirm icon color consistent

- Add red color styling to batch delete popconfirm icon to match individual delete
- Update i18n translations for batch warning message across all locales

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: improve batch delete UX and i18n translations

- Change "batch_operation" label to "Select All" across all languages
- Remove unused batch_warning translation for paintings
- Simplify delete confirmation messages to use single form
- Optimize batch delete to use Promise.all for better performance

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Pleasure1234
2025-08-29 00:57:24 +08:00
committed by GitHub
parent 144012b980
commit 279ab8f808
10 changed files with 124 additions and 19 deletions
+2
View File
@@ -868,6 +868,8 @@
"files": {
"actions": "Actions",
"all": "All Files",
"batch_delete": "Batch delete",
"batch_operation": "Select All",
"count": "files",
"created_at": "Created At",
"delete": {
+2
View File
@@ -868,6 +868,8 @@
"files": {
"actions": "操作",
"all": "すべてのファイル",
"batch_delete": "一括削除",
"batch_operation": "すべて選択",
"count": "ファイル",
"created_at": "作成日",
"delete": {
+2
View File
@@ -868,6 +868,8 @@
"files": {
"actions": "Действия",
"all": "Все файлы",
"batch_delete": "массовое удаление",
"batch_operation": "Выделить всё",
"count": "файлов",
"created_at": "Дата создания",
"delete": {
+2
View File
@@ -868,6 +868,8 @@
"files": {
"actions": "操作",
"all": "所有文件",
"batch_delete": "批量删除",
"batch_operation": "全选",
"count": "个文件",
"created_at": "创建时间",
"delete": {
+2
View File
@@ -868,6 +868,8 @@
"files": {
"actions": "操作",
"all": "所有檔案",
"batch_delete": "批次刪除",
"batch_operation": "全選",
"count": "個檔案",
"created_at": "建立時間",
"delete": {
@@ -868,6 +868,8 @@
"files": {
"actions": "Ενέργειες",
"all": "Όλα τα αρχεία",
"batch_delete": "μαζική διαγραφή",
"batch_operation": "Επιλογή όλων",
"count": "Αριθμός αρχείων",
"created_at": "Ημερομηνία δημιουργίας",
"delete": {
@@ -868,6 +868,8 @@
"files": {
"actions": "Acciones",
"all": "Todos los archivos",
"batch_delete": "Eliminación masiva",
"batch_operation": "Seleccionar todo",
"count": "Número de archivos",
"created_at": "Fecha de creación",
"delete": {
@@ -868,6 +868,8 @@
"files": {
"actions": "Actions",
"all": "Tous les fichiers",
"batch_delete": "supprimer en masse",
"batch_operation": "Tout sélectionner",
"count": "Nombre de fichiers",
"created_at": "Date de création",
"delete": {
@@ -868,6 +868,8 @@
"files": {
"actions": "Ações",
"all": "Todos os Arquivos",
"batch_delete": "excluir em massa",
"batch_operation": "Selecionar tudo",
"count": "Número de Arquivos",
"created_at": "Data de Criação",
"delete": {
+106 -19
View File
@@ -6,9 +6,10 @@ import db from '@renderer/databases'
import { getFileFieldLabel } from '@renderer/i18n/label'
import { handleDelete, handleRename, sortFiles, tempFilesSort } from '@renderer/services/FileAction'
import FileManager from '@renderer/services/FileManager'
import store from '@renderer/store'
import { FileMetadata, FileTypes } from '@renderer/types'
import { formatFileSize } from '@renderer/utils'
import { Button, Empty, Flex, Popconfirm } from 'antd'
import { Button, Checkbox, Dropdown, Empty, Flex, Popconfirm } from 'antd'
import dayjs from 'dayjs'
import { useLiveQuery } from 'dexie-react-hooks'
import {
@@ -19,7 +20,7 @@ import {
FileText,
FileType as FileTypeIcon
} from 'lucide-react'
import { FC, useState } from 'react'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -33,6 +34,11 @@ const FilesPage: FC = () => {
const [fileType, setFileType] = useState<string>('document')
const [sortField, setSortField] = useState<SortField>('created_at')
const [sortOrder, setSortOrder] = useState<SortOrder>('desc')
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([])
useEffect(() => {
setSelectedFileIds([])
}, [fileType])
const files = useLiveQuery<FileMetadata[]>(() => {
if (fileType === 'all') {
@@ -43,6 +49,44 @@ const FilesPage: FC = () => {
const sortedFiles = files ? sortFiles(files, sortField, sortOrder) : []
const handleBatchDelete = async () => {
const selectedFiles = await Promise.all(selectedFileIds.map((id) => FileManager.getFile(id)))
const validFiles = selectedFiles.filter((file) => file !== null && file !== undefined)
const paintings = store.getState().paintings.paintings
const paintingsFiles = paintings.flatMap((p) => p.files)
const filesInPaintings = validFiles.filter((file) => paintingsFiles.some((p) => p.id === file.id))
if (filesInPaintings.length > 0) {
window.modal.warning({
content: t('files.delete.paintings.warning'),
centered: true
})
return
}
await Promise.all(selectedFileIds.map((fileId) => handleDelete(fileId, t)))
setSelectedFileIds([])
}
const handleSelectFile = (fileId: string, checked: boolean) => {
if (checked) {
setSelectedFileIds((prev) => [...prev, fileId])
} else {
setSelectedFileIds((prev) => prev.filter((id) => id !== fileId))
}
}
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedFileIds(sortedFiles.map((file) => file.id))
} else {
setSelectedFileIds([])
}
}
const dataSource = sortedFiles?.map((file) => {
return {
key: file.id,
@@ -71,6 +115,13 @@ const FilesPage: FC = () => {
icon={<ExclamationCircleOutlined style={{ color: 'red' }} />}>
<Button type="text" danger icon={<DeleteIcon size={14} className="lucide-custom" />} />
</Popconfirm>
{fileType !== 'image' && (
<Checkbox
checked={selectedFileIds.includes(file.id)}
onChange={(e) => handleSelectFile(file.id, e.target.checked)}
style={{ margin: '0 8px' }}
/>
)}
</Flex>
)
}
@@ -102,23 +153,58 @@ const FilesPage: FC = () => {
</SideNav>
<MainContent>
<SortContainer>
{(['created_at', 'size', 'name'] as const).map((field) => (
<SortButton
key={field}
active={sortField === field}
onClick={() => {
if (sortField === field) {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
} else {
setSortField(field as 'created_at' | 'size' | 'name')
setSortOrder('desc')
}
}}>
{getFileFieldLabel(field)}
{sortField === field &&
(sortOrder === 'desc' ? <ArrowUpWideNarrow size={12} /> : <ArrowDownNarrowWide size={12} />)}
</SortButton>
))}
<Flex gap={8} align="center">
{(['created_at', 'size', 'name'] as const).map((field) => (
<SortButton
key={field}
active={sortField === field}
onClick={() => {
if (sortField === field) {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
} else {
setSortField(field as 'created_at' | 'size' | 'name')
setSortOrder('desc')
}
}}>
{getFileFieldLabel(field)}
{sortField === field &&
(sortOrder === 'desc' ? <ArrowUpWideNarrow size={12} /> : <ArrowDownNarrowWide size={12} />)}
</SortButton>
))}
</Flex>
{fileType !== 'image' && (
<Dropdown.Button
style={{ width: 'auto' }}
menu={{
items: [
{
key: 'delete',
disabled: selectedFileIds.length === 0,
danger: true,
label: (
<Popconfirm
disabled={selectedFileIds.length === 0}
title={t('files.delete.title')}
description={t('files.delete.content')}
okText={t('common.confirm')}
cancelText={t('common.cancel')}
onConfirm={handleBatchDelete}
icon={<ExclamationCircleOutlined style={{ color: 'red' }} />}>
{t('files.batch_delete')} ({selectedFileIds.length})
</Popconfirm>
)
}
]
}}
trigger={['click']}>
<Checkbox
indeterminate={selectedFileIds.length > 0 && selectedFileIds.length < sortedFiles.length}
checked={selectedFileIds.length === sortedFiles.length && sortedFiles.length > 0}
onChange={(e) => handleSelectAll(e.target.checked)}>
{t('files.batch_operation')}
</Checkbox>
</Dropdown.Button>
)}
</SortContainer>
{dataSource && dataSource?.length > 0 ? (
<FileList id={fileType} list={dataSource} files={sortedFiles} />
@@ -147,6 +233,7 @@ const MainContent = styled.div`
const SortContainer = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 16px;
border-bottom: 0.5px solid var(--color-border);