mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
🪟 feat: DataTable update + Various UI enhancements (#9698)
* 🎨 feat: Enhance Import Conversations UI with loading state and new localization key * fix: Correct pluralization in selected items message in translation.json * Refactor Chat Input File Table Headers to Use SortFilterHeader Component - Replaced button-based sorting headers in the Chat Input Files Table with a new SortFilterHeader component for better code organization and consistency. - Updated the header for filename, updatedAt, and bytes columns to utilize the new component. Enhance Navigation Component with Skeleton Loading States - Added Skeleton loading states to the Nav component for better user experience during data fetching. - Updated Suspense fallbacks for AgentMarketplaceButton and BookmarkNav components to display Skeletons. Refactor Avatar Component for Improved UI - Enhanced the Avatar component by adding a Label for drag-and-drop functionality. - Improved styling and structure for the file upload area. Update Shared Links Component for Better Error Handling and Sorting - Improved error handling in the Shared Links component for fetching next pages and deleting shared links. - Simplified the header rendering for sorting columns and added sorting functionality to the title and createdAt columns. Refactor Archived Chats Component - Merged ArchivedChats and ArchivedChatsTable components into a single ArchivedChats component for better maintainability. - Implemented sorting and searching functionality with debouncing for improved performance. - Enhanced the UI with better loading states and error handling. Update DataTable Component for Sorting Icons - Added sorting icons (ChevronUp, ChevronDown, ChevronsUpDown) to the DataTable headers for better visual feedback on sorting state. Localization Updates - Updated translation.json to fix missing translations and improve existing ones for better user experience. * ✨ feat: Update DataTable component to streamline props and enhance sorting icons * fix: TS issues * feat: polish and redefine DataTable + shared links and archived chats * feat: enhance DataTable with column pinning and improve sorting functionality * feat: enhance deepEqual function for array support and improve column style stability * refactor: DataTable and ArchivedChats; fix: sorting ArchivedChats API * feat(DataTable): Implement new DataTable component with hooks and optimized features - Added DataTable component with support for virtual scrolling, row selection, and customizable columns. - Introduced hooks for debouncing search input, managing row selection, and calculating column styles. - Enhanced accessibility with keyboard navigation and selection checkboxes. - Implemented skeleton loading state for better user experience during data fetching. - Added DataTableSearch component for filtering data with debounced input. - Created utility logger for improved debugging in development. - Updated translations to support new UI elements and actions. * refactor: update SharedLinks and ArchivedChats to use desktopOnly instead of hideOnMobile; remove unused DataTableColumnHeader component * fix: ensure desktopOnly columns are hidden on mobile in DataTable * refactor: reorganize imports in DataTable components and update index exports * refactor: improve styling and animations in Artifacts, ArtifactsSubMenu, and MCPSubMenu components; update border-radius in style.css * refactor(Artifacts): enhance button toggle functionality and manage expanded state with useEffect * refactor: comment out desktopOnly property in SharedLinks and ArchivedChats components; update translation.json with new keys for link actions * refactor(DataTable): streamline column visibility logic and enhance type definitions; improve cleanup timers and optimize rendering * refactor(DataTable): enhance type definitions for processed data rows and update custom actions renderer type * refactor(DataTable): optimize processed data handling and improve warning for missing IDs; streamline DataTableComponents imports * refactor(DataTable): enhance accessibility features and improve localization for selection and loading states * refactor: improve padding in dialog content and enhance row selection functionality in ArchivedChats and DataTable components * refactor(DataTable): remove unnecessary role and tabindex attributes from select all button for improved accessibility * refactor(translation): remove outdated error messages and unused UI strings for cleaner localization * refactor(DataTable): enhance virtualization and scrolling performance with dynamic overscan adjustments * refactor(DataTableErrorBoundary): enhance error handling and localization support * refactor(DataTable): improve column sizing and visibility handling; remove deprecated features * refactor: enhance UI components with improved class handling and state management * refactor(DataTable): improve column width handling and responsiveness; disable row selection * refactor(DataTable): enhance accessibility with row header support and improve column visibility handling * chore(DataTable): comments update * refactor(Table): add unwrapped prop for direct table rendering; adjust minWidth calculation for responsiveness * refactor(DataTable): simplify search handling by removing unnecessary trimming; adjust column width handling for better responsiveness * refactor(translation): remove redundant drag and drop UI text for clarity * refactor(parsers): change uiResources to a constant and streamline artifacts handling * chore: remove unused file, bump @librechat/client to 0.3.2; fix(SharedLinks): missing import; * refactor: change button variant from destructive to ghost for delete actions in SharedLinks and ArchivedChats components * refactor(DataTable): simplify aria-sort assignment for better readability * refactor(DataTable): update aria-label and ariaLabel to use indexed placeholder for localization * refactor(translation): update no data messages for consistency * Refactor code structure for improved readability and maintainability * chore: restore linting fixes * chore: restore linting fixes 2; refactor: remove unused translation keys * feat(tests): add unit tests for DataTable components and error handling - Implement tests for SelectionCheckbox and SkeletonRows components in DataTable. - Add tests for DataTableErrorBoundary to ensure proper error handling and UI rendering. - Create tests for DataTableSearch to validate search functionality and accessibility. - Update DialogTemplate tests to reflect hardcoded cancel text. - Remove redundant IntersectionObserver mock in SplitText tests. - Unmock react-i18next in Translation tests to validate actual i18n functionality. * refactor: Remove jest-environment-jsdom dependency from package.json; fix: reset package-lock * chore: revert lint fixes * chore: clean up package.json by removing unused devDependencies and redundant test scripts * chore: update package dependencies in package.json and package-lock.json - Added new devDependencies: @babel/core, @babel/preset-env, @babel/preset-react, @babel/preset-typescript, @tanstack/react-table, @tanstack/react-virtual, @testing-library/jest-dom, identity-obj-proxy, jest, jest-environment-jsdom, and lucide-react. - Updated existing devDependencies to their latest versions. - Added new module @asamuzakjp/css-color to package-lock.json with its dependencies. - Updated version of @babel/plugin-transform-destructuring and added @babel/plugin-transform-explicit-resource-management in package-lock.json. --------- Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
9400148175
commit
b4b5a2cd69
49 changed files with 7578 additions and 917 deletions
|
|
@ -1,17 +1,6 @@
|
|||
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,
|
||||
ArrowUpDown,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
ExternalLink,
|
||||
} from 'lucide-react';
|
||||
import type { SharedLinkItem, SharedLinksListParams } from 'librechat-data-provider';
|
||||
import { TrashIcon, MessageSquare, ExternalLink } from 'lucide-react';
|
||||
import {
|
||||
OGDialog,
|
||||
useToastContext,
|
||||
|
|
@ -21,89 +10,162 @@ 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 { useLocalize } from '~/hooks';
|
||||
import { NotificationSeverity } from '~/common';
|
||||
import { formatDate } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
import { formatDate, cn } from '~/utils';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
const DEFAULT_PARAMS: SharedLinksListParams = {
|
||||
pageSize: PAGE_SIZE,
|
||||
pageSize: 25,
|
||||
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 isSearchEnabled = useRecoilValue(store.search);
|
||||
const [queryParams, setQueryParams] = useState<SharedLinksListParams>(DEFAULT_PARAMS);
|
||||
const [deleteRow, setDeleteRow] = useState<SharedLinkItem | null>(null);
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||
const [deleteRow, setDeleteRow] = useState<SharedLinkItem | null>(null);
|
||||
|
||||
const [queryParams, setQueryParams] = useState<SharedLinksListParams>(DEFAULT_PARAMS);
|
||||
const [sorting, setSorting] = useState<SortingState>(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<SharedLinkItem[]>([]);
|
||||
|
||||
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<Record<string, unknown>, unknown>[] = 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')
|
||||
}
|
||||
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>
|
||||
);
|
||||
accessorFn: (row: Record<string, unknown>): unknown => {
|
||||
const link = row as SharedLinkItem;
|
||||
return link.title;
|
||||
},
|
||||
header: () => (
|
||||
<span className="text-xs text-text-primary sm:text-sm">{localize('com_ui_name')}</span>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const { title, shareId } = row.original;
|
||||
const link = row.original as SharedLinkItem;
|
||||
const { title, shareId } = link;
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
|
|
@ -199,7 +213,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"
|
||||
title={title}
|
||||
aria-label={localize('com_ui_open_link', { 0: title })}
|
||||
>
|
||||
<span className="truncate">{title}</span>
|
||||
<ExternalLink
|
||||
|
|
@ -211,141 +225,139 @@ export default function SharedLinks() {
|
|||
);
|
||||
},
|
||||
meta: {
|
||||
size: '35%',
|
||||
mobileSize: '50%',
|
||||
className: 'min-w-[150px] flex-1',
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
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_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>
|
||||
);
|
||||
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);
|
||||
},
|
||||
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<string, unknown>): unknown => null,
|
||||
header: () => (
|
||||
<Label className="px-2 py-0 text-xs hover:bg-surface-hover sm:px-2 sm:py-2 sm:text-sm">
|
||||
<span className="text-xs text-text-primary sm:text-sm">
|
||||
{localize('com_assistants_actions')}
|
||||
</Label>
|
||||
</span>
|
||||
),
|
||||
meta: {
|
||||
size: '7%',
|
||||
mobileSize: '25%',
|
||||
cell: ({ row }) => {
|
||||
const link = row.original as SharedLinkItem;
|
||||
const { title, conversationId } = link;
|
||||
|
||||
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>
|
||||
);
|
||||
},
|
||||
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>
|
||||
),
|
||||
meta: {
|
||||
className: 'w-24',
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
],
|
||||
[isSmallScreen, localize, queryParams, handleSort],
|
||||
[isSmallScreen, localize],
|
||||
);
|
||||
|
||||
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 onClick={() => setIsOpen(true)}>
|
||||
<OGDialogTrigger asChild>
|
||||
<Button aria-labelledby="shared-links-label" variant="outline">
|
||||
{localize('com_ui_manage')}
|
||||
</Button>
|
||||
</OGDialogTrigger>
|
||||
|
||||
<OGDialogContent
|
||||
title={localize('com_nav_my_files')}
|
||||
className="w-11/12 max-w-5xl bg-background text-text-primary shadow-2xl"
|
||||
>
|
||||
<OGDialogContent className={cn('w-11/12 max-w-6xl', isSmallScreen && 'px-1 pb-1')}>
|
||||
<OGDialogHeader>
|
||||
<OGDialogTitle>{localize('com_nav_shared_links')}</OGDialogTitle>
|
||||
</OGDialogHeader>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={allLinks}
|
||||
onDelete={handleDelete}
|
||||
filterColumn="title"
|
||||
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}
|
||||
hasNextPage={hasNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
fetchNextPage={handleFetchNextPage}
|
||||
showCheckboxes={false}
|
||||
onFilterChange={debouncedFilterChange}
|
||||
filterValue={queryParams.search}
|
||||
isLoading={isLoading}
|
||||
enableSearch={isSearchEnabled}
|
||||
sorting={sorting}
|
||||
onSortingChange={handleSortingChange}
|
||||
/>
|
||||
</OGDialogContent>
|
||||
</OGDialog>
|
||||
<OGDialog open={isDeleteOpen} onOpenChange={setIsDeleteOpen}>
|
||||
<OGDialogTemplate
|
||||
showCloseButton={false}
|
||||
title={localize('com_ui_delete_shared_link_heading')}
|
||||
className="max-w-[450px]"
|
||||
title={localize('com_ui_delete_shared_link')}
|
||||
className="w-11/12 max-w-md"
|
||||
main={
|
||||
<>
|
||||
<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 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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue