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:
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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": "رفع",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "上传",
|
||||
|
||||
@@ -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": "上傳",
|
||||
|
||||
Reference in New Issue
Block a user