mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
* chore: Remove @rollup/plugin-terser from package.json and rollup.config.js * 🔧 chore: Revert Shared Links / Data-Table Changes from #9698 - Added sorting functionality for title and creation date in the Shared Links table. - Implemented debounced search filtering for improved performance. - Updated DataTable integration to support new features and improved user experience. - Refactored delete handling to support bulk deletion of shared links. - Adjusted dialog components for better accessibility and user feedback. * 🗑️ chore: Remove unused translation keys from English locale - Deleted keys related to managing archived chats, deleting archived chats, and opening conversations to streamline the translation file.
This commit is contained in:
parent
a5eff768e3
commit
b8a149e563
8 changed files with 564 additions and 642 deletions
|
|
@ -1,6 +1,17 @@
|
|||
import { useCallback, useState, useMemo, useEffect } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { TrashIcon, MessageSquare, ExternalLink } from 'lucide-react';
|
||||
import {
|
||||
TrashIcon,
|
||||
MessageSquare,
|
||||
ArrowUpDown,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
ExternalLink,
|
||||
} from 'lucide-react';
|
||||
import type { SharedLinkItem, SharedLinksListParams } from 'librechat-data-provider';
|
||||
import {
|
||||
OGDialog,
|
||||
useToastContext,
|
||||
|
|
@ -10,162 +21,89 @@ import {
|
|||
useMediaQuery,
|
||||
OGDialogHeader,
|
||||
OGDialogTitle,
|
||||
TooltipAnchor,
|
||||
DataTable,
|
||||
Spinner,
|
||||
Button,
|
||||
Label,
|
||||
} from '@librechat/client';
|
||||
import type { SharedLinkItem, SharedLinksListParams } from 'librechat-data-provider';
|
||||
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import { useDeleteSharedLinkMutation, useSharedLinksQuery } from '~/data-provider';
|
||||
import { NotificationSeverity } from '~/common';
|
||||
import { formatDate, cn } from '~/utils';
|
||||
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: 25,
|
||||
pageSize: PAGE_SIZE,
|
||||
isPublic: true,
|
||||
sortBy: 'createdAt',
|
||||
sortDirection: 'desc',
|
||||
search: '',
|
||||
};
|
||||
|
||||
type SortKey = 'createdAt' | 'title';
|
||||
const isSortKey = (v: string): v is SortKey => v === 'createdAt' || v === 'title';
|
||||
|
||||
const defaultSort: SortingState = [
|
||||
{
|
||||
id: 'createdAt',
|
||||
desc: true,
|
||||
},
|
||||
];
|
||||
|
||||
type TableColumn<TData, TValue> = ColumnDef<TData, TValue> & {
|
||||
meta?: {
|
||||
className?: string;
|
||||
desktopOnly?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export default function SharedLinks() {
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||
const [deleteRow, setDeleteRow] = useState<SharedLinkItem | null>(null);
|
||||
|
||||
const isSearchEnabled = useRecoilValue(store.search);
|
||||
const [queryParams, setQueryParams] = useState<SharedLinksListParams>(DEFAULT_PARAMS);
|
||||
const [sorting, setSorting] = useState<SortingState>(defaultSort);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [deleteRow, setDeleteRow] = useState<SharedLinkItem | null>(null);
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch, isLoading } =
|
||||
useSharedLinksQuery(queryParams, {
|
||||
enabled: isOpen,
|
||||
keepPreviousData: true,
|
||||
staleTime: 30 * 1000,
|
||||
staleTime: 0,
|
||||
cacheTime: 5 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
const [allKnownLinks, setAllKnownLinks] = useState<SharedLinkItem[]>([]);
|
||||
|
||||
const handleSearchChange = useCallback((value: string) => {
|
||||
setSearchValue(value);
|
||||
setAllKnownLinks([]);
|
||||
const handleSort = useCallback((sortField: string, sortOrder: 'asc' | 'desc') => {
|
||||
setQueryParams((prev) => ({
|
||||
...prev,
|
||||
search: value,
|
||||
sortBy: sortField as 'title' | 'createdAt',
|
||||
sortDirection: sortOrder,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleSortingChange = useCallback(
|
||||
(updater: SortingState | ((old: SortingState) => SortingState)) => {
|
||||
setSorting((prev) => {
|
||||
const next = typeof updater === 'function' ? updater(prev) : updater;
|
||||
const handleFilterChange = useCallback((value: string) => {
|
||||
const encodedValue = encodeURIComponent(value.trim());
|
||||
setQueryParams((prev) => ({
|
||||
...prev,
|
||||
search: encodedValue,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
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],
|
||||
const debouncedFilterChange = useMemo(
|
||||
() => debounce(handleFilterChange, 300),
|
||||
[handleFilterChange],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data?.pages) return;
|
||||
return () => {
|
||||
debouncedFilterChange.cancel();
|
||||
};
|
||||
}, [debouncedFilterChange]);
|
||||
|
||||
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]);
|
||||
const allLinks = useMemo(() => {
|
||||
if (!data?.pages) {
|
||||
return [];
|
||||
}
|
||||
|
||||
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: (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,
|
||||
});
|
||||
onSuccess: async () => {
|
||||
setIsDeleteOpen(false);
|
||||
refetch();
|
||||
setDeleteRow(null);
|
||||
await refetch();
|
||||
},
|
||||
onError: () => {
|
||||
onError: (error) => {
|
||||
console.error('Delete error:', error);
|
||||
showToast({
|
||||
message: localize('com_ui_share_delete_error'),
|
||||
severity: NotificationSeverity.ERROR,
|
||||
|
|
@ -173,39 +111,87 @@ 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 || isFetchingNextPage) return;
|
||||
if (hasNextPage !== true || isFetchingNextPage) {
|
||||
return;
|
||||
}
|
||||
await fetchNextPage();
|
||||
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
|
||||
|
||||
const effectiveIsLoading = isLoading && displayData.length === 0;
|
||||
const effectiveIsFetching = isFetchingNextPage;
|
||||
|
||||
const confirmDelete = useCallback(() => {
|
||||
if (!deleteRow?.shareId) {
|
||||
showToast({
|
||||
message: localize('com_ui_share_delete_error'),
|
||||
severity: NotificationSeverity.WARNING,
|
||||
});
|
||||
return;
|
||||
if (deleteRow) {
|
||||
handleDelete([deleteRow]);
|
||||
}
|
||||
deleteMutation.mutate({ shareId: deleteRow.shareId });
|
||||
}, [deleteMutation, deleteRow, localize, showToast]);
|
||||
setIsDeleteOpen(false);
|
||||
}, [deleteRow, handleDelete]);
|
||||
|
||||
const columns: TableColumn<Record<string, unknown>, unknown>[] = useMemo(
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
accessorKey: 'title',
|
||||
accessorFn: (row: Record<string, unknown>): unknown => {
|
||||
const link = row as SharedLinkItem;
|
||||
return link.title;
|
||||
header: () => {
|
||||
const isSorted = queryParams.sortBy === 'title';
|
||||
const sortDirection = queryParams.sortDirection;
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm"
|
||||
onClick={() =>
|
||||
handleSort('title', isSorted && sortDirection === 'asc' ? 'desc' : 'asc')
|
||||
}
|
||||
aria-label={localize('com_ui_name_sort')}
|
||||
>
|
||||
{localize('com_ui_name')}
|
||||
{isSorted && sortDirection === 'asc' && (
|
||||
<ArrowUp className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
|
||||
)}
|
||||
{isSorted && sortDirection === 'desc' && (
|
||||
<ArrowDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
|
||||
)}
|
||||
{!isSorted && <ArrowUpDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
header: () => (
|
||||
<span className="text-xs text-text-primary sm:text-sm">{localize('com_ui_name')}</span>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const link = row.original as SharedLinkItem;
|
||||
const { title, shareId } = link;
|
||||
const { title, shareId } = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
|
|
@ -213,7 +199,7 @@ export default function SharedLinks() {
|
|||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group flex items-center gap-1 truncate rounded-sm text-blue-600 underline decoration-1 underline-offset-2 hover:decoration-2 focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
aria-label={localize('com_ui_open_link', { 0: title })}
|
||||
title={title}
|
||||
>
|
||||
<span className="truncate">{title}</span>
|
||||
<ExternalLink
|
||||
|
|
@ -225,139 +211,141 @@ export default function SharedLinks() {
|
|||
);
|
||||
},
|
||||
meta: {
|
||||
className: 'min-w-[150px] flex-1',
|
||||
size: '35%',
|
||||
mobileSize: '50%',
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: 'createdAt',
|
||||
accessorFn: (row: Record<string, unknown>): unknown => {
|
||||
const link = row as SharedLinkItem;
|
||||
return link.createdAt;
|
||||
},
|
||||
header: () => (
|
||||
<span className="text-xs text-text-primary sm:text-sm">{localize('com_ui_date')}</span>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const link = row.original as SharedLinkItem;
|
||||
return formatDate(link.createdAt?.toString() ?? '', isSmallScreen);
|
||||
},
|
||||
meta: {
|
||||
className: 'w-32 sm:w-40',
|
||||
desktopOnly: true,
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
accessorFn: (row: Record<string, unknown>): unknown => null,
|
||||
header: () => (
|
||||
<span className="text-xs text-text-primary sm:text-sm">
|
||||
{localize('com_assistants_actions')}
|
||||
</span>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const link = row.original as SharedLinkItem;
|
||||
const { title, conversationId } = link;
|
||||
|
||||
header: () => {
|
||||
const isSorted = queryParams.sortBy === 'createdAt';
|
||||
const sortDirection = queryParams.sortDirection;
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_view_source')}
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0 hover:bg-surface-hover"
|
||||
onClick={() => {
|
||||
window.open(`/c/${conversationId}`, '_blank');
|
||||
}}
|
||||
aria-label={localize('com_ui_view_source_conversation', { 0: title })}
|
||||
>
|
||||
<MessageSquare className="size-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_delete')}
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => {
|
||||
setDeleteRow(link);
|
||||
setIsDeleteOpen(true);
|
||||
}}
|
||||
aria-label={localize('com_ui_delete_link_title', { 0: title })}
|
||||
>
|
||||
<TrashIcon className="size-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm"
|
||||
onClick={() =>
|
||||
handleSort('createdAt', isSorted && sortDirection === 'asc' ? 'desc' : 'asc')
|
||||
}
|
||||
aria-label={localize('com_ui_creation_date_sort')}
|
||||
>
|
||||
{localize('com_ui_date')}
|
||||
{isSorted && sortDirection === 'asc' && (
|
||||
<ArrowUp className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
|
||||
)}
|
||||
{isSorted && sortDirection === 'desc' && (
|
||||
<ArrowDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
|
||||
)}
|
||||
{!isSorted && <ArrowUpDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => formatDate(row.original.createdAt?.toString() ?? '', isSmallScreen),
|
||||
meta: {
|
||||
className: 'w-24',
|
||||
size: '10%',
|
||||
mobileSize: '20%',
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: 'actions',
|
||||
header: () => (
|
||||
<Label className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm">
|
||||
{localize('com_assistants_actions')}
|
||||
</Label>
|
||||
),
|
||||
meta: {
|
||||
size: '7%',
|
||||
mobileSize: '25%',
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<a
|
||||
href={`/c/${row.original.conversationId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-md p-0 transition-colors hover:bg-surface-hover focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
aria-label={`${localize('com_ui_view_source')} - ${row.original.title || localize('com_ui_untitled')}`}
|
||||
>
|
||||
<MessageSquare className="size-4" aria-hidden="true" />
|
||||
</a>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0 hover:bg-surface-hover"
|
||||
onClick={() => {
|
||||
setDeleteRow(row.original);
|
||||
setIsDeleteOpen(true);
|
||||
}}
|
||||
aria-label={localize('com_ui_delete_shared_link', {
|
||||
title: row.original.title || localize('com_ui_untitled'),
|
||||
})}
|
||||
aria-haspopup="dialog"
|
||||
aria-controls="delete-shared-link-dialog"
|
||||
>
|
||||
<TrashIcon className="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
[isSmallScreen, localize],
|
||||
[isSmallScreen, localize, queryParams, handleSort],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<Label id="shared-links-label">{localize('com_nav_shared_links')}</Label>
|
||||
|
||||
<OGDialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<OGDialogTrigger asChild>
|
||||
<OGDialogTrigger asChild onClick={() => setIsOpen(true)}>
|
||||
<Button aria-labelledby="shared-links-label" variant="outline">
|
||||
{localize('com_ui_manage')}
|
||||
</Button>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogContent className={cn('w-11/12 max-w-6xl', isSmallScreen && 'px-1 pb-1')}>
|
||||
|
||||
<OGDialogContent
|
||||
title={localize('com_nav_my_files')}
|
||||
className="w-11/12 max-w-5xl bg-background text-text-primary shadow-2xl"
|
||||
>
|
||||
<OGDialogHeader>
|
||||
<OGDialogTitle>{localize('com_nav_shared_links')}</OGDialogTitle>
|
||||
</OGDialogHeader>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={displayData}
|
||||
isLoading={effectiveIsLoading}
|
||||
isFetching={effectiveIsFetching}
|
||||
config={{
|
||||
skeleton: { count: 11 },
|
||||
search: {
|
||||
filterColumn: 'title',
|
||||
enableSearch: true,
|
||||
debounce: 300,
|
||||
},
|
||||
selection: {
|
||||
enableRowSelection: false,
|
||||
showCheckboxes: false,
|
||||
},
|
||||
}}
|
||||
filterValue={searchValue}
|
||||
onFilterChange={handleSearchChange}
|
||||
fetchNextPage={handleFetchNextPage}
|
||||
data={allLinks}
|
||||
onDelete={handleDelete}
|
||||
filterColumn="title"
|
||||
hasNextPage={hasNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
sorting={sorting}
|
||||
onSortingChange={handleSortingChange}
|
||||
fetchNextPage={handleFetchNextPage}
|
||||
showCheckboxes={false}
|
||||
onFilterChange={debouncedFilterChange}
|
||||
filterValue={queryParams.search}
|
||||
isLoading={isLoading}
|
||||
enableSearch={isSearchEnabled}
|
||||
/>
|
||||
</OGDialogContent>
|
||||
</OGDialog>
|
||||
<OGDialog open={isDeleteOpen} onOpenChange={setIsDeleteOpen}>
|
||||
<OGDialogTemplate
|
||||
showCloseButton={false}
|
||||
title={localize('com_ui_delete_shared_link')}
|
||||
className="w-11/12 max-w-md"
|
||||
title={localize('com_ui_delete_shared_link_heading')}
|
||||
className="max-w-[450px]"
|
||||
main={
|
||||
<div className="flex w-full flex-col items-center gap-2">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label className="text-left text-sm font-medium">
|
||||
{localize('com_ui_delete_confirm')} <strong>{deleteRow?.title}</strong>
|
||||
</Label>
|
||||
<>
|
||||
<div
|
||||
id="delete-shared-link-dialog"
|
||||
className="flex w-full flex-col items-center gap-2"
|
||||
>
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label htmlFor="dialog-confirm-delete" className="text-left text-sm font-medium">
|
||||
<Trans
|
||||
i18nKey="com_ui_delete_confirm_strong"
|
||||
values={{ title: deleteRow?.title }}
|
||||
components={{ strong: <strong /> }}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
selection={{
|
||||
selectHandler: confirmDelete,
|
||||
|
|
|
|||
|
|
@ -1,406 +1,26 @@
|
|||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { QueryKeys } from 'librechat-data-provider';
|
||||
import { TrashIcon, ArchiveRestore } from 'lucide-react';
|
||||
import { useQueryClient, InfiniteData } from '@tanstack/react-query';
|
||||
import {
|
||||
Button,
|
||||
OGDialog,
|
||||
OGDialogTrigger,
|
||||
OGDialogTemplate,
|
||||
OGDialogContent,
|
||||
OGDialogHeader,
|
||||
OGDialogTitle,
|
||||
Label,
|
||||
TooltipAnchor,
|
||||
Spinner,
|
||||
useToastContext,
|
||||
useMediaQuery,
|
||||
DataTable,
|
||||
type TableColumn,
|
||||
} from '@librechat/client';
|
||||
import type { ConversationListParams, TConversation } from 'librechat-data-provider';
|
||||
import type { SortingState } from '@tanstack/react-table';
|
||||
import {
|
||||
useArchiveConvoMutation,
|
||||
useConversationsInfiniteQuery,
|
||||
useDeleteConversationMutation,
|
||||
} from '~/data-provider';
|
||||
import { MinimalIcon } from '~/components/Endpoints';
|
||||
import { NotificationSeverity } from '~/common';
|
||||
import { formatDate, cn } from '~/utils';
|
||||
import { useState } from 'react';
|
||||
import { OGDialogTemplate, OGDialog, OGDialogTrigger, Button } from '@librechat/client';
|
||||
import ArchivedChatsTable from './ArchivedChatsTable';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
isArchived: true,
|
||||
sortBy: 'createdAt',
|
||||
sortDirection: 'desc',
|
||||
search: '',
|
||||
} as const satisfies ConversationListParams;
|
||||
|
||||
type SortKey = 'createdAt' | 'title';
|
||||
const isSortKey = (v: string): v is SortKey => v === 'createdAt' || v === 'title';
|
||||
|
||||
const defaultSort: SortingState = [
|
||||
{
|
||||
id: 'createdAt',
|
||||
desc: true,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Helper: remove a conversation from all infinite queries whose key starts with the provided root
|
||||
*/
|
||||
function removeConversationFromInfinite(
|
||||
queryClient: ReturnType<typeof useQueryClient>,
|
||||
rootKey: string,
|
||||
conversationId: string,
|
||||
) {
|
||||
const queries = queryClient.getQueryCache().findAll([rootKey], { exact: false });
|
||||
for (const query of queries) {
|
||||
queryClient.setQueryData<
|
||||
InfiniteData<{ conversations: TConversation[]; nextCursor?: string | null }>
|
||||
>(query.queryKey, (old) => {
|
||||
if (!old) return old;
|
||||
return {
|
||||
...old,
|
||||
pages: old.pages.map((page) => ({
|
||||
...page,
|
||||
conversations: page.conversations.filter((c) => c.conversationId !== conversationId),
|
||||
})),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default function ArchivedChatsTable() {
|
||||
export default function ArchivedChats() {
|
||||
const localize = useLocalize();
|
||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||
const { showToast } = useToastContext();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||
const [deleteRow, setDeleteRow] = useState<TConversation | null>(null);
|
||||
const [unarchivingId, setUnarchivingId] = useState<string | null>(null);
|
||||
|
||||
const [queryParams, setQueryParams] = useState<ConversationListParams>(DEFAULT_PARAMS);
|
||||
const [sorting, setSorting] = useState<SortingState>(defaultSort);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } =
|
||||
useConversationsInfiniteQuery(queryParams, {
|
||||
enabled: isOpen,
|
||||
keepPreviousData: false,
|
||||
staleTime: 30 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
const handleSearchChange = useCallback((value: string) => {
|
||||
setSearchValue(value);
|
||||
setQueryParams((prev) => ({
|
||||
...prev,
|
||||
search: value,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleSortingChange = useCallback(
|
||||
(updater: SortingState | ((old: SortingState) => SortingState)) => {
|
||||
setSorting((prev) => {
|
||||
const next = typeof updater === 'function' ? updater(prev) : updater;
|
||||
const primary = next[0];
|
||||
setQueryParams((p) => {
|
||||
let sortBy: SortKey = 'createdAt';
|
||||
let sortDirection: 'asc' | 'desc' = 'desc';
|
||||
if (primary && isSortKey(primary.id)) {
|
||||
sortBy = primary.id;
|
||||
sortDirection = primary.desc ? 'desc' : 'asc';
|
||||
}
|
||||
return {
|
||||
...p,
|
||||
sortBy,
|
||||
sortDirection,
|
||||
};
|
||||
});
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const flattenedConversations = useMemo(
|
||||
() => data?.pages?.flatMap((page) => page?.conversations?.filter(Boolean) ?? []) ?? [],
|
||||
[data?.pages],
|
||||
);
|
||||
|
||||
const unarchiveMutation = useArchiveConvoMutation({
|
||||
onSuccess: (_res, variables) => {
|
||||
const { conversationId } = variables;
|
||||
if (conversationId) {
|
||||
removeConversationFromInfinite(
|
||||
queryClient,
|
||||
QueryKeys.archivedConversations,
|
||||
conversationId,
|
||||
);
|
||||
}
|
||||
queryClient.invalidateQueries([QueryKeys.allConversations]);
|
||||
setUnarchivingId(null);
|
||||
},
|
||||
onError: () => {
|
||||
showToast({
|
||||
message: localize('com_ui_unarchive_error'),
|
||||
severity: NotificationSeverity.ERROR,
|
||||
});
|
||||
setUnarchivingId(null);
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useDeleteConversationMutation({
|
||||
onSuccess: (_data, variables) => {
|
||||
const { conversationId } = variables;
|
||||
if (conversationId) {
|
||||
removeConversationFromInfinite(
|
||||
queryClient,
|
||||
QueryKeys.archivedConversations,
|
||||
conversationId,
|
||||
);
|
||||
}
|
||||
showToast({
|
||||
message: localize('com_ui_archived_conversation_delete_success'),
|
||||
severity: NotificationSeverity.SUCCESS,
|
||||
});
|
||||
setIsDeleteOpen(false);
|
||||
},
|
||||
onError: () => {
|
||||
showToast({
|
||||
message: localize('com_ui_archive_delete_error'),
|
||||
severity: NotificationSeverity.ERROR,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleFetchNextPage = useCallback(async () => {
|
||||
if (!hasNextPage || isFetchingNextPage) return;
|
||||
await fetchNextPage();
|
||||
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
|
||||
|
||||
const effectiveIsLoading = isLoading;
|
||||
const effectiveIsFetching = isFetchingNextPage;
|
||||
|
||||
const confirmDelete = useCallback(() => {
|
||||
if (!deleteRow?.conversationId) {
|
||||
showToast({
|
||||
message: localize('com_ui_convo_delete_error'),
|
||||
severity: NotificationSeverity.WARNING,
|
||||
});
|
||||
return;
|
||||
}
|
||||
deleteMutation.mutate({ conversationId: deleteRow.conversationId });
|
||||
}, [deleteMutation, deleteRow, localize, showToast]);
|
||||
|
||||
const handleUnarchive = useCallback(
|
||||
(conversationId: string) => {
|
||||
setUnarchivingId(conversationId);
|
||||
unarchiveMutation.mutate(
|
||||
{ conversationId, isArchived: false },
|
||||
{ onSettled: () => setUnarchivingId(null) },
|
||||
);
|
||||
},
|
||||
[unarchiveMutation],
|
||||
);
|
||||
|
||||
const columns: TableColumn<Record<string, unknown>, unknown>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
accessorKey: 'title',
|
||||
accessorFn: (row: Record<string, unknown>): unknown => {
|
||||
const convo = row as TConversation;
|
||||
return convo.title;
|
||||
},
|
||||
header: () => (
|
||||
<span className="text-xs text-text-primary sm:text-sm">
|
||||
{localize('com_nav_archive_name')}
|
||||
</span>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const convo = row.original as TConversation;
|
||||
const { conversationId, title } = convo;
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<MinimalIcon
|
||||
endpoint={convo.endpoint}
|
||||
size={28}
|
||||
isCreatedByUser={false}
|
||||
iconClassName="size-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<a
|
||||
href={`/c/${conversationId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center truncate underline"
|
||||
aria-label={localize('com_ui_open_conversation', { 0: title })}
|
||||
>
|
||||
{title}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
className: 'min-w-[150px] flex-1',
|
||||
isRowHeader: true,
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: 'createdAt',
|
||||
accessorFn: (row: Record<string, unknown>): unknown => {
|
||||
const convo = row as TConversation;
|
||||
return convo.createdAt;
|
||||
},
|
||||
header: () => (
|
||||
<span className="text-xs text-text-primary sm:text-sm">
|
||||
{localize('com_nav_archive_created_at')}
|
||||
</span>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const convo = row.original as TConversation;
|
||||
return formatDate(convo.createdAt?.toString() ?? '', isSmallScreen);
|
||||
},
|
||||
meta: {
|
||||
className: 'w-32 sm:w-40',
|
||||
desktopOnly: true,
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
accessorFn: () => null,
|
||||
header: () => (
|
||||
<span className="text-xs text-text-primary sm:text-sm">
|
||||
{localize('com_assistants_actions')}
|
||||
</span>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const convo = row.original as TConversation;
|
||||
const { title } = convo;
|
||||
const isRowUnarchiving = unarchivingId === convo.conversationId;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 md:gap-2">
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_unarchive')}
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-9 w-9 p-0 hover:bg-surface-hover md:h-8 md:w-8"
|
||||
onClick={() => {
|
||||
const conversationId = convo.conversationId;
|
||||
if (!conversationId) return;
|
||||
handleUnarchive(conversationId);
|
||||
}}
|
||||
disabled={isRowUnarchiving}
|
||||
aria-label={localize('com_ui_unarchive_conversation_title', { 0: title })}
|
||||
>
|
||||
{isRowUnarchiving ? <Spinner /> : <ArchiveRestore className="size-4" />}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_delete')}
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-9 w-9 p-0 md:h-8 md:w-8"
|
||||
onClick={() => {
|
||||
setDeleteRow(convo);
|
||||
setIsDeleteOpen(true);
|
||||
}}
|
||||
aria-label={localize('com_ui_delete_conversation_title', { 0: title })}
|
||||
>
|
||||
<TrashIcon className="size-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
className: 'w-24',
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
],
|
||||
[isSmallScreen, localize, handleUnarchive, unarchivingId],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="archived-chats-button" className="text-sm font-medium">
|
||||
{localize('com_nav_archived_chats')}
|
||||
</Label>
|
||||
<div>{localize('com_nav_archived_chats')}</div>
|
||||
<OGDialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<OGDialogTrigger asChild>
|
||||
<Button
|
||||
id="archived-chats-button"
|
||||
variant="outline"
|
||||
aria-label={localize('com_ui_manage_archived_chats')}
|
||||
>
|
||||
<Button variant="outline" aria-label="Archived chats">
|
||||
{localize('com_ui_manage')}
|
||||
</Button>
|
||||
</OGDialogTrigger>
|
||||
<OGDialogContent className={cn('w-11/12 max-w-6xl', isSmallScreen && 'px-1 pb-1')}>
|
||||
<OGDialogHeader>
|
||||
<OGDialogTitle>{localize('com_nav_archived_chats')}</OGDialogTitle>
|
||||
</OGDialogHeader>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={flattenedConversations}
|
||||
isLoading={effectiveIsLoading}
|
||||
isFetching={effectiveIsFetching}
|
||||
config={{
|
||||
skeleton: { count: 11 },
|
||||
search: {
|
||||
filterColumn: 'title',
|
||||
enableSearch: true,
|
||||
debounce: 300,
|
||||
},
|
||||
selection: {
|
||||
enableRowSelection: false,
|
||||
showCheckboxes: false,
|
||||
},
|
||||
}}
|
||||
filterValue={searchValue}
|
||||
onFilterChange={handleSearchChange}
|
||||
fetchNextPage={handleFetchNextPage}
|
||||
hasNextPage={hasNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
sorting={sorting}
|
||||
onSortingChange={handleSortingChange}
|
||||
/>
|
||||
</OGDialogContent>
|
||||
</OGDialog>
|
||||
<OGDialog open={isDeleteOpen} onOpenChange={setIsDeleteOpen}>
|
||||
<OGDialogTemplate
|
||||
showCloseButton={false}
|
||||
title={localize('com_ui_delete_archived_chats')}
|
||||
className="w-11/12 max-w-md"
|
||||
main={
|
||||
<div className="flex w-full flex-col items-center gap-2">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label className="text-left text-sm font-medium">
|
||||
{localize('com_ui_delete_confirm')} <strong>{deleteRow?.title}</strong>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
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 ? <Spinner /> : localize('com_ui_delete'),
|
||||
}}
|
||||
title={localize('com_nav_archived_chats')}
|
||||
className="max-w-[1000px]"
|
||||
showCancelButton={false}
|
||||
main={<ArchivedChatsTable isOpen={isOpen} onOpenChange={setIsOpen} />}
|
||||
/>
|
||||
</OGDialog>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,328 @@
|
|||
import { useState, useCallback, useMemo, useEffect } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { TrashIcon, ArchiveRestore, ArrowUp, ArrowDown, ArrowUpDown } from 'lucide-react';
|
||||
import {
|
||||
Button,
|
||||
Label,
|
||||
Spinner,
|
||||
OGDialog,
|
||||
DataTable,
|
||||
TooltipAnchor,
|
||||
useMediaQuery,
|
||||
OGDialogTitle,
|
||||
OGDialogHeader,
|
||||
useToastContext,
|
||||
OGDialogContent,
|
||||
} from '@librechat/client';
|
||||
import type { ConversationListParams, TConversation } from 'librechat-data-provider';
|
||||
import {
|
||||
useConversationsInfiniteQuery,
|
||||
useDeleteConversationMutation,
|
||||
useArchiveConvoMutation,
|
||||
} from '~/data-provider';
|
||||
import { MinimalIcon } from '~/components/Endpoints';
|
||||
import { NotificationSeverity } from '~/common';
|
||||
import { formatDate, logger } from '~/utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
||||
const DEFAULT_PARAMS: ConversationListParams = {
|
||||
isArchived: true,
|
||||
sortBy: 'createdAt',
|
||||
sortDirection: 'desc',
|
||||
search: '',
|
||||
};
|
||||
|
||||
export default function ArchivedChatsTable({
|
||||
onOpenChange,
|
||||
}: {
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||
const { showToast } = useToastContext();
|
||||
const searchState = useRecoilValue(store.search);
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||
const [queryParams, setQueryParams] = useState<ConversationListParams>(DEFAULT_PARAMS);
|
||||
const [deleteConversation, setDeleteConversation] = useState<TConversation | null>(null);
|
||||
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch, isLoading } =
|
||||
useConversationsInfiniteQuery(queryParams, {
|
||||
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 allConversations = useMemo(() => {
|
||||
if (!data?.pages) {
|
||||
return [];
|
||||
}
|
||||
return data.pages.flatMap((page) => page?.conversations?.filter(Boolean) ?? []);
|
||||
}, [data?.pages]);
|
||||
|
||||
const deleteMutation = useDeleteConversationMutation({
|
||||
onSuccess: async () => {
|
||||
setIsDeleteOpen(false);
|
||||
await refetch();
|
||||
showToast({
|
||||
message: localize('com_ui_convo_delete_success'),
|
||||
severity: NotificationSeverity.SUCCESS,
|
||||
showIcon: true,
|
||||
});
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
logger.error('Error deleting archived conversation:', error);
|
||||
showToast({
|
||||
message: localize('com_ui_archive_delete_error') as string,
|
||||
severity: NotificationSeverity.ERROR,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const unarchiveMutation = useArchiveConvoMutation({
|
||||
onSuccess: async () => {
|
||||
await refetch();
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
logger.error('Error unarchiving conversation', error);
|
||||
showToast({
|
||||
message: localize('com_ui_unarchive_error') as string,
|
||||
severity: NotificationSeverity.ERROR,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleFetchNextPage = useCallback(async () => {
|
||||
if (!hasNextPage || isFetchingNextPage) {
|
||||
return;
|
||||
}
|
||||
await fetchNextPage();
|
||||
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
accessorKey: 'title',
|
||||
header: () => {
|
||||
const isSorted = queryParams.sortBy === 'title';
|
||||
const sortDirection = queryParams.sortDirection;
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm"
|
||||
onClick={() =>
|
||||
handleSort('title', isSorted && sortDirection === 'asc' ? 'desc' : 'asc')
|
||||
}
|
||||
>
|
||||
{localize('com_nav_archive_name')}
|
||||
{isSorted && sortDirection === 'asc' && (
|
||||
<ArrowUp className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
|
||||
)}
|
||||
{isSorted && sortDirection === 'desc' && (
|
||||
<ArrowDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
|
||||
)}
|
||||
{!isSorted && <ArrowUpDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const { conversationId, title } = row.original;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 truncate rounded-sm"
|
||||
onClick={() => window.open(`/c/${conversationId}`, '_blank')}
|
||||
>
|
||||
<MinimalIcon
|
||||
endpoint={row.original.endpoint}
|
||||
size={28}
|
||||
isCreatedByUser={false}
|
||||
iconClassName="size-4"
|
||||
/>
|
||||
<span className="underline">{title}</span>
|
||||
</button>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
size: isSmallScreen ? '70%' : '50%',
|
||||
mobileSize: '70%',
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'createdAt',
|
||||
header: () => {
|
||||
const isSorted = queryParams.sortBy === 'createdAt';
|
||||
const sortDirection = queryParams.sortDirection;
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm"
|
||||
onClick={() =>
|
||||
handleSort('createdAt', isSorted && sortDirection === 'asc' ? 'desc' : 'asc')
|
||||
}
|
||||
aria-label={localize('com_nav_archive_created_at_sort')}
|
||||
>
|
||||
{localize('com_nav_archive_created_at')}
|
||||
{isSorted && sortDirection === 'asc' && (
|
||||
<ArrowUp className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
|
||||
)}
|
||||
{isSorted && sortDirection === 'desc' && (
|
||||
<ArrowDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />
|
||||
)}
|
||||
{!isSorted && <ArrowUpDown className="ml-2 h-3 w-4 sm:h-4 sm:w-4" />}
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => formatDate(row.original.createdAt?.toString() ?? '', isSmallScreen),
|
||||
meta: {
|
||||
size: isSmallScreen ? '30%' : '35%',
|
||||
mobileSize: '30%',
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'actions',
|
||||
header: () => (
|
||||
<Label className="px-2 py-0 text-xs sm:px-2 sm:py-2 sm:text-sm">
|
||||
{localize('com_assistants_actions')}
|
||||
</Label>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const conversation = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_unarchive')}
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0 hover:bg-surface-hover"
|
||||
onClick={() =>
|
||||
unarchiveMutation.mutate({
|
||||
conversationId: conversation.conversationId,
|
||||
isArchived: false,
|
||||
})
|
||||
}
|
||||
title={localize('com_ui_unarchive')}
|
||||
aria-label={localize('com_ui_unarchive')}
|
||||
disabled={unarchiveMutation.isLoading}
|
||||
>
|
||||
{unarchiveMutation.isLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<ArchiveRestore className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_delete')}
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0 hover:bg-surface-hover"
|
||||
onClick={() => {
|
||||
setDeleteConversation(row.original);
|
||||
setIsDeleteOpen(true);
|
||||
}}
|
||||
title={localize('com_ui_delete')}
|
||||
aria-label={localize('com_ui_delete')}
|
||||
>
|
||||
<TrashIcon className="size-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
size: '15%',
|
||||
mobileSize: '25%',
|
||||
},
|
||||
},
|
||||
],
|
||||
[handleSort, isSmallScreen, localize, queryParams, unarchiveMutation],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={allConversations}
|
||||
filterColumn="title"
|
||||
onFilterChange={debouncedFilterChange}
|
||||
filterValue={queryParams.search}
|
||||
fetchNextPage={handleFetchNextPage}
|
||||
hasNextPage={hasNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
isLoading={isLoading}
|
||||
showCheckboxes={false}
|
||||
enableSearch={searchState.enabled === true}
|
||||
/>
|
||||
|
||||
<OGDialog open={isDeleteOpen} onOpenChange={onOpenChange}>
|
||||
<OGDialogContent
|
||||
title={localize('com_ui_delete_confirm', {
|
||||
title: deleteConversation?.title ?? localize('com_ui_untitled'),
|
||||
})}
|
||||
className="w-11/12 max-w-md"
|
||||
>
|
||||
<OGDialogHeader>
|
||||
<OGDialogTitle>
|
||||
<Trans
|
||||
i18nKey="com_ui_delete_confirm_strong"
|
||||
values={{ title: deleteConversation?.title }}
|
||||
components={{ strong: <strong /> }}
|
||||
/>
|
||||
</OGDialogTitle>
|
||||
</OGDialogHeader>
|
||||
<div className="flex justify-end gap-4 pt-4">
|
||||
<Button aria-label="cancel" variant="outline" onClick={() => setIsDeleteOpen(false)}>
|
||||
{localize('com_ui_cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() =>
|
||||
deleteMutation.mutate({
|
||||
conversationId: deleteConversation?.conversationId ?? '',
|
||||
})
|
||||
}
|
||||
disabled={deleteMutation.isLoading}
|
||||
>
|
||||
{deleteMutation.isLoading ? <Spinner /> : localize('com_ui_delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</OGDialogContent>
|
||||
</OGDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -396,7 +396,6 @@
|
|||
"com_nav_archive_created_at": "Date Archived",
|
||||
"com_nav_archive_name": "Name",
|
||||
"com_nav_archived_chats": "Archived chats",
|
||||
"com_ui_manage_archived_chats": "Manage archived chats",
|
||||
"com_nav_at_command": "@-Command",
|
||||
"com_nav_at_command_description": "Toggle command \"@\" for switching endpoints, models, presets, etc.",
|
||||
"com_nav_audio_play_error": "Error playing audio: {{0}}",
|
||||
|
|
@ -876,7 +875,6 @@
|
|||
"com_ui_delete_shared_link": "Delete Shared Link - {{title}}",
|
||||
"com_ui_delete_shared_link_heading": "Delete Shared Link",
|
||||
"com_ui_delete_prompt_name": "Delete Prompt - {{name}}",
|
||||
"com_ui_delete_archived_chats": "Delete archived chat?",
|
||||
"com_ui_delete_success": "Successfully deleted",
|
||||
"com_ui_delete_tool": "Delete Tool",
|
||||
"com_ui_delete_tool_confirm": "Are you sure you want to delete this tool?",
|
||||
|
|
@ -1265,11 +1263,7 @@
|
|||
"com_ui_share_update_message": "Your name, custom instructions, and any messages you add after sharing stay private.",
|
||||
"com_ui_share_var": "Share {{0}}",
|
||||
"com_ui_shared_link_delete_success": "Successfully deleted shared link",
|
||||
"com_ui_archived_conversation_delete_success": "Successfully deleted archived conversation",
|
||||
"com_ui_shared_link_not_found": "Shared link not found",
|
||||
"com_ui_open_link": "Open Link {{0}}",
|
||||
"com_ui_view_source_conversation": "View Source Conversation {{0}}",
|
||||
"com_ui_delete_link_title": "Delete Shared Link {{0}}",
|
||||
"com_ui_shared_prompts": "Shared Prompts",
|
||||
"com_ui_shop": "Shopping",
|
||||
"com_ui_show_all": "Show All",
|
||||
|
|
@ -1400,8 +1394,5 @@
|
|||
"com_ui_zoom_in": "Zoom in",
|
||||
"com_ui_zoom_level": "Zoom level",
|
||||
"com_ui_zoom_out": "Zoom out",
|
||||
"com_ui_open_conversation": "Open conversation {{0}}",
|
||||
"com_ui_delete_conversation_title": "Delete conversation {{0}}",
|
||||
"com_ui_unarchive_conversation_title": "Unarchive conversation {{0}}",
|
||||
"com_user_message": "You"
|
||||
}
|
||||
|
|
|
|||
1
package-lock.json
generated
1
package-lock.json
generated
|
|
@ -48827,7 +48827,6 @@
|
|||
"@rollup/plugin-commonjs": "^29.0.0",
|
||||
"@rollup/plugin-node-resolve": "^15.0.0",
|
||||
"@rollup/plugin-replace": "^5.0.5",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@tanstack/react-query": "^4.28.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/react-virtual": "^3.13.13",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"scripts": {
|
||||
"clean": "rimraf dist",
|
||||
"build": "npm run clean && rollup -c --bundleConfigAsCjs",
|
||||
"build:dev": "npm run clean && NODE_ENV=development rollup -c --bundleConfigAsCjs",
|
||||
"build:watch": "rollup -c -w --bundleConfigAsCjs",
|
||||
"dev": "rollup -c -w --bundleConfigAsCjs"
|
||||
},
|
||||
|
|
@ -77,7 +78,6 @@
|
|||
"@rollup/plugin-commonjs": "^29.0.0",
|
||||
"@rollup/plugin-node-resolve": "^15.0.0",
|
||||
"@rollup/plugin-replace": "^5.0.5",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@tanstack/react-query": "^4.28.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/react-virtual": "^3.13.13",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// ESM bundler config for React components
|
||||
import { fileURLToPath } from 'url';
|
||||
import alias from '@rollup/plugin-alias';
|
||||
import terser from '@rollup/plugin-terser';
|
||||
import postcss from 'rollup-plugin-postcss';
|
||||
import replace from '@rollup/plugin-replace';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
|
|
@ -46,26 +45,23 @@ const plugins = [
|
|||
clean: true,
|
||||
check: false,
|
||||
}),
|
||||
terser({
|
||||
compress: {
|
||||
directives: false,
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
const isDev = process.env.NODE_ENV !== 'production';
|
||||
|
||||
export default {
|
||||
input: 'src/index.ts',
|
||||
output: [
|
||||
{
|
||||
file: pkg.main,
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
sourcemap: isDev ? 'inline' : true,
|
||||
exports: 'named',
|
||||
},
|
||||
{
|
||||
file: pkg.module,
|
||||
format: 'esm',
|
||||
sourcemap: true,
|
||||
sourcemap: isDev ? 'inline' : true,
|
||||
exports: 'named',
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export * from './InputOTP';
|
|||
export * from './MultiSearch';
|
||||
export * from './Resizable';
|
||||
export * from './Select';
|
||||
export * from './DataTable/index';
|
||||
export { default as DataTable } from './DataTable';
|
||||
export { default as Radio } from './Radio';
|
||||
export { default as Badge } from './Badge';
|
||||
export { default as Avatar } from './Avatar';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue