diff --git a/client/src/components/Nav/SettingsTabs/Data/SharedLinks.tsx b/client/src/components/Nav/SettingsTabs/Data/SharedLinks.tsx index e2073c02e6..143ab126ad 100644 --- a/client/src/components/Nav/SettingsTabs/Data/SharedLinks.tsx +++ b/client/src/components/Nav/SettingsTabs/Data/SharedLinks.tsx @@ -1,5 +1,4 @@ -import { useCallback, useState, useMemo, useEffect, useRef } from 'react'; -import debounce from 'lodash/debounce'; +import { useCallback, useState, useMemo } from 'react'; import { useRecoilValue } from 'recoil'; import { Link } from 'react-router-dom'; import { TrashIcon, MessageSquare } from 'lucide-react'; @@ -23,7 +22,6 @@ import { useDeleteSharedLinkMutation, useSharedLinksQuery } from '~/data-provide import { NotificationSeverity } from '~/common'; import { useLocalize } from '~/hooks'; import { formatDate } from '~/utils'; -import store from '~/store'; const DEFAULT_PARAMS: SharedLinksListParams = { pageSize: 25, @@ -37,38 +35,20 @@ export default function SharedLinks() { const localize = useLocalize(); const { showToast } = useToastContext(); const isSmallScreen = useMediaQuery('(max-width: 768px)'); - const isSearchEnabled = useRecoilValue(store.search); const [queryParams, setQueryParams] = useState(DEFAULT_PARAMS); const [deleteRow, setDeleteRow] = useState(null); const [isDeleteOpen, setIsDeleteOpen] = useState(false); const [isOpen, setIsOpen] = useState(false); - const prevSortRef = useRef({ - sortBy: DEFAULT_PARAMS.sortBy, - sortDirection: DEFAULT_PARAMS.sortDirection, - }); - const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch, isLoading } = + const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isFetching, refetch, isLoading } = useSharedLinksQuery(queryParams, { enabled: isOpen, + keepPreviousData: true, staleTime: 30 * 1000, - cacheTime: 5 * 60 * 1000, refetchOnWindowFocus: false, refetchOnMount: false, - keepPreviousData: false, }); - useEffect(() => { - if (!isOpen) return; - - const { sortBy, sortDirection } = queryParams; - const prevSort = prevSortRef.current; - - if (sortBy !== prevSort.sortBy || sortDirection !== prevSort.sortDirection) { - refetch(); - prevSortRef.current = { sortBy, sortDirection }; - } - }, [queryParams, isOpen, refetch]); - const handleSort = useCallback((sortField: string, sortOrder: 'asc' | 'desc') => { setQueryParams((prev) => ({ ...prev, @@ -85,17 +65,6 @@ export default function SharedLinks() { })); }, []); - const debouncedFilterChange = useMemo( - () => debounce(handleFilterChange, 500), // Increased debounce time to 500ms - [handleFilterChange], - ); - - useEffect(() => { - return () => { - debouncedFilterChange.cancel(); - }; - }, [debouncedFilterChange]); - const allLinks = useMemo(() => { if (!data?.pages) { return []; @@ -286,15 +255,13 @@ export default function SharedLinks() { columns={columns} data={allLinks} onDelete={handleDelete} - filterColumn="title" + config={{ skeleton: { count: 10 }, search: { filterColumn: 'title' } }} hasNextPage={hasNextPage} isFetchingNextPage={isFetchingNextPage} + isFetching={isFetching} fetchNextPage={handleFetchNextPage} - showCheckboxes={false} - onFilterChange={debouncedFilterChange} - filterValue={queryParams.search} + onFilterChange={handleFilterChange} isLoading={isLoading} - enableSearch={!!isSearchEnabled} onSortChange={handleSort} sortBy={queryParams.sortBy} sortDirection={queryParams.sortDirection} diff --git a/client/src/components/Nav/SettingsTabs/General/ArchivedChats.tsx b/client/src/components/Nav/SettingsTabs/General/ArchivedChats.tsx index 8bd569d8c6..153955de19 100644 --- a/client/src/components/Nav/SettingsTabs/General/ArchivedChats.tsx +++ b/client/src/components/Nav/SettingsTabs/General/ArchivedChats.tsx @@ -1,7 +1,6 @@ -import { useState, useCallback, useMemo, useEffect, useRef } from 'react'; -import debounce from 'lodash/debounce'; -import { useRecoilValue } from 'recoil'; +import { useState, useCallback, useMemo } from 'react'; import { TrashIcon, ArchiveRestore } from 'lucide-react'; +import type { ColumnDef, SortingState } from '@tanstack/react-table'; import { Button, OGDialog, @@ -27,7 +26,6 @@ import { MinimalIcon } from '~/components/Endpoints'; import { NotificationSeverity } from '~/common'; import { useLocalize } from '~/hooks'; import { formatDate } from '~/utils'; -import store from '~/store'; const DEFAULT_PARAMS: ConversationListParams = { isArchived: true, @@ -36,82 +34,86 @@ const DEFAULT_PARAMS: ConversationListParams = { search: '', }; -type SortField = 'title' | 'createdAt'; +const defaultSort: SortingState = [ + { + id: 'createdAt', + desc: true, + }, +]; + +// Define the table column type for better type safety +type TableColumn = ColumnDef & { + meta?: { + size?: string | number; + mobileSize?: string | number; + minWidth?: string | number; + }; +}; export default function ArchivedChatsTable() { const localize = useLocalize(); const isSmallScreen = useMediaQuery('(max-width: 768px)'); const { showToast } = useToastContext(); - const isSearchEnabled = useRecoilValue(store.search); const [isOpen, setIsOpen] = useState(false); const [isDeleteOpen, setIsDeleteOpen] = useState(false); const [deleteRow, setDeleteRow] = useState(null); const [unarchivingId, setUnarchivingId] = useState(null); - const prevSortRef = useRef({ - sortBy: DEFAULT_PARAMS.sortBy, - sortDirection: DEFAULT_PARAMS.sortDirection, - }); const [queryParams, setQueryParams] = useState(DEFAULT_PARAMS); - const [searchInput, setSearchInput] = useState(''); + const [sorting, setSorting] = useState(defaultSort); + const [searchValue, setSearchValue] = useState(''); - const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch, isLoading } = + const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isFetching, refetch, isLoading } = useConversationsInfiniteQuery(queryParams, { enabled: isOpen, + keepPreviousData: true, staleTime: 30 * 1000, - cacheTime: 5 * 60 * 1000, refetchOnWindowFocus: false, refetchOnMount: false, - keepPreviousData: false, }); - const handleSort = useCallback((field: string, direction: 'asc' | 'desc') => { + const handleSearchChange = useCallback((value: string) => { + const trimmedValue = value.trim(); + setSearchValue(trimmedValue); setQueryParams((prev) => ({ ...prev, - sortBy: field as SortField, - sortDirection: direction, + search: trimmedValue, })); }, []); - // Trigger refetch when sort parameters change - useEffect(() => { - if (!isOpen) return; // Only refetch if dialog is open - - const { sortBy, sortDirection } = queryParams; - const prevSort = prevSortRef.current; - - if (sortBy !== prevSort.sortBy || sortDirection !== prevSort.sortDirection) { - console.log('Sort changed, refetching...', { from: prevSort, to: { sortBy, sortDirection } }); - refetch(); - prevSortRef.current = { sortBy, sortDirection }; - } - }, [queryParams, isOpen, refetch]); - - const debouncedApplySearch = useMemo( - () => - debounce((value: string) => { + const handleSortingChange = useCallback( + (updater: SortingState | ((old: SortingState) => SortingState)) => { + const newSorting = typeof updater === 'function' ? updater(sorting) : updater; + setSorting(newSorting); + const sortDescriptor = newSorting[0]; + if (sortDescriptor) { setQueryParams((prev) => ({ ...prev, - search: encodeURIComponent(value.trim()), + sortBy: sortDescriptor.id as 'createdAt' | 'title', + sortDirection: sortDescriptor.desc ? 'desc' : 'asc', })); - }, 500), // Increased debounce time to 500ms for better UX - [], - ); - - const onFilterChange = useCallback( - (value: string) => { - setSearchInput(value); - debouncedApplySearch(value); + } else { + setQueryParams((prev) => ({ + ...prev, + sortBy: 'createdAt', + sortDirection: 'desc', + })); + } }, - [debouncedApplySearch], + [sorting], ); - useEffect(() => { - return () => { - debouncedApplySearch.cancel(); - }; - }, [debouncedApplySearch]); + const handleError = useCallback( + (error: Error) => { + console.error('DataTable error:', error); + showToast({ + message: localize('com_ui_unarchive_error'), + severity: NotificationSeverity.ERROR, + }); + }, + [showToast, localize], + ); const allConversations = useMemo(() => { if (!data?.pages) return []; @@ -149,26 +151,21 @@ export default function ArchivedChatsTable() { const handleFetchNextPage = useCallback(async () => { if (!hasNextPage || isFetchingNextPage) return; - - try { - await fetchNextPage(); - } catch (error) { - console.error('Failed to fetch next page:', error); - showToast({ - message: localize('com_ui_unarchive_error'), - severity: NotificationSeverity.ERROR, - }); - } - }, [fetchNextPage, hasNextPage, isFetchingNextPage, showToast, localize]); + await fetchNextPage(); + }, [fetchNextPage, hasNextPage, isFetchingNextPage]); const confirmDelete = useCallback(() => { - if (!deleteRow?.conversationId) return; + if (!deleteRow?.conversationId) { + showToast({ + message: localize('com_ui_convo_delete_error'), + severity: NotificationSeverity.WARNING, + }); + return; + } deleteMutation.mutate({ conversationId: deleteRow.conversationId }); - }, [deleteMutation, deleteRow]); + }, [deleteMutation, deleteRow, localize, showToast]); - const { sortBy, sortDirection } = queryParams; - - const columns = useMemo( + const columns: TableColumn[] = useMemo( () => [ { accessorKey: 'title', @@ -178,26 +175,29 @@ export default function ArchivedChatsTable() { cell: ({ row }) => { const { conversationId, title } = row.original; return ( - + {title} + ); }, meta: { size: isSmallScreen ? '70%' : '50%', mobileSize: '70%', - enableSorting: true, }, + enableSorting: true, }, { accessorKey: 'createdAt', @@ -208,11 +208,11 @@ export default function ArchivedChatsTable() { meta: { size: isSmallScreen ? '30%' : '35%', mobileSize: '30%', - enableSorting: true, }, + enableSorting: true, }, { - accessorKey: 'actions', + id: 'actions', header: () => (