mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00
refactor: DataTable and ArchivedChats; fix: sorting ArchivedChats API
This commit is contained in:
parent
8e22e70439
commit
a253aa5d22
5 changed files with 2296 additions and 549 deletions
|
@ -28,7 +28,7 @@ const getConvo = async (user, conversationId) => {
|
|||
return await Conversation.findOne({ user, conversationId }).lean();
|
||||
} catch (error) {
|
||||
logger.error('[getConvo] Error getting single conversation', error);
|
||||
return { message: 'Error getting single conversation' };
|
||||
throw new Error('Error getting single conversation');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -151,13 +151,21 @@ module.exports = {
|
|||
const result = await Conversation.bulkWrite(bulkOps);
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('[saveBulkConversations] Error saving conversations in bulk', error);
|
||||
logger.error('[bulkSaveConvos] Error saving conversations in bulk', error);
|
||||
throw new Error('Failed to save conversations in bulk.');
|
||||
}
|
||||
},
|
||||
getConvosByCursor: async (
|
||||
user,
|
||||
{ cursor, limit = 25, isArchived = false, tags, search, order = 'desc' } = {},
|
||||
{
|
||||
cursor,
|
||||
limit = 25,
|
||||
isArchived = false,
|
||||
tags,
|
||||
search,
|
||||
sortBy = 'createdAt',
|
||||
sortDirection = 'desc',
|
||||
} = {},
|
||||
) => {
|
||||
const filters = [{ user }];
|
||||
if (isArchived) {
|
||||
|
@ -184,35 +192,77 @@ module.exports = {
|
|||
filters.push({ conversationId: { $in: matchingIds } });
|
||||
} catch (error) {
|
||||
logger.error('[getConvosByCursor] Error during meiliSearch', error);
|
||||
return { message: 'Error during meiliSearch' };
|
||||
throw new Error('Error during meiliSearch');
|
||||
}
|
||||
}
|
||||
|
||||
const validSortFields = ['title', 'createdAt', 'updatedAt'];
|
||||
if (!validSortFields.includes(sortBy)) {
|
||||
throw new Error(
|
||||
`Invalid sortBy field: ${sortBy}. Must be one of ${validSortFields.join(', ')}`,
|
||||
);
|
||||
}
|
||||
const finalSortBy = sortBy;
|
||||
const finalSortDirection = sortDirection === 'asc' ? 'asc' : 'desc';
|
||||
|
||||
let cursorFilter = null;
|
||||
if (cursor) {
|
||||
filters.push({ updatedAt: { $lt: new Date(cursor) } });
|
||||
try {
|
||||
const decoded = JSON.parse(Buffer.from(cursor, 'base64').toString());
|
||||
const { primary, secondary } = decoded;
|
||||
const primaryValue = finalSortBy === 'title' ? primary : new Date(primary);
|
||||
const secondaryValue = new Date(secondary);
|
||||
const op = finalSortDirection === 'asc' ? '$gt' : '$lt';
|
||||
|
||||
cursorFilter = {
|
||||
$or: [
|
||||
{ [finalSortBy]: { [op]: primaryValue } },
|
||||
{
|
||||
[finalSortBy]: primaryValue,
|
||||
updatedAt: { [op]: secondaryValue },
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (err) {
|
||||
logger.warn('[getConvosByCursor] Invalid cursor format, starting from beginning');
|
||||
}
|
||||
if (cursorFilter) {
|
||||
filters.push(cursorFilter);
|
||||
}
|
||||
}
|
||||
|
||||
const query = filters.length === 1 ? filters[0] : { $and: filters };
|
||||
|
||||
try {
|
||||
const sortOrder = finalSortDirection === 'asc' ? 1 : -1;
|
||||
const sortObj = { [finalSortBy]: sortOrder };
|
||||
|
||||
if (finalSortBy !== 'updatedAt') {
|
||||
sortObj.updatedAt = sortOrder;
|
||||
}
|
||||
|
||||
const convos = await Conversation.find(query)
|
||||
.select(
|
||||
'conversationId endpoint title createdAt updatedAt user model agent_id assistant_id spec iconURL',
|
||||
)
|
||||
.sort({ updatedAt: order === 'asc' ? 1 : -1 })
|
||||
.sort(sortObj)
|
||||
.limit(limit + 1)
|
||||
.lean();
|
||||
|
||||
let nextCursor = null;
|
||||
if (convos.length > limit) {
|
||||
const lastConvo = convos.pop();
|
||||
nextCursor = lastConvo.updatedAt.toISOString();
|
||||
const primaryValue = lastConvo[finalSortBy];
|
||||
const primaryStr = finalSortBy === 'title' ? primaryValue : primaryValue.toISOString();
|
||||
const secondaryStr = lastConvo.updatedAt.toISOString();
|
||||
const composite = { primary: primaryStr, secondary: secondaryStr };
|
||||
nextCursor = Buffer.from(JSON.stringify(composite)).toString('base64');
|
||||
}
|
||||
|
||||
return { conversations: convos, nextCursor };
|
||||
} catch (error) {
|
||||
logger.error('[getConvosByCursor] Error getting conversations', error);
|
||||
return { message: 'Error getting conversations' };
|
||||
throw new Error('Error getting conversations');
|
||||
}
|
||||
},
|
||||
getConvosQueried: async (user, convoIds, cursor = null, limit = 25) => {
|
||||
|
@ -252,7 +302,7 @@ module.exports = {
|
|||
return { conversations: limited, nextCursor, convoMap };
|
||||
} catch (error) {
|
||||
logger.error('[getConvosQueried] Error getting conversations', error);
|
||||
return { message: 'Error fetching conversations' };
|
||||
throw new Error('Error fetching conversations');
|
||||
}
|
||||
},
|
||||
getConvo,
|
||||
|
@ -269,7 +319,7 @@ module.exports = {
|
|||
}
|
||||
} catch (error) {
|
||||
logger.error('[getConvoTitle] Error getting conversation title', error);
|
||||
return { message: 'Error getting conversation title' };
|
||||
throw new Error('Error getting conversation title');
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
|
|
@ -30,7 +30,8 @@ router.get('/', async (req, res) => {
|
|||
const cursor = req.query.cursor;
|
||||
const isArchived = isEnabled(req.query.isArchived);
|
||||
const search = req.query.search ? decodeURIComponent(req.query.search) : undefined;
|
||||
const order = req.query.order || 'desc';
|
||||
const sortBy = req.query.sortBy || 'createdAt';
|
||||
const sortDirection = req.query.sortDirection || 'desc';
|
||||
|
||||
let tags;
|
||||
if (req.query.tags) {
|
||||
|
@ -44,7 +45,8 @@ router.get('/', async (req, res) => {
|
|||
isArchived,
|
||||
tags,
|
||||
search,
|
||||
order,
|
||||
sortBy,
|
||||
sortDirection,
|
||||
});
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
Label,
|
||||
TooltipAnchor,
|
||||
Spinner,
|
||||
DataTable,
|
||||
useToastContext,
|
||||
useMediaQuery,
|
||||
} from '@librechat/client';
|
||||
|
@ -26,6 +25,7 @@ import { MinimalIcon } from '~/components/Endpoints';
|
|||
import { NotificationSeverity } from '~/common';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { formatDate } from '~/utils';
|
||||
import DataTable from './DataTable';
|
||||
|
||||
const DEFAULT_PARAMS = {
|
||||
isArchived: true,
|
||||
|
@ -44,13 +44,12 @@ const defaultSort: SortingState = [
|
|||
},
|
||||
];
|
||||
|
||||
// Define the table column type for better type safety
|
||||
// (kept from your original code)
|
||||
type TableColumn<TData, TValue> = ColumnDef<TData, TValue> & {
|
||||
meta?: {
|
||||
size?: string | number;
|
||||
mobileSize?: string | number;
|
||||
minWidth?: string | number;
|
||||
priority?: number;
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -86,36 +85,41 @@ export default function ArchivedChatsTable() {
|
|||
}));
|
||||
}, []);
|
||||
|
||||
// Robust against stale state; keeps UI sort in sync with backend defaults
|
||||
const handleSortingChange = useCallback(
|
||||
(updater: SortingState | ((old: SortingState) => SortingState)) => {
|
||||
setSorting((prev) => {
|
||||
const next = typeof updater === 'function' ? updater(prev) : updater;
|
||||
|
||||
// If user clears sorting, fall back to default both in UI and query
|
||||
const coerced = next.length === 0 ? defaultSort : next;
|
||||
const coerced = next;
|
||||
const primary = coerced[0];
|
||||
|
||||
setQueryParams((p) => {
|
||||
if (primary && isSortKey(primary.id)) {
|
||||
const newParams = (() => {
|
||||
if (primary && isSortKey(primary.id)) {
|
||||
return {
|
||||
...p,
|
||||
sortBy: primary.id,
|
||||
sortDirection: primary.desc ? 'desc' : 'asc',
|
||||
};
|
||||
}
|
||||
return {
|
||||
...p,
|
||||
sortBy: primary.id,
|
||||
sortDirection: primary.desc ? 'desc' : 'asc',
|
||||
sortBy: 'createdAt',
|
||||
sortDirection: 'desc',
|
||||
};
|
||||
}
|
||||
// Fallback if id isn't one of the permitted keys
|
||||
return {
|
||||
...p,
|
||||
sortBy: 'createdAt',
|
||||
sortDirection: 'desc',
|
||||
};
|
||||
})();
|
||||
|
||||
setTimeout(() => {
|
||||
refetch();
|
||||
}, 0);
|
||||
|
||||
return newParams;
|
||||
});
|
||||
|
||||
return coerced;
|
||||
});
|
||||
},
|
||||
[setQueryParams, setSorting],
|
||||
[setQueryParams, setSorting, refetch],
|
||||
);
|
||||
|
||||
const handleError = useCallback(
|
||||
|
@ -189,13 +193,7 @@ export default function ArchivedChatsTable() {
|
|||
cell: ({ row }) => {
|
||||
const { conversationId, title } = row.original;
|
||||
return (
|
||||
<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 })}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<MinimalIcon
|
||||
endpoint={row.original.endpoint}
|
||||
size={28}
|
||||
|
@ -203,13 +201,21 @@ export default function ArchivedChatsTable() {
|
|||
iconClassName="size-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{title}</span>
|
||||
</a>
|
||||
<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: {
|
||||
size: isSmallScreen ? '70%' : '50%',
|
||||
mobileSize: '70%',
|
||||
priority: 3,
|
||||
minWidth: 'min-content',
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
|
@ -220,8 +226,8 @@ export default function ArchivedChatsTable() {
|
|||
),
|
||||
cell: ({ row }) => formatDate(row.original.createdAt?.toString() ?? '', isSmallScreen),
|
||||
meta: {
|
||||
size: isSmallScreen ? '30%' : '35%',
|
||||
mobileSize: '30%',
|
||||
priority: 2,
|
||||
minWidth: '80px',
|
||||
},
|
||||
enableSorting: true,
|
||||
},
|
||||
|
@ -265,7 +271,7 @@ export default function ArchivedChatsTable() {
|
|||
description={localize('com_ui_delete')}
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="destructive"
|
||||
className="h-8 w-8 p-0 hover:bg-surface-hover"
|
||||
onClick={() => {
|
||||
setDeleteRow(row.original);
|
||||
|
@ -281,8 +287,8 @@ export default function ArchivedChatsTable() {
|
|||
);
|
||||
},
|
||||
meta: {
|
||||
size: '15%',
|
||||
mobileSize: '25%',
|
||||
priority: 1,
|
||||
minWidth: '120px',
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -31,6 +31,7 @@ export * from './InputOTP';
|
|||
export * from './MultiSearch';
|
||||
export * from './Resizable';
|
||||
export * from './Select';
|
||||
export { default as DataTableErrorBoundary } from './DataTable/DataTableErrorBoundary';
|
||||
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