fix(ui): fix selection state management in paginated views

- Replace DeselectDocumentsDialog with smart selection button
- Auto-reset selection on page/filter changes
- Remove deletion restrictions and update i18n
This commit is contained in:
yangdx
2025-08-17 10:38:12 +08:00
parent 3e4214cef3
commit 1af0803c62
8 changed files with 93 additions and 133 deletions

View File

@@ -35,11 +35,10 @@ const Label = ({
interface DeleteDocumentsDialogProps {
selectedDocIds: string[]
totalCompletedCount: number
onDocumentsDeleted?: () => Promise<void>
}
export default function DeleteDocumentsDialog({ selectedDocIds, totalCompletedCount, onDocumentsDeleted }: DeleteDocumentsDialogProps) {
export default function DeleteDocumentsDialog({ selectedDocIds, onDocumentsDeleted }: DeleteDocumentsDialogProps) {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const [confirmText, setConfirmText] = useState('')
@@ -59,12 +58,6 @@ export default function DeleteDocumentsDialog({ selectedDocIds, totalCompletedCo
const handleDelete = useCallback(async () => {
if (!isConfirmEnabled || selectedDocIds.length === 0) return
// Check if user is trying to delete all completed documents
if (selectedDocIds.length === totalCompletedCount && totalCompletedCount > 0) {
toast.error(t('documentPanel.deleteDocuments.cannotDeleteAll'))
return
}
setIsDeleting(true)
try {
const result = await deleteDocuments(selectedDocIds, deleteFile)
@@ -101,7 +94,7 @@ export default function DeleteDocumentsDialog({ selectedDocIds, totalCompletedCo
} finally {
setIsDeleting(false)
}
}, [isConfirmEnabled, selectedDocIds, totalCompletedCount, deleteFile, setOpen, t, onDocumentsDeleted])
}, [isConfirmEnabled, selectedDocIds, deleteFile, setOpen, t, onDocumentsDeleted])
return (
<Dialog open={open} onOpenChange={setOpen}>

View File

@@ -1,74 +0,0 @@
import { useState, useCallback, useEffect } from 'react'
import Button from '@/components/ui/Button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter
} from '@/components/ui/Dialog'
import { XIcon, AlertCircleIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'
interface DeselectDocumentsDialogProps {
selectedCount: number
onDeselect: () => void
}
export default function DeselectDocumentsDialog({ selectedCount, onDeselect }: DeselectDocumentsDialogProps) {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
// Reset state when dialog closes
useEffect(() => {
if (!open) {
// No state to reset for this simple dialog
}
}, [open])
const handleDeselect = useCallback(() => {
onDeselect()
setOpen(false)
}, [onDeselect, setOpen])
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
variant="outline"
side="bottom"
tooltip={t('documentPanel.deselectDocuments.tooltip')}
size="sm"
>
<XIcon/> {t('documentPanel.deselectDocuments.button')}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md" onCloseAutoFocus={(e) => e.preventDefault()}>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<AlertCircleIcon className="h-5 w-5" />
{t('documentPanel.deselectDocuments.title')}
</DialogTitle>
<DialogDescription className="pt-2">
{t('documentPanel.deselectDocuments.description', { count: selectedCount })}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>
{t('common.cancel')}
</Button>
<Button
variant="default"
onClick={handleDeselect}
>
{t('documentPanel.deselectDocuments.confirmButton')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -17,7 +17,6 @@ import Checkbox from '@/components/ui/Checkbox'
import UploadDocumentsDialog from '@/components/documents/UploadDocumentsDialog'
import ClearDocumentsDialog from '@/components/documents/ClearDocumentsDialog'
import DeleteDocumentsDialog from '@/components/documents/DeleteDocumentsDialog'
import DeselectDocumentsDialog from '@/components/documents/DeselectDocumentsDialog'
import PaginationControls from '@/components/ui/PaginationControls'
import {
@@ -33,7 +32,7 @@ import { errorMessage } from '@/lib/utils'
import { toast } from 'sonner'
import { useBackendState } from '@/stores/state'
import { RefreshCwIcon, ActivityIcon, ArrowUpIcon, ArrowDownIcon, RotateCcwIcon } from 'lucide-react'
import { RefreshCwIcon, ActivityIcon, ArrowUpIcon, ArrowDownIcon, RotateCcwIcon, CheckSquareIcon, XIcon } from 'lucide-react'
import PipelineStatusDialog from '@/components/documents/PipelineStatusDialog'
type StatusFilter = DocStatus | 'all';
@@ -327,6 +326,52 @@ export default function DocumentManager() {
return allDocuments;
}, [docs, sortField, sortDirection, statusFilter, sortDocuments]);
// Calculate current page selection state (after filteredAndSortedDocs is defined)
const currentPageDocIds = useMemo(() => {
return filteredAndSortedDocs?.map(doc => doc.id) || []
}, [filteredAndSortedDocs])
const selectedCurrentPageCount = useMemo(() => {
return currentPageDocIds.filter(id => selectedDocIds.includes(id)).length
}, [currentPageDocIds, selectedDocIds])
const isCurrentPageFullySelected = useMemo(() => {
return currentPageDocIds.length > 0 && selectedCurrentPageCount === currentPageDocIds.length
}, [currentPageDocIds, selectedCurrentPageCount])
const hasCurrentPageSelection = useMemo(() => {
return selectedCurrentPageCount > 0
}, [selectedCurrentPageCount])
// Handle select current page
const handleSelectCurrentPage = useCallback(() => {
setSelectedDocIds(currentPageDocIds)
}, [currentPageDocIds])
// Get selection button properties
const getSelectionButtonProps = useCallback(() => {
if (!hasCurrentPageSelection) {
return {
text: t('documentPanel.selectDocuments.selectCurrentPage', { count: currentPageDocIds.length }),
action: handleSelectCurrentPage,
icon: CheckSquareIcon
}
} else if (isCurrentPageFullySelected) {
return {
text: t('documentPanel.selectDocuments.deselectAll', { count: currentPageDocIds.length }),
action: handleDeselectAll,
icon: XIcon
}
} else {
return {
text: t('documentPanel.selectDocuments.selectCurrentPage', { count: currentPageDocIds.length }),
action: handleSelectCurrentPage,
icon: CheckSquareIcon
}
}
}, [hasCurrentPageSelection, isCurrentPageFullySelected, currentPageDocIds.length, handleSelectCurrentPage, handleDeselectAll, t])
// Calculate document counts for each status
const documentCounts = useMemo(() => {
if (!docs) return { all: 0 } as Record<string, number>;
@@ -766,6 +811,11 @@ export default function DocumentManager() {
}
}, [showFileName, sortField]);
// Reset selection state when page, status filter, or sort changes
useEffect(() => {
setSelectedDocIds([])
}, [pagination.page, statusFilter, sortField, sortDirection]);
// Central effect to handle all data fetching
useEffect(() => {
if (currentTab === 'documents') {
@@ -830,18 +880,29 @@ export default function DocumentManager() {
{isSelectionMode && (
<DeleteDocumentsDialog
selectedDocIds={selectedDocIds}
totalCompletedCount={documentCounts.processed || 0}
onDocumentsDeleted={handleDocumentsDeleted}
/>
)}
{isSelectionMode ? (
<DeselectDocumentsDialog
selectedCount={selectedDocIds.length}
onDeselect={handleDeselectAll}
/>
) : (
{isSelectionMode && hasCurrentPageSelection ? (
(() => {
const buttonProps = getSelectionButtonProps();
const IconComponent = buttonProps.icon;
return (
<Button
variant="outline"
size="sm"
onClick={buttonProps.action}
side="bottom"
tooltip={buttonProps.text}
>
<IconComponent className="h-4 w-4" />
{buttonProps.text}
</Button>
);
})()
) : !isSelectionMode ? (
<ClearDocumentsDialog onDocumentsCleared={handleDocumentsCleared} />
)}
) : null}
<UploadDocumentsDialog onDocumentsUploaded={fetchDocuments} />
<PipelineStatusDialog
open={showPipelineStatus}

View File

@@ -74,15 +74,11 @@
"failed": "فشل حذف المستندات:\n{{message}}",
"error": "فشل حذف المستندات:\n{{error}}",
"busy": "خط المعالجة مشغول، يرجى المحاولة مرة أخرى لاحقًا",
"notAllowed": "لا توجد صلاحية لتنفيذ هذه العملية",
"cannotDeleteAll": "لا يمكن حذف جميع المستندات. إذا كنت بحاجة لحذف جميع المستندات، يرجى استخدام ميزة مسح المستندات."
"notAllowed": "لا توجد صلاحية لتنفيذ هذه العملية"
},
"deselectDocuments": {
"button": "إلغاء التحديد",
"tooltip": "إلغاء تحديد جميع المستندات المحددة",
"title": "إلغاء تحديد المستندات",
"description": "سيؤدي هذا إلى مسح جميع المستندات المحددة ({{count}} محدد)",
"confirmButton": "إلغاء تحديد الكل"
"selectDocuments": {
"selectCurrentPage": "تحديد الصفحة الحالية ({{count}})",
"deselectAll": "إلغاء تحديد الكل ({{count}})"
},
"uploadDocuments": {
"button": "رفع",

View File

@@ -74,15 +74,11 @@
"failed": "Delete Documents Failed:\n{{message}}",
"error": "Delete Documents Failed:\n{{error}}",
"busy": "Pipeline is busy, please try again later",
"notAllowed": "No permission to perform this operation",
"cannotDeleteAll": "Cannot delete all documents. If you need to delete all documents, please use the Clear Documents feature."
"notAllowed": "No permission to perform this operation"
},
"deselectDocuments": {
"button": "Deselect",
"tooltip": "Deselect all selected documents",
"title": "Deselect Documents",
"description": "This will clear all selected documents ({{count}} selected)",
"confirmButton": "Deselect All"
"selectDocuments": {
"selectCurrentPage": "Select Current Page ({{count}})",
"deselectAll": "Deselect All ({{count}})"
},
"uploadDocuments": {
"button": "Upload",

View File

@@ -74,15 +74,11 @@
"failed": "Échec de la suppression des documents :\n{{message}}",
"error": "Échec de la suppression des documents :\n{{error}}",
"busy": "Le pipeline est occupé, veuillez réessayer plus tard",
"notAllowed": "Aucune autorisation pour effectuer cette opération",
"cannotDeleteAll": "Impossible de supprimer tous les documents. Si vous devez supprimer tous les documents, veuillez utiliser la fonction Effacer les documents."
"notAllowed": "Aucune autorisation pour effectuer cette opération"
},
"deselectDocuments": {
"button": "Désélectionner",
"tooltip": "Désélectionner tous les documents sélectionnés",
"title": "Désélectionner les documents",
"description": "Cette action effacera tous les documents sélectionnés ({{count}} sélectionnés)",
"confirmButton": "Tout désélectionner"
"selectDocuments": {
"selectCurrentPage": "Sélectionner la page actuelle ({{count}})",
"deselectAll": "Tout désélectionner ({{count}})"
},
"uploadDocuments": {
"button": "Télécharger",

View File

@@ -74,15 +74,11 @@
"failed": "删除文档失败:\n{{message}}",
"error": "删除文档失败:\n{{error}}",
"busy": "流水线被占用,请稍后再试",
"notAllowed": "没有操作权限",
"cannotDeleteAll": "无法删除所有文档。如确实需要删除所有文档请使用清空文档功能。"
"notAllowed": "没有操作权限"
},
"deselectDocuments": {
"button": "取消选择",
"tooltip": "取消选择所有文档",
"title": "取消选择文档",
"description": "此操作将清除所有选中的文档(已选择 {{count}} 个)",
"confirmButton": "取消全部选择"
"selectDocuments": {
"selectCurrentPage": "全选当前页 ({{count}})",
"deselectAll": "取消全选 ({{count}})"
},
"uploadDocuments": {
"button": "上传",

View File

@@ -74,15 +74,11 @@
"failed": "刪除文件失敗:\n{{message}}",
"error": "刪除文件失敗:\n{{error}}",
"busy": "pipeline 被佔用,請稍後再試",
"notAllowed": "沒有操作權限",
"cannotDeleteAll": "無法刪除所有文件。如確實需要刪除所有文件請使用清空文件功能。"
"notAllowed": "沒有操作權限"
},
"deselectDocuments": {
"button": "取消選取",
"tooltip": "取消選取所有文件",
"title": "取消選取文件",
"description": "此操作將清除所有選取的文件(已選取 {{count}} 個)",
"confirmButton": "取消全部選取"
"selectDocuments": {
"selectCurrentPage": "全選當前頁 ({{count}})",
"deselectAll": "取消全選 ({{count}})"
},
"uploadDocuments": {
"button": "上傳",