diff --git a/api/models/Conversation.js b/api/models/Conversation.js index 13c329aa4a..6428d3970a 100644 --- a/api/models/Conversation.js +++ b/api/models/Conversation.js @@ -28,7 +28,7 @@ const getConvo = async (user, conversationId) => { return await Conversation.findOne({ user, conversationId }).lean(); } catch (error) { logger.error('[getConvo] Error getting single conversation', error); - return { message: 'Error getting single conversation' }; + throw new Error('Error getting single conversation'); } }; @@ -151,13 +151,21 @@ module.exports = { const result = await Conversation.bulkWrite(bulkOps); return result; } catch (error) { - logger.error('[saveBulkConversations] Error saving conversations in bulk', error); + logger.error('[bulkSaveConvos] Error saving conversations in bulk', error); throw new Error('Failed to save conversations in bulk.'); } }, getConvosByCursor: async ( user, - { cursor, limit = 25, isArchived = false, tags, search, order = 'desc' } = {}, + { + cursor, + limit = 25, + isArchived = false, + tags, + search, + sortBy = 'createdAt', + sortDirection = 'desc', + } = {}, ) => { const filters = [{ user }]; if (isArchived) { @@ -184,35 +192,77 @@ module.exports = { filters.push({ conversationId: { $in: matchingIds } }); } catch (error) { logger.error('[getConvosByCursor] Error during meiliSearch', error); - return { message: 'Error during meiliSearch' }; + throw new Error('Error during meiliSearch'); } } + const validSortFields = ['title', 'createdAt', 'updatedAt']; + if (!validSortFields.includes(sortBy)) { + throw new Error( + `Invalid sortBy field: ${sortBy}. Must be one of ${validSortFields.join(', ')}`, + ); + } + const finalSortBy = sortBy; + const finalSortDirection = sortDirection === 'asc' ? 'asc' : 'desc'; + + let cursorFilter = null; if (cursor) { - filters.push({ updatedAt: { $lt: new Date(cursor) } }); + try { + const decoded = JSON.parse(Buffer.from(cursor, 'base64').toString()); + const { primary, secondary } = decoded; + const primaryValue = finalSortBy === 'title' ? primary : new Date(primary); + const secondaryValue = new Date(secondary); + const op = finalSortDirection === 'asc' ? '$gt' : '$lt'; + + cursorFilter = { + $or: [ + { [finalSortBy]: { [op]: primaryValue } }, + { + [finalSortBy]: primaryValue, + updatedAt: { [op]: secondaryValue }, + }, + ], + }; + } catch (err) { + logger.warn('[getConvosByCursor] Invalid cursor format, starting from beginning'); + } + if (cursorFilter) { + filters.push(cursorFilter); + } } const query = filters.length === 1 ? filters[0] : { $and: filters }; try { + const sortOrder = finalSortDirection === 'asc' ? 1 : -1; + const sortObj = { [finalSortBy]: sortOrder }; + + if (finalSortBy !== 'updatedAt') { + sortObj.updatedAt = sortOrder; + } + const convos = await Conversation.find(query) .select( 'conversationId endpoint title createdAt updatedAt user model agent_id assistant_id spec iconURL', ) - .sort({ updatedAt: order === 'asc' ? 1 : -1 }) + .sort(sortObj) .limit(limit + 1) .lean(); let nextCursor = null; if (convos.length > limit) { const lastConvo = convos.pop(); - nextCursor = lastConvo.updatedAt.toISOString(); + const primaryValue = lastConvo[finalSortBy]; + const primaryStr = finalSortBy === 'title' ? primaryValue : primaryValue.toISOString(); + const secondaryStr = lastConvo.updatedAt.toISOString(); + const composite = { primary: primaryStr, secondary: secondaryStr }; + nextCursor = Buffer.from(JSON.stringify(composite)).toString('base64'); } return { conversations: convos, nextCursor }; } catch (error) { logger.error('[getConvosByCursor] Error getting conversations', error); - return { message: 'Error getting conversations' }; + throw new Error('Error getting conversations'); } }, getConvosQueried: async (user, convoIds, cursor = null, limit = 25) => { @@ -252,7 +302,7 @@ module.exports = { return { conversations: limited, nextCursor, convoMap }; } catch (error) { logger.error('[getConvosQueried] Error getting conversations', error); - return { message: 'Error fetching conversations' }; + throw new Error('Error fetching conversations'); } }, getConvo, @@ -269,7 +319,7 @@ module.exports = { } } catch (error) { logger.error('[getConvoTitle] Error getting conversation title', error); - return { message: 'Error getting conversation title' }; + throw new Error('Error getting conversation title'); } }, /** diff --git a/api/server/routes/convos.js b/api/server/routes/convos.js index 766b2a21b0..ad82ede10a 100644 --- a/api/server/routes/convos.js +++ b/api/server/routes/convos.js @@ -31,7 +31,8 @@ router.get('/', async (req, res) => { const cursor = req.query.cursor; const isArchived = isEnabled(req.query.isArchived); const search = req.query.search ? decodeURIComponent(req.query.search) : undefined; - const order = req.query.order || 'desc'; + const sortBy = req.query.sortBy || 'createdAt'; + const sortDirection = req.query.sortDirection || 'desc'; let tags; if (req.query.tags) { @@ -45,7 +46,8 @@ router.get('/', async (req, res) => { isArchived, tags, search, - order, + sortBy, + sortDirection, }); res.status(200).json(result); } catch (error) { diff --git a/client/src/components/Chat/Input/Artifacts.tsx b/client/src/components/Chat/Input/Artifacts.tsx index 493f126e3c..adb34ceff6 100644 --- a/client/src/components/Chat/Input/Artifacts.tsx +++ b/client/src/components/Chat/Input/Artifacts.tsx @@ -1,4 +1,4 @@ -import React, { memo, useState, useCallback, useMemo } from 'react'; +import React, { memo, useState, useCallback, useMemo, useEffect } from 'react'; import * as Ariakit from '@ariakit/react'; import { CheckboxButton } from '@librechat/client'; import { ArtifactModes } from 'librechat-data-provider'; @@ -18,6 +18,7 @@ function Artifacts() { const { toggleState, debouncedChange, isPinned } = artifacts; const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [isButtonExpanded, setIsButtonExpanded] = useState(false); const currentState = useMemo(() => { if (typeof toggleState === 'string' && toggleState) { @@ -33,11 +34,26 @@ function Artifacts() { const handleToggle = useCallback(() => { if (isEnabled) { debouncedChange({ value: '' }); + setIsButtonExpanded(false); } else { debouncedChange({ value: ArtifactModes.DEFAULT }); } }, [isEnabled, debouncedChange]); + const handleMenuButtonClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + setIsButtonExpanded(!isButtonExpanded); + }, + [isButtonExpanded], + ); + + useEffect(() => { + if (!isPopoverOpen) { + setIsButtonExpanded(false); + } + }, [isPopoverOpen]); + const handleShadcnToggle = useCallback(() => { if (isShadcnEnabled) { debouncedChange({ value: ArtifactModes.DEFAULT }); @@ -77,21 +93,24 @@ function Artifacts() { 'border-amber-600/40 bg-amber-500/10 hover:bg-amber-700/10', 'transition-colors', )} - onClick={(e) => e.stopPropagation()} + onClick={handleMenuButtonClick} > - +
@@ -106,18 +125,16 @@ function Artifacts() { event.stopPropagation(); handleShadcnToggle(); }} - disabled={isCustomEnabled} className={cn( - 'mb-1 flex items-center justify-between rounded-lg px-2 py-2', - 'cursor-pointer outline-none transition-colors', - 'hover:bg-black/[0.075] dark:hover:bg-white/10', - 'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10', - isCustomEnabled && 'cursor-not-allowed opacity-50', + 'mb-1 flex items-center justify-between gap-2 rounded-lg px-2 py-2', + 'cursor-pointer bg-surface-secondary text-text-primary outline-none transition-colors', + 'hover:bg-surface-hover data-[active-item]:bg-surface-hover', + isShadcnEnabled && 'bg-surface-active', )} > -
+ {localize('com_ui_include_shadcnui' as any)} +
- {localize('com_ui_include_shadcnui' as any)}
@@ -130,15 +147,15 @@ function Artifacts() { handleCustomToggle(); }} className={cn( - 'flex items-center justify-between rounded-lg px-2 py-2', - 'cursor-pointer outline-none transition-colors', - 'hover:bg-black/[0.075] dark:hover:bg-white/10', - 'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10', + 'mb-1 flex items-center justify-between gap-2 rounded-lg px-2 py-2', + 'cursor-pointer bg-surface-secondary text-text-primary outline-none transition-colors', + 'hover:bg-surface-hover data-[active-item]:bg-surface-hover', + isCustomEnabled && 'bg-surface-active', )} > -
+ {localize('com_ui_custom_prompt_mode' as any)} +
- {localize('com_ui_custom_prompt_mode' as any)}
diff --git a/client/src/components/Chat/Input/ArtifactsSubMenu.tsx b/client/src/components/Chat/Input/ArtifactsSubMenu.tsx index e27fa43c0e..099a476bfa 100644 --- a/client/src/components/Chat/Input/ArtifactsSubMenu.tsx +++ b/client/src/components/Chat/Input/ArtifactsSubMenu.tsx @@ -90,8 +90,8 @@ const ArtifactsSubMenu = React.forwardRef portal={true} unmountOnHide={true} className={cn( - 'animate-popover-left z-50 ml-3 flex min-w-[250px] flex-col rounded-xl', - 'border border-border-light bg-surface-secondary px-1.5 py-1 shadow-lg', + 'animate-popover-left z-50 ml-3 mt-6 flex min-w-[250px] flex-col rounded-xl', + 'border border-border-light bg-surface-secondary shadow-lg', )} >
@@ -107,18 +107,16 @@ const ArtifactsSubMenu = React.forwardRef event.stopPropagation(); handleShadcnToggle(); }} - disabled={isCustomEnabled} className={cn( - 'mb-1 flex items-center justify-between rounded-lg px-2 py-2', - 'cursor-pointer text-text-primary outline-none transition-colors', - 'hover:bg-black/[0.075] dark:hover:bg-white/10', - 'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10', - isCustomEnabled && 'cursor-not-allowed opacity-50', + 'mb-1 flex items-center justify-between gap-2 rounded-lg px-2 py-2', + 'cursor-pointer bg-surface-secondary text-text-primary outline-none transition-colors', + 'hover:bg-surface-hover data-[active-item]:bg-surface-hover', + isShadcnEnabled && 'bg-surface-active', )} > -
+ {localize('com_ui_include_shadcnui' as any)} +
- {localize('com_ui_include_shadcnui' as any)}
@@ -131,15 +129,15 @@ const ArtifactsSubMenu = React.forwardRef handleCustomToggle(); }} className={cn( - 'flex items-center justify-between rounded-lg px-2 py-2', - 'cursor-pointer text-text-primary outline-none transition-colors', - 'hover:bg-black/[0.075] dark:hover:bg-white/10', - 'data-[active-item]:bg-black/[0.075] dark:data-[active-item]:bg-white/10', + 'mb-1 flex items-center justify-between gap-2 rounded-lg px-2 py-2', + 'cursor-pointer bg-surface-secondary text-text-primary outline-none transition-colors', + 'hover:bg-surface-hover data-[active-item]:bg-surface-hover', + isCustomEnabled && 'bg-surface-active', )} > -
+ {localize('com_ui_custom_prompt_mode' as any)} +
- {localize('com_ui_custom_prompt_mode' as any)}
diff --git a/client/src/components/Chat/Input/ChatForm.tsx b/client/src/components/Chat/Input/ChatForm.tsx index edd12bd3ac..8cccf6cf53 100644 --- a/client/src/components/Chat/Input/ChatForm.tsx +++ b/client/src/components/Chat/Input/ChatForm.tsx @@ -251,6 +251,7 @@ const ChatForm = memo(({ index = 0 }: { index?: number }) => { )} > + {/* WIP */}
diff --git a/client/src/components/Chat/Input/Files/Table/Columns.tsx b/client/src/components/Chat/Input/Files/Table/Columns.tsx index b5630c5fdc..6bd150823f 100644 --- a/client/src/components/Chat/Input/Files/Table/Columns.tsx +++ b/client/src/components/Chat/Input/Files/Table/Columns.tsx @@ -1,13 +1,7 @@ /* eslint-disable react-hooks/rules-of-hooks */ -import { ArrowUpDown, Database } from 'lucide-react'; +import { Database } from 'lucide-react'; import { FileSources, FileContext } from 'librechat-data-provider'; -import { - Button, - Checkbox, - OpenAIMinimalIcon, - AzureMinimalIcon, - useMediaQuery, -} from '@librechat/client'; +import { Checkbox, OpenAIMinimalIcon, AzureMinimalIcon, useMediaQuery } from '@librechat/client'; import type { ColumnDef } from '@tanstack/react-table'; import type { TFile } from 'librechat-data-provider'; import ImagePreview from '~/components/Chat/Input/Files/ImagePreview'; @@ -61,16 +55,7 @@ export const columns: ColumnDef[] = [ accessorKey: 'filename', header: ({ column }) => { const localize = useLocalize(); - return ( - - ); + return ; }, cell: ({ row }) => { const file = row.original; @@ -100,16 +85,7 @@ export const columns: ColumnDef[] = [ accessorKey: 'updatedAt', header: ({ column }) => { const localize = useLocalize(); - return ( - - ); + return ; }, cell: ({ row }) => { const isSmallScreen = useMediaQuery('(max-width: 768px)'); @@ -197,16 +173,7 @@ export const columns: ColumnDef[] = [ accessorKey: 'bytes', header: ({ column }) => { const localize = useLocalize(); - return ( - - ); + return ; }, cell: ({ row }) => { const suffix = ' MB'; diff --git a/client/src/components/Chat/Input/MCPSelect.tsx b/client/src/components/Chat/Input/MCPSelect.tsx index 12b6bd627e..90e22ce17c 100644 --- a/client/src/components/Chat/Input/MCPSelect.tsx +++ b/client/src/components/Chat/Input/MCPSelect.tsx @@ -5,6 +5,7 @@ import MCPServerStatusIcon from '~/components/MCP/MCPServerStatusIcon'; import MCPConfigDialog from '~/components/MCP/MCPConfigDialog'; import { useBadgeRowContext } from '~/Providers'; import { useHasAccess } from '~/hooks'; +import { cn } from '~/utils'; function MCPSelectContent() { const { conversationId, mcpServerManager } = useBadgeRowContext(); @@ -97,7 +98,10 @@ function MCPSelectContent() { className="badge-icon min-w-fit" selectIcon={} selectItemsClassName="border border-blue-600/50 bg-blue-500/10 hover:bg-blue-700/10" - selectClassName="group relative inline-flex items-center justify-center md:justify-start gap-1.5 rounded-full border border-border-medium text-sm font-medium transition-all md:w-full size-9 p-2 md:p-3 bg-transparent shadow-sm hover:bg-surface-hover hover:shadow-md active:shadow-inner" + selectClassName={cn( + 'group relative inline-flex items-center justify-center md:justify-start gap-1.5 rounded-full border border-border-medium text-sm font-medium transition-all', + 'md:w-full size-9 p-2 md:p-3 bg-transparent shadow-sm hover:bg-surface-hover hover:shadow-md active:shadow-inner', + )} /> {configDialogProps && ( diff --git a/client/src/components/Chat/Input/MCPSubMenu.tsx b/client/src/components/Chat/Input/MCPSubMenu.tsx index fc4152be5c..38e6167b65 100644 --- a/client/src/components/Chat/Input/MCPSubMenu.tsx +++ b/client/src/components/Chat/Input/MCPSubMenu.tsx @@ -108,6 +108,7 @@ const MCPSubMenu = React.forwardRef( 'w-full min-w-0 justify-between text-sm', isServerInitializing && 'opacity-50 hover:bg-transparent dark:hover:bg-transparent', + isSelected && 'bg-surface-active', )} >
diff --git a/client/src/components/Chat/Input/ToolsDropdown.tsx b/client/src/components/Chat/Input/ToolsDropdown.tsx index f1eaed06c9..7fa17b4b5b 100644 --- a/client/src/components/Chat/Input/ToolsDropdown.tsx +++ b/client/src/components/Chat/Input/ToolsDropdown.tsx @@ -312,10 +312,11 @@ const ToolsDropdown = ({ disabled }: ToolsDropdownProps) => { aria-label="Tools Options" className={cn( 'flex size-9 items-center justify-center rounded-full p-1 transition-colors hover:bg-surface-hover focus:outline-none focus:ring-2 focus:ring-primary focus:ring-opacity-50', + isPopoverActive && 'bg-surface-hover', )} >
- +
} diff --git a/client/src/components/Nav/AccountSettings.tsx b/client/src/components/Nav/AccountSettings.tsx index 6bf48e6fb2..654bf9319c 100644 --- a/client/src/components/Nav/AccountSettings.tsx +++ b/client/src/components/Nav/AccountSettings.tsx @@ -25,7 +25,7 @@ function AccountSettings() {
@@ -40,11 +40,10 @@ function AccountSettings() {
diff --git a/client/src/components/Nav/Nav.tsx b/client/src/components/Nav/Nav.tsx index 60486f96c8..16b8329483 100644 --- a/client/src/components/Nav/Nav.tsx +++ b/client/src/components/Nav/Nav.tsx @@ -1,6 +1,5 @@ import { useCallback, useEffect, useState, useMemo, memo, lazy, Suspense, useRef } from 'react'; import { useRecoilValue } from 'recoil'; -import { List } from 'react-virtualized'; import { AnimatePresence, motion } from 'framer-motion'; import { Skeleton, useMediaQuery } from '@librechat/client'; import { PermissionTypes, Permissions } from 'librechat-data-provider'; @@ -244,7 +243,7 @@ const Nav = memo( />
- + }> diff --git a/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx b/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx index 816c5a2deb..6334fb363c 100644 --- a/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx +++ b/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx @@ -118,11 +118,16 @@ function ImportConversations() { aria-labelledby="import-conversation-label" > {isUploading ? ( - + <> + + {localize('com_ui_importing')} + ) : ( - + <> + + {localize('com_ui_import')} + )} - {localize('com_ui_import')} v === 'createdAt' || v === 'title'; + +const defaultSort: SortingState = [ + { + id: 'createdAt', + desc: true, + }, +]; + +type TableColumn = ColumnDef & { + meta?: { + className?: string; + desktopOnly?: boolean; + }; +}; + 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 [isDeleteOpen, setIsDeleteOpen] = useState(false); + const [deleteRow, setDeleteRow] = useState(null); + + const [queryParams, setQueryParams] = useState(DEFAULT_PARAMS); + const [sorting, setSorting] = useState(defaultSort); + const [searchValue, setSearchValue] = useState(''); const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch, isLoading } = useSharedLinksQuery(queryParams, { enabled: isOpen, - staleTime: 0, - cacheTime: 5 * 60 * 1000, + keepPreviousData: true, + staleTime: 30 * 1000, refetchOnWindowFocus: false, refetchOnMount: false, }); - const handleSort = useCallback((sortField: string, sortOrder: 'asc' | 'desc') => { + const [allKnownLinks, setAllKnownLinks] = useState([]); + + const handleSearchChange = useCallback((value: string) => { + setSearchValue(value); + setAllKnownLinks([]); setQueryParams((prev) => ({ ...prev, - sortBy: sortField as 'title' | 'createdAt', - sortDirection: sortOrder, + search: value, })); }, []); - const handleFilterChange = useCallback((value: string) => { - const encodedValue = encodeURIComponent(value.trim()); - setQueryParams((prev) => ({ - ...prev, - search: encodedValue, - })); - }, []); + const handleSortingChange = useCallback( + (updater: SortingState | ((old: SortingState) => SortingState)) => { + setSorting((prev) => { + const next = typeof updater === 'function' ? updater(prev) : updater; - const debouncedFilterChange = useMemo( - () => debounce(handleFilterChange, 300), - [handleFilterChange], + const coerced = next; + const primary = coerced[0]; + + if (data?.pages) { + const currentFlattened = data.pages.flatMap((page) => page?.links?.filter(Boolean) ?? []); + setAllKnownLinks(currentFlattened); + } + + setQueryParams((p) => { + let sortBy: SortKey; + let sortDirection: 'asc' | 'desc'; + + if (primary && isSortKey(primary.id)) { + sortBy = primary.id; + sortDirection = primary.desc ? 'desc' : 'asc'; + } else { + sortBy = 'createdAt'; + sortDirection = 'desc'; + } + + const newParams = { + ...p, + sortBy, + sortDirection, + }; + + return newParams; + }); + + return coerced; + }); + }, + [setQueryParams, data?.pages], ); useEffect(() => { - return () => { - debouncedFilterChange.cancel(); - }; - }, [debouncedFilterChange]); + if (!data?.pages) return; - const allLinks = useMemo(() => { - if (!data?.pages) { - return []; + const newFlattened = data.pages.flatMap((page) => page?.links?.filter(Boolean) ?? []); + + const toAdd = newFlattened.filter( + (link: SharedLinkItem) => !allKnownLinks.some((known) => known.shareId === link.shareId), + ); + + if (toAdd.length > 0) { + setAllKnownLinks((prev) => [...prev, ...toAdd]); } - - return data.pages.flatMap((page) => page.links.filter(Boolean)); }, [data?.pages]); + const displayData = useMemo(() => { + const primary = sorting[0]; + if (!primary || allKnownLinks.length === 0) return allKnownLinks; + + return [...allKnownLinks].sort((a: SharedLinkItem, b: SharedLinkItem) => { + let compare: number; + if (primary.id === 'createdAt') { + const aDate = new Date(a.createdAt || 0); + const bDate = new Date(b.createdAt || 0); + compare = aDate.getTime() - bDate.getTime(); + } else if (primary.id === 'title') { + compare = (a.title || '').localeCompare(b.title || ''); + } else { + return 0; + } + return primary.desc ? -compare : compare; + }); + }, [allKnownLinks, sorting]); + const deleteMutation = useDeleteSharedLinkMutation({ - onSuccess: async () => { + onSuccess: (data, variables) => { + const { shareId } = variables; + setAllKnownLinks((prev) => prev.filter((link) => link.shareId !== shareId)); + showToast({ + message: localize('com_ui_shared_link_delete_success'), + severity: NotificationSeverity.SUCCESS, + }); setIsDeleteOpen(false); - setDeleteRow(null); - await refetch(); + refetch(); }, - onError: (error) => { - console.error('Delete error:', error); + onError: () => { showToast({ message: localize('com_ui_share_delete_error'), severity: NotificationSeverity.ERROR, @@ -111,87 +173,39 @@ export default function SharedLinks() { }, }); - 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; - } + if (!hasNextPage || isFetchingNextPage) return; await fetchNextPage(); }, [fetchNextPage, hasNextPage, isFetchingNextPage]); - const confirmDelete = useCallback(() => { - if (deleteRow) { - handleDelete([deleteRow]); - } - setIsDeleteOpen(false); - }, [deleteRow, handleDelete]); + const effectiveIsLoading = isLoading && displayData.length === 0; + const effectiveIsFetching = isFetchingNextPage; - const columns = useMemo( + const confirmDelete = useCallback(() => { + if (!deleteRow?.shareId) { + showToast({ + message: localize('com_ui_share_delete_error'), + severity: NotificationSeverity.WARNING, + }); + return; + } + deleteMutation.mutate({ shareId: deleteRow.shareId }); + }, [deleteMutation, deleteRow, localize, showToast]); + + const columns: TableColumn, unknown>[] = useMemo( () => [ { accessorKey: 'title', - header: () => { - const isSorted = queryParams.sortBy === 'title'; - const sortDirection = queryParams.sortDirection; - return ( - - ); + accessorFn: (row: Record): unknown => { + const link = row as SharedLinkItem; + return link.title; }, + header: () => ( + {localize('com_ui_name')} + ), cell: ({ row }) => { - const { title, shareId } = row.original; + const link = row.original as SharedLinkItem; + const { title, shareId } = link; return (
{title} { - const isSorted = queryParams.sortBy === 'createdAt'; - const sortDirection = queryParams.sortDirection; - return ( - - ); + accessorFn: (row: Record): unknown => { + const link = row as SharedLinkItem; + return link.createdAt; + }, + header: () => ( + {localize('com_ui_date')} + ), + cell: ({ row }) => { + const link = row.original as SharedLinkItem; + return formatDate(link.createdAt?.toString() ?? '', isSmallScreen); }, - cell: ({ row }) => formatDate(row.original.createdAt?.toString() ?? '', isSmallScreen), meta: { - size: '10%', - mobileSize: '20%', + className: 'w-32 sm:w-40', + desktopOnly: true, }, + enableSorting: true, }, { - accessorKey: 'actions', + id: 'actions', + accessorFn: (row: Record): unknown => null, header: () => ( - + ), - meta: { - size: '7%', - mobileSize: '25%', + cell: ({ row }) => { + const link = row.original as SharedLinkItem; + const { title, conversationId } = link; + + return ( +
+ { + window.open(`/c/${conversationId}`, '_blank'); + }} + aria-label={localize('com_ui_view_source_conversation', { 0: title })} + > + + + } + /> + { + setDeleteRow(link); + setIsDeleteOpen(true); + }} + aria-label={localize('com_ui_delete_link_title', { 0: title })} + > + + + } + /> +
+ ); }, - cell: ({ row }) => ( -
- - - -
- ), + meta: { + className: 'w-24', + }, + enableSorting: false, }, ], - [isSmallScreen, localize, queryParams, handleSort], + [isSmallScreen, localize], ); return (
- - setIsOpen(true)}> + - - + {localize('com_nav_shared_links')} -