From 7c77847c25fe1a2ae4ed2682ccc07bb279312ea3 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Mon, 10 Feb 2025 17:39:50 +0100 Subject: [PATCH] feat: move ArchivedChats to cursor and DataTable standard --- .../General/ArchivedChatsTable.tsx | 482 +++++++++--------- 1 file changed, 233 insertions(+), 249 deletions(-) diff --git a/client/src/components/Nav/SettingsTabs/General/ArchivedChatsTable.tsx b/client/src/components/Nav/SettingsTabs/General/ArchivedChatsTable.tsx index 3b24fe80b62..82faba8a473 100644 --- a/client/src/components/Nav/SettingsTabs/General/ArchivedChatsTable.tsx +++ b/client/src/components/Nav/SettingsTabs/General/ArchivedChatsTable.tsx @@ -1,287 +1,271 @@ -import { useState, useCallback, useMemo } from 'react'; -import { - Search, - TrashIcon, - ChevronLeft, - ChevronRight, - // ChevronsLeft, - // ChevronsRight, - MessageCircle, - ArchiveRestore, -} from 'lucide-react'; +import { useState, useCallback, useMemo, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import debounce from 'lodash/debounce'; +import { Search, TrashIcon, MessageCircle, ArchiveRestore } from 'lucide-react'; import type { TConversation } from 'librechat-data-provider'; import { - Table, - Input, Button, - TableRow, - Skeleton, OGDialog, + OGDialogContent, + OGDialogHeader, + OGDialogTitle, + Label, Separator, - TableCell, - TableBody, - TableHead, - TableHeader, - TooltipAnchor, - OGDialogTrigger, + Skeleton, + Spinner, } from '~/components'; -import { useConversationsInfiniteQuery, useArchiveConvoMutation } from '~/data-provider'; -import { DeleteConversationDialog } from '~/components/Conversations/ConvoOptions'; +import { + useConversationsInfiniteQuery, + useArchiveConvoMutation, + useDeleteConversationMutation, +} from '~/data-provider'; import { useAuthContext, useLocalize, useMediaQuery } from '~/hooks'; -import { cn } from '~/utils'; +import DataTable from '~/components/ui/DataTable'; +import { NotificationSeverity } from '~/common'; +import { useToastContext } from '~/Providers'; +import { cn, formatDate } from '~/utils'; + +const DEFAULT_PARAMS = { + isArchived: true, + search: '', + sortBy: 'createdAt', + sortDirection: 'desc', + // use nextCursor for pagination +}; export default function ArchivedChatsTable() { const localize = useLocalize(); const { isAuthenticated } = useAuthContext(); const isSmallScreen = useMediaQuery('(max-width: 768px)'); - const [isOpened, setIsOpened] = useState(false); - const [currentPage, setCurrentPage] = useState(1); - const [searchQuery, setSearchQuery] = useState(''); + const { showToast } = useToastContext(); + + const [queryParams, setQueryParams] = useState(DEFAULT_PARAMS); + const [deleteConversation, setDeleteConversation] = useState(null); + const [isDeleteOpen, setIsDeleteOpen] = useState(false); const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage, refetch } = - useConversationsInfiniteQuery( - { pageNumber: currentPage.toString(), isArchived: true }, - { enabled: isAuthenticated && isOpened }, - ); - const mutation = useArchiveConvoMutation(); + useConversationsInfiniteQuery(queryParams, { + enabled: isAuthenticated, + staleTime: 0, + cacheTime: 5 * 60 * 1000, + refetchOnWindowFocus: false, + refetchOnMount: false, + }); + + const unarchiveMutation = useArchiveConvoMutation(); + const deleteMutation = useDeleteConversationMutation({ + onSuccess: async () => { + showToast({ + message: localize('com_ui_delete_success'), + severity: NotificationSeverity.SUCCESS, + }); + setIsDeleteOpen(false); + setDeleteConversation(null); + await refetch(); + }, + onError: (error) => { + console.error('Delete error:', error); + showToast({ + message: localize('com_ui_delete_error'), + severity: NotificationSeverity.ERROR, + }); + }, + }); + const handleUnarchive = useCallback( (conversationId: string) => { - mutation.mutate({ conversationId, isArchived: false }); + unarchiveMutation.mutate({ conversationId, isArchived: false }); }, - [mutation], + [unarchiveMutation], ); - const conversations = useMemo( - () => data?.pages[currentPage - 1]?.conversations ?? [], - [data, currentPage], - ); - const totalPages = useMemo(() => Math.ceil(Number(data?.pages[0].pages ?? 1)) ?? 1, [data]); - - const handleChatClick = useCallback((conversationId: string) => { - if (!conversationId) { - return; + const allConversations: TConversation[] = useMemo(() => { + if (!data?.pages) { + return []; } - window.open(`/c/${conversationId}`, '_blank'); - }, []); - - const handlePageChange = useCallback( - (newPage: number) => { - setCurrentPage(newPage); - if (!(hasNextPage ?? false)) { - return; - } - fetchNextPage({ pageParam: newPage }); - }, - [fetchNextPage, hasNextPage], - ); + return data.pages.flatMap((page) => page.conversations ?? []); + }, [data]); - const handleSearch = useCallback((query: string) => { - setSearchQuery(query); - setCurrentPage(1); + const handleSort = useCallback((sortField: string, sortOrder: 'asc' | 'desc') => { + setQueryParams((prev) => ({ + ...prev, + sortBy: sortField, + sortDirection: sortOrder, + })); }, []); - const getRandomWidth = () => Math.floor(Math.random() * (400 - 170 + 1)) + 170; - - const skeletons = Array.from({ length: 11 }, (_, index) => { - const randomWidth = getRandomWidth(); - return ( -
-
- -
-
- -
-
- -
-
- ); - }); - - if (isLoading || isFetchingNextPage) { - return
{skeletons}
; - } - - if (!data || (conversations.length === 0 && totalPages === 0)) { - return
{localize('com_nav_archived_chats_empty')}
; - } + const handleFilterChange = useCallback((value: string) => { + setQueryParams((prev) => ({ + ...prev, + search: encodeURIComponent(value.trim()), + })); + }, []); - return ( -
setIsOpened(true)} - > -
- - handleSearch(e.target.value)} - className="w-full border-none placeholder:text-text-secondary" - /> -
- - {conversations.length === 0 ? ( -
{localize('com_nav_no_search_results')}
- ) : ( - <> - - - - - {localize('com_nav_archive_name')} - - {!isSmallScreen && ( - - {localize('com_nav_archive_created_at')} - - )} - - {localize('com_assistants_actions')} - - - - - {conversations.map((conversation: TConversation) => ( - - - - - {!isSmallScreen && ( - -
-
- {new Date(conversation.createdAt).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - })} -
-
-
- )} - - { - const conversationId = conversation.conversationId ?? ''; - if (!conversationId) { - return; - } - handleUnarchive(conversationId); - }} - > - - - } - /> + const debouncedFilterChange = useMemo( + () => debounce(handleFilterChange, 300), + [handleFilterChange], + ); + useEffect(() => { + return () => { + debouncedFilterChange.cancel(); + }; + }, [debouncedFilterChange]); - - - - - - } - /> - - - - -
- ))} -
-
+ const handleFetchNextPage = useCallback(async () => { + if (!hasNextPage || isFetchingNextPage) { + return; + } + await fetchNextPage(); + }, [fetchNextPage, hasNextPage, isFetchingNextPage]); -
-
- {localize('com_ui_page')} {currentPage} {localize('com_ui_of')} {totalPages} -
-
- {/* */} + const columns = useMemo( + () => [ + { + accessorKey: 'title', + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const { conversationId, title } = row.original; + return ( + + ); + }, + meta: { + size: isSmallScreen ? '70%' : '50%', + mobileSize: '70%', + }, + }, + { + accessorKey: 'createdAt', + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => formatDate(row.original.createdAt?.toString() ?? '', isSmallScreen), + meta: { + size: isSmallScreen ? '30%' : '35%', + mobileSize: '30%', + }, + }, + { + accessorKey: 'actions', + header: () => ( + + ), + cell: ({ row }) => { + const conversation = row.original; + return ( +
- {/* */}
+ ); + }, + meta: { + size: '15%', + mobileSize: '25%', + }, + }, + ], + [handleSort, handleUnarchive, isSmallScreen, localize], + ); + + if (isLoading) { + return ( +
+ {Array.from({ length: 10 }, (_, index) => ( +
+ +
+ ))} +
+ ); + } + + return ( + <> + + + + + + + {localize('com_ui_delete_confirm')} {deleteConversation?.title} + + +
+ +
- - )} -
+ + + ); }