import { useCallback, useState, useMemo, useEffect } from 'react'; import debounce from 'lodash/debounce'; import { useRecoilValue } from 'recoil'; import { Link } from 'react-router-dom'; import { TrashIcon, MessageSquare, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react'; import type { SharedLinkItem, SharedLinksListParams } from 'librechat-data-provider'; import { OGDialog, useToastContext, OGDialogTemplate, OGDialogTrigger, OGDialogContent, useMediaQuery, OGDialogHeader, OGDialogTitle, TooltipAnchor, DataTable, Spinner, Button, Label, } from '@librechat/client'; import { useDeleteSharedLinkMutation, useSharedLinksQuery } from '~/data-provider'; import { useLocalize } from '~/hooks'; import { NotificationSeverity } from '~/common'; import { formatDate } from '~/utils'; import store from '~/store'; const PAGE_SIZE = 25; const DEFAULT_PARAMS: SharedLinksListParams = { pageSize: PAGE_SIZE, isPublic: true, sortBy: 'createdAt', sortDirection: 'desc', search: '', }; 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 { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch, isLoading } = useSharedLinksQuery(queryParams, { enabled: isOpen, staleTime: 0, cacheTime: 5 * 60 * 1000, refetchOnWindowFocus: false, refetchOnMount: false, }); const handleSort = useCallback((sortField: string, sortOrder: 'asc' | 'desc') => { setQueryParams((prev) => ({ ...prev, sortBy: sortField as 'title' | 'createdAt', sortDirection: sortOrder, })); }, []); const handleFilterChange = useCallback((value: string) => { const encodedValue = encodeURIComponent(value.trim()); setQueryParams((prev) => ({ ...prev, search: encodedValue, })); }, []); const debouncedFilterChange = useMemo( () => debounce(handleFilterChange, 300), [handleFilterChange], ); useEffect(() => { return () => { debouncedFilterChange.cancel(); }; }, [debouncedFilterChange]); const allLinks = useMemo(() => { if (!data?.pages) { return []; } return data.pages.flatMap((page) => page.links.filter(Boolean)); }, [data?.pages]); const deleteMutation = useDeleteSharedLinkMutation({ onSuccess: async () => { setIsDeleteOpen(false); setDeleteRow(null); await refetch(); }, onError: (error) => { console.error('Delete error:', error); showToast({ message: localize('com_ui_share_delete_error'), severity: NotificationSeverity.ERROR, }); }, }); const handleDelete = useCallback( async (selectedRows: SharedLinkItem[]) => { const validRows = selectedRows.filter( (row) => typeof row.shareId === 'string' && row.shareId.length > 0, ); if (validRows.length === 0) { showToast({ message: localize('com_ui_no_valid_items'), severity: NotificationSeverity.WARNING, }); return; } try { for (const row of validRows) { await deleteMutation.mutateAsync({ shareId: row.shareId }); } showToast({ message: localize( validRows.length === 1 ? 'com_ui_shared_link_delete_success' : 'com_ui_shared_link_bulk_delete_success', ), severity: NotificationSeverity.SUCCESS, }); } catch (error) { console.error('Failed to delete shared links:', error); showToast({ message: localize('com_ui_bulk_delete_error'), severity: NotificationSeverity.ERROR, }); } }, [deleteMutation, showToast, localize], ); const handleFetchNextPage = useCallback(async () => { if (hasNextPage !== true || isFetchingNextPage) { return; } await fetchNextPage(); }, [fetchNextPage, hasNextPage, isFetchingNextPage]); const confirmDelete = useCallback(() => { if (deleteRow) { handleDelete([deleteRow]); } setIsDeleteOpen(false); }, [deleteRow, handleDelete]); const columns = useMemo( () => [ { accessorKey: 'title', header: () => { const isSorted = queryParams.sortBy === 'title'; const sortDirection = queryParams.sortDirection; return ( ); }, cell: ({ row }) => { const { title, shareId } = row.original; return (
{title}
); }, meta: { size: '35%', mobileSize: '50%', }, }, { accessorKey: 'createdAt', header: () => { const isSorted = queryParams.sortBy === 'createdAt'; const sortDirection = queryParams.sortDirection; return ( ); }, cell: ({ row }) => formatDate(row.original.createdAt?.toString() ?? '', isSmallScreen), meta: { size: '10%', mobileSize: '20%', }, }, { accessorKey: 'actions', header: () => ( ), meta: { size: '7%', mobileSize: '25%', }, cell: ({ row }) => (
{ window.open(`/c/${row.original.conversationId}`, '_blank'); }} title={localize('com_ui_view_source')} > } /> { setDeleteRow(row.original); setIsDeleteOpen(true); }} title={localize('com_ui_delete')} > } />
), }, ], [isSmallScreen, localize, queryParams, handleSort], ); return (
setIsOpen(true)}> {localize('com_nav_shared_links')}
} selection={{ selectHandler: confirmDelete, selectClasses: `bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white ${ deleteMutation.isLoading ? 'cursor-not-allowed opacity-80' : '' }`, selectText: deleteMutation.isLoading ? : localize('com_ui_delete'), }} />
); }