mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-30 07:08:50 +01:00
feat: polish and redefine DataTable + shared links and archived chats
This commit is contained in:
parent
c5efa9c9d4
commit
8b5f9104ef
6 changed files with 1128 additions and 403 deletions
|
|
@ -1,5 +1,4 @@
|
|||
import { useCallback, useState, useMemo, useEffect, useRef } from 'react';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useCallback, useState, useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { TrashIcon, MessageSquare } from 'lucide-react';
|
||||
|
|
@ -23,7 +22,6 @@ import { useDeleteSharedLinkMutation, useSharedLinksQuery } from '~/data-provide
|
|||
import { NotificationSeverity } from '~/common';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { formatDate } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
const DEFAULT_PARAMS: SharedLinksListParams = {
|
||||
pageSize: 25,
|
||||
|
|
@ -37,38 +35,20 @@ export default function SharedLinks() {
|
|||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||
const isSearchEnabled = useRecoilValue(store.search);
|
||||
const [queryParams, setQueryParams] = useState<SharedLinksListParams>(DEFAULT_PARAMS);
|
||||
const [deleteRow, setDeleteRow] = useState<SharedLinkItem | null>(null);
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const prevSortRef = useRef({
|
||||
sortBy: DEFAULT_PARAMS.sortBy,
|
||||
sortDirection: DEFAULT_PARAMS.sortDirection,
|
||||
});
|
||||
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch, isLoading } =
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isFetching, refetch, isLoading } =
|
||||
useSharedLinksQuery(queryParams, {
|
||||
enabled: isOpen,
|
||||
keepPreviousData: true,
|
||||
staleTime: 30 * 1000,
|
||||
cacheTime: 5 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
keepPreviousData: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const { sortBy, sortDirection } = queryParams;
|
||||
const prevSort = prevSortRef.current;
|
||||
|
||||
if (sortBy !== prevSort.sortBy || sortDirection !== prevSort.sortDirection) {
|
||||
refetch();
|
||||
prevSortRef.current = { sortBy, sortDirection };
|
||||
}
|
||||
}, [queryParams, isOpen, refetch]);
|
||||
|
||||
const handleSort = useCallback((sortField: string, sortOrder: 'asc' | 'desc') => {
|
||||
setQueryParams((prev) => ({
|
||||
...prev,
|
||||
|
|
@ -85,17 +65,6 @@ export default function SharedLinks() {
|
|||
}));
|
||||
}, []);
|
||||
|
||||
const debouncedFilterChange = useMemo(
|
||||
() => debounce(handleFilterChange, 500), // Increased debounce time to 500ms
|
||||
[handleFilterChange],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
debouncedFilterChange.cancel();
|
||||
};
|
||||
}, [debouncedFilterChange]);
|
||||
|
||||
const allLinks = useMemo(() => {
|
||||
if (!data?.pages) {
|
||||
return [];
|
||||
|
|
@ -286,15 +255,13 @@ export default function SharedLinks() {
|
|||
columns={columns}
|
||||
data={allLinks}
|
||||
onDelete={handleDelete}
|
||||
filterColumn="title"
|
||||
config={{ skeleton: { count: 10 }, search: { filterColumn: 'title' } }}
|
||||
hasNextPage={hasNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
isFetching={isFetching}
|
||||
fetchNextPage={handleFetchNextPage}
|
||||
showCheckboxes={false}
|
||||
onFilterChange={debouncedFilterChange}
|
||||
filterValue={queryParams.search}
|
||||
onFilterChange={handleFilterChange}
|
||||
isLoading={isLoading}
|
||||
enableSearch={!!isSearchEnabled}
|
||||
onSortChange={handleSort}
|
||||
sortBy={queryParams.sortBy}
|
||||
sortDirection={queryParams.sortDirection}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { TrashIcon, ArchiveRestore } from 'lucide-react';
|
||||
import type { ColumnDef, SortingState } from '@tanstack/react-table';
|
||||
import {
|
||||
Button,
|
||||
OGDialog,
|
||||
|
|
@ -27,7 +26,6 @@ import { MinimalIcon } from '~/components/Endpoints';
|
|||
import { NotificationSeverity } from '~/common';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { formatDate } from '~/utils';
|
||||
import store from '~/store';
|
||||
|
||||
const DEFAULT_PARAMS: ConversationListParams = {
|
||||
isArchived: true,
|
||||
|
|
@ -36,82 +34,86 @@ const DEFAULT_PARAMS: ConversationListParams = {
|
|||
search: '',
|
||||
};
|
||||
|
||||
type SortField = 'title' | 'createdAt';
|
||||
const defaultSort: SortingState = [
|
||||
{
|
||||
id: 'createdAt',
|
||||
desc: true,
|
||||
},
|
||||
];
|
||||
|
||||
// Define the table column type for better type safety
|
||||
type TableColumn<TData, TValue> = ColumnDef<TData, TValue> & {
|
||||
meta?: {
|
||||
size?: string | number;
|
||||
mobileSize?: string | number;
|
||||
minWidth?: string | number;
|
||||
};
|
||||
};
|
||||
|
||||
export default function ArchivedChatsTable() {
|
||||
const localize = useLocalize();
|
||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||
const { showToast } = useToastContext();
|
||||
const isSearchEnabled = useRecoilValue(store.search);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||
const [deleteRow, setDeleteRow] = useState<TConversation | null>(null);
|
||||
const [unarchivingId, setUnarchivingId] = useState<string | null>(null);
|
||||
const prevSortRef = useRef({
|
||||
sortBy: DEFAULT_PARAMS.sortBy,
|
||||
sortDirection: DEFAULT_PARAMS.sortDirection,
|
||||
});
|
||||
|
||||
const [queryParams, setQueryParams] = useState<ConversationListParams>(DEFAULT_PARAMS);
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
const [sorting, setSorting] = useState<SortingState>(defaultSort);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch, isLoading } =
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isFetching, refetch, isLoading } =
|
||||
useConversationsInfiniteQuery(queryParams, {
|
||||
enabled: isOpen,
|
||||
keepPreviousData: true,
|
||||
staleTime: 30 * 1000,
|
||||
cacheTime: 5 * 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
keepPreviousData: false,
|
||||
});
|
||||
|
||||
const handleSort = useCallback((field: string, direction: 'asc' | 'desc') => {
|
||||
const handleSearchChange = useCallback((value: string) => {
|
||||
const trimmedValue = value.trim();
|
||||
setSearchValue(trimmedValue);
|
||||
setQueryParams((prev) => ({
|
||||
...prev,
|
||||
sortBy: field as SortField,
|
||||
sortDirection: direction,
|
||||
search: trimmedValue,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// Trigger refetch when sort parameters change
|
||||
useEffect(() => {
|
||||
if (!isOpen) return; // Only refetch if dialog is open
|
||||
|
||||
const { sortBy, sortDirection } = queryParams;
|
||||
const prevSort = prevSortRef.current;
|
||||
|
||||
if (sortBy !== prevSort.sortBy || sortDirection !== prevSort.sortDirection) {
|
||||
console.log('Sort changed, refetching...', { from: prevSort, to: { sortBy, sortDirection } });
|
||||
refetch();
|
||||
prevSortRef.current = { sortBy, sortDirection };
|
||||
}
|
||||
}, [queryParams, isOpen, refetch]);
|
||||
|
||||
const debouncedApplySearch = useMemo(
|
||||
() =>
|
||||
debounce((value: string) => {
|
||||
const handleSortingChange = useCallback(
|
||||
(updater: SortingState | ((old: SortingState) => SortingState)) => {
|
||||
const newSorting = typeof updater === 'function' ? updater(sorting) : updater;
|
||||
setSorting(newSorting);
|
||||
const sortDescriptor = newSorting[0];
|
||||
if (sortDescriptor) {
|
||||
setQueryParams((prev) => ({
|
||||
...prev,
|
||||
search: encodeURIComponent(value.trim()),
|
||||
sortBy: sortDescriptor.id as 'createdAt' | 'title',
|
||||
sortDirection: sortDescriptor.desc ? 'desc' : 'asc',
|
||||
}));
|
||||
}, 500), // Increased debounce time to 500ms for better UX
|
||||
[],
|
||||
);
|
||||
|
||||
const onFilterChange = useCallback(
|
||||
(value: string) => {
|
||||
setSearchInput(value);
|
||||
debouncedApplySearch(value);
|
||||
} else {
|
||||
setQueryParams((prev) => ({
|
||||
...prev,
|
||||
sortBy: 'createdAt',
|
||||
sortDirection: 'desc',
|
||||
}));
|
||||
}
|
||||
},
|
||||
[debouncedApplySearch],
|
||||
[sorting],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
debouncedApplySearch.cancel();
|
||||
};
|
||||
}, [debouncedApplySearch]);
|
||||
const handleError = useCallback(
|
||||
(error: Error) => {
|
||||
console.error('DataTable error:', error);
|
||||
showToast({
|
||||
message: localize('com_ui_unarchive_error'),
|
||||
severity: NotificationSeverity.ERROR,
|
||||
});
|
||||
},
|
||||
[showToast, localize],
|
||||
);
|
||||
|
||||
const allConversations = useMemo(() => {
|
||||
if (!data?.pages) return [];
|
||||
|
|
@ -149,26 +151,21 @@ export default function ArchivedChatsTable() {
|
|||
|
||||
const handleFetchNextPage = useCallback(async () => {
|
||||
if (!hasNextPage || isFetchingNextPage) return;
|
||||
|
||||
try {
|
||||
await fetchNextPage();
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch next page:', error);
|
||||
showToast({
|
||||
message: localize('com_ui_unarchive_error'),
|
||||
severity: NotificationSeverity.ERROR,
|
||||
});
|
||||
}
|
||||
}, [fetchNextPage, hasNextPage, isFetchingNextPage, showToast, localize]);
|
||||
await fetchNextPage();
|
||||
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
|
||||
|
||||
const confirmDelete = useCallback(() => {
|
||||
if (!deleteRow?.conversationId) return;
|
||||
if (!deleteRow?.conversationId) {
|
||||
showToast({
|
||||
message: localize('com_ui_convo_delete_error'),
|
||||
severity: NotificationSeverity.WARNING,
|
||||
});
|
||||
return;
|
||||
}
|
||||
deleteMutation.mutate({ conversationId: deleteRow.conversationId });
|
||||
}, [deleteMutation, deleteRow]);
|
||||
}, [deleteMutation, deleteRow, localize, showToast]);
|
||||
|
||||
const { sortBy, sortDirection } = queryParams;
|
||||
|
||||
const columns = useMemo(
|
||||
const columns: TableColumn<TConversation, any>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
accessorKey: 'title',
|
||||
|
|
@ -178,26 +175,29 @@ export default function ArchivedChatsTable() {
|
|||
cell: ({ row }) => {
|
||||
const { conversationId, title } = row.original;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 truncate"
|
||||
onClick={() => window.open(`/c/${conversationId}`, '_blank')}
|
||||
<a
|
||||
href={`/c/${conversationId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 truncate underline"
|
||||
aria-label={localize('com_ui_open_conversation', { 0: title })}
|
||||
>
|
||||
<MinimalIcon
|
||||
endpoint={row.original.endpoint}
|
||||
size={28}
|
||||
isCreatedByUser={false}
|
||||
iconClassName="size-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="underline">{title}</span>
|
||||
</button>
|
||||
<span>{title}</span>
|
||||
</a>
|
||||
);
|
||||
},
|
||||
meta: {
|
||||
size: isSmallScreen ? '70%' : '50%',
|
||||
mobileSize: '70%',
|
||||
enableSorting: true,
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: 'createdAt',
|
||||
|
|
@ -208,11 +208,11 @@ export default function ArchivedChatsTable() {
|
|||
meta: {
|
||||
size: isSmallScreen ? '30%' : '35%',
|
||||
mobileSize: '30%',
|
||||
enableSorting: true,
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: 'actions',
|
||||
id: 'actions',
|
||||
header: () => (
|
||||
<Label className="px-2 py-0 text-xs sm:px-2 sm:py-2 sm:text-sm">
|
||||
{localize('com_assistants_actions')}
|
||||
|
|
@ -220,6 +220,7 @@ export default function ArchivedChatsTable() {
|
|||
),
|
||||
cell: ({ row }) => {
|
||||
const conversation = row.original;
|
||||
const { title } = conversation;
|
||||
const isRowUnarchiving = unarchivingId === conversation.conversationId;
|
||||
|
||||
return (
|
||||
|
|
@ -231,13 +232,16 @@ export default function ArchivedChatsTable() {
|
|||
variant="ghost"
|
||||
className="h-8 w-8 p-0 hover:bg-surface-hover"
|
||||
onClick={() => {
|
||||
setUnarchivingId(conversation.conversationId);
|
||||
const conversationId = conversation.conversationId;
|
||||
if (!conversationId) return;
|
||||
setUnarchivingId(conversationId);
|
||||
unarchiveMutation.mutate(
|
||||
{ conversationId: conversation.conversationId, isArchived: false },
|
||||
{ conversationId, isArchived: false },
|
||||
{ onSettled: () => setUnarchivingId(null) },
|
||||
);
|
||||
}}
|
||||
disabled={isRowUnarchiving}
|
||||
aria-label={localize('com_ui_unarchive_conversation_title', { 0: title })}
|
||||
>
|
||||
{isRowUnarchiving ? <Spinner /> : <ArchiveRestore className="size-4" />}
|
||||
</Button>
|
||||
|
|
@ -253,6 +257,7 @@ export default function ArchivedChatsTable() {
|
|||
setDeleteRow(row.original);
|
||||
setIsDeleteOpen(true);
|
||||
}}
|
||||
aria-label={localize('com_ui_delete_conversation_title', { 0: title })}
|
||||
>
|
||||
<TrashIcon className="size-4" />
|
||||
</Button>
|
||||
|
|
@ -264,11 +269,11 @@ export default function ArchivedChatsTable() {
|
|||
meta: {
|
||||
size: '15%',
|
||||
mobileSize: '25%',
|
||||
enableSorting: false,
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
],
|
||||
[isSmallScreen, localize, unarchivingId, unarchiveMutation],
|
||||
[isSmallScreen, localize, unarchiveMutation, unarchivingId],
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -288,43 +293,30 @@ export default function ArchivedChatsTable() {
|
|||
columns={columns}
|
||||
data={allConversations}
|
||||
isLoading={isLoading}
|
||||
enableSearch={!!isSearchEnabled}
|
||||
filterColumn="title"
|
||||
onFilterChange={onFilterChange}
|
||||
filterValue={searchInput}
|
||||
isFetching={isFetching}
|
||||
config={{
|
||||
skeleton: { count: 10 },
|
||||
search: {
|
||||
filterColumn: 'title',
|
||||
enableSearch: true,
|
||||
debounce: 300,
|
||||
},
|
||||
selection: {
|
||||
enableRowSelection: false,
|
||||
showCheckboxes: false,
|
||||
},
|
||||
}}
|
||||
filterValue={searchValue}
|
||||
onFilterChange={handleSearchChange}
|
||||
fetchNextPage={handleFetchNextPage}
|
||||
hasNextPage={hasNextPage}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
showCheckboxes={false}
|
||||
onSortChange={handleSort}
|
||||
sortBy={sortBy}
|
||||
sortDirection={sortDirection}
|
||||
sorting={sorting}
|
||||
onSortingChange={handleSortingChange}
|
||||
onError={handleError}
|
||||
/>
|
||||
</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'),
|
||||
}}
|
||||
/>
|
||||
</OGDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1042,7 +1042,6 @@
|
|||
"com_ui_no_read_access": "You don't have permission to view memories",
|
||||
"com_ui_no_results_found": "No results found",
|
||||
"com_ui_no_terms_content": "No terms and conditions content to display",
|
||||
"com_ui_no_valid_items": "something needs to go here. was empty",
|
||||
"com_ui_none": "None",
|
||||
"com_ui_not_used": "Not Used",
|
||||
"com_ui_nothing_found": "Nothing found",
|
||||
|
|
@ -1304,5 +1303,10 @@
|
|||
"com_ui_zoom_in": "Zoom in",
|
||||
"com_ui_zoom_level": "Zoom level",
|
||||
"com_ui_zoom_out": "Zoom out",
|
||||
"com_ui_add_anything": "Add anything",
|
||||
"com_ui_drag_and_drop": "Drop any file here to add it to the conversation",
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,98 @@
|
|||
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||
import { Button } from '../Button';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* Error boundary specifically for DataTable component.
|
||||
* Catches JavaScript errors in the table rendering and provides a fallback UI.
|
||||
* Handles errors from virtualizer, cell renderers, fetch operations, and child components.
|
||||
*/
|
||||
interface DataTableErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
interface DataTableErrorBoundaryProps {
|
||||
children: ReactNode;
|
||||
onError?: (error: Error) => void;
|
||||
onReset?: () => void;
|
||||
}
|
||||
|
||||
export class DataTableErrorBoundary extends Component<
|
||||
DataTableErrorBoundaryProps,
|
||||
DataTableErrorBoundaryState
|
||||
> {
|
||||
constructor(props: DataTableErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): DataTableErrorBoundaryState {
|
||||
// Update state to show fallback UI and store error for logging
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
// Log the error (you can also log to an error reporting service)
|
||||
console.error('DataTable Error Boundary caught an error:', error, errorInfo);
|
||||
|
||||
// Call parent error handler if provided
|
||||
this.props.onError?.(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the error state and attempt to re-render the children.
|
||||
* This can be used to retry after a table error (e.g., network retry).
|
||||
*/
|
||||
private handleReset = () => {
|
||||
this.setState({ hasError: false, error: undefined });
|
||||
this.props.onReset?.();
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
// Custom fallback UI for DataTable errors
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center p-8">
|
||||
<div className="max-w-md rounded-lg border border-red-200 bg-red-50 p-6 shadow-sm dark:border-red-800 dark:bg-red-950/20">
|
||||
<div className="flex items-center gap-2 text-red-800 dark:text-red-200">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
<h3 className="text-sm font-medium">Table Error</h3>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-red-700 dark:text-red-300">
|
||||
Table failed to load. Please refresh or try again.
|
||||
</p>
|
||||
<div className="mt-4 flex justify-center">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={this.handleReset}
|
||||
className="flex items-center gap-2 border-red-300 bg-red-50 px-3 py-1.5 text-sm hover:bg-red-100 dark:border-red-700 dark:bg-red-950/20 dark:hover:bg-red-900/20"
|
||||
aria-label="Retry loading table"
|
||||
>
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Optional: Show technical error details in development */}
|
||||
{process.env.NODE_ENV === 'development' && this.state.error && (
|
||||
<details className="mt-4 max-w-md rounded-md bg-gray-100 p-3 text-xs dark:bg-gray-800">
|
||||
<summary className="cursor-pointer font-medium text-gray-900 dark:text-gray-100">
|
||||
Error Details (Dev)
|
||||
</summary>
|
||||
<pre className="mt-2 whitespace-pre-wrap text-gray-700 dark:text-gray-300">
|
||||
{this.state.error.message}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
// Named export for convenience
|
||||
export default DataTableErrorBoundary;
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"com_ui_cancel": "Cancel",
|
||||
"com_ui_no_options": "No options available",
|
||||
"com_ui_no_data": "No Data Available"
|
||||
"com_ui_no_results_found": "No results found",
|
||||
"com_ui_no_data_available": "No data available"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue