From b4b5a2cd6949666850f3593cba5b229b0d8beb22 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Wed, 10 Dec 2025 02:08:41 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=AA=9F=20feat:=20DataTable=20update=20+?= =?UTF-8?q?=20Various=20UI=20enhancements=20(#9698)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🎨 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 --- api/models/Conversation.js | 70 +- api/server/routes/convos.js | 6 +- .../src/components/Chat/Input/Artifacts.tsx | 65 +- .../Chat/Input/ArtifactsSubMenu.tsx | 30 +- client/src/components/Chat/Input/ChatForm.tsx | 1 + .../Chat/Input/Files/AttachFileMenu.tsx | 1 + .../Chat/Input/Files/Table/Columns.tsx | 43 +- .../src/components/Chat/Input/MCPSelect.tsx | 6 +- .../src/components/Chat/Input/MCPSubMenu.tsx | 1 + .../components/Chat/Input/ToolsDropdown.tsx | 3 +- client/src/components/Nav/AccountSettings.tsx | 7 +- client/src/components/Nav/Nav.tsx | 3 +- .../SettingsTabs/Data/ImportConversations.tsx | 11 +- .../Nav/SettingsTabs/Data/SharedLinks.tsx | 448 +-- .../SettingsTabs/General/ArchivedChats.tsx | 400 ++- .../General/ArchivedChatsTable.tsx | 328 -- client/src/components/OAuth/OAuthSuccess.tsx | 2 +- client/src/locales/en/translation.json | 24 +- client/src/style.css | 88 +- package-lock.json | 2962 ++++++++++++++++- packages/client/babel.config.js | 24 + packages/client/jest.config.js | 29 + packages/client/jest.setup.ts | 159 + packages/client/package.json | 13 + packages/client/rollup.config.js | 2 + packages/client/src/components/Checkbox.tsx | 2 +- .../DataTable/DataTable.hooks.spec.ts | 470 +++ .../components/DataTable/DataTable.hooks.ts | 135 + .../components/DataTable/DataTable.spec.tsx | 973 ++++++ .../src/components/DataTable/DataTable.tsx | 609 ++++ .../components/DataTable/DataTable.types.ts | 124 + .../DataTable/DataTableComponents.spec.tsx | 362 ++ .../DataTable/DataTableComponents.tsx | 168 + .../DataTable/DataTableErrorBoundary.spec.tsx | 311 ++ .../DataTable/DataTableErrorBoundary.tsx | 122 + .../DataTable/DataTableSearch.spec.tsx | 178 + .../components/DataTable/DataTableSearch.tsx | 37 + .../client/src/components/DataTable/index.ts | 3 + .../src/components/DataTableColumnHeader.tsx | 73 - .../src/components/DialogTemplate.spec.tsx | 8 +- .../client/src/components/MultiSelect.tsx | 17 +- .../client/src/components/SplitText.spec.tsx | 13 - packages/client/src/components/Table.tsx | 48 +- packages/client/src/components/index.ts | 3 +- .../client/src/locales/Translation.spec.ts | 18 +- .../client/src/locales/en/translation.json | 25 +- packages/client/src/utils/index.ts | 1 + packages/client/src/utils/logger.ts | 49 + packages/client/tsconfig.test.json | 20 + 49 files changed, 7578 insertions(+), 917 deletions(-) delete mode 100644 client/src/components/Nav/SettingsTabs/General/ArchivedChatsTable.tsx create mode 100644 packages/client/babel.config.js create mode 100644 packages/client/jest.config.js create mode 100644 packages/client/jest.setup.ts create mode 100644 packages/client/src/components/DataTable/DataTable.hooks.spec.ts create mode 100644 packages/client/src/components/DataTable/DataTable.hooks.ts create mode 100644 packages/client/src/components/DataTable/DataTable.spec.tsx create mode 100644 packages/client/src/components/DataTable/DataTable.tsx create mode 100644 packages/client/src/components/DataTable/DataTable.types.ts create mode 100644 packages/client/src/components/DataTable/DataTableComponents.spec.tsx create mode 100644 packages/client/src/components/DataTable/DataTableComponents.tsx create mode 100644 packages/client/src/components/DataTable/DataTableErrorBoundary.spec.tsx create mode 100644 packages/client/src/components/DataTable/DataTableErrorBoundary.tsx create mode 100644 packages/client/src/components/DataTable/DataTableSearch.spec.tsx create mode 100644 packages/client/src/components/DataTable/DataTableSearch.tsx create mode 100644 packages/client/src/components/DataTable/index.ts delete mode 100644 packages/client/src/components/DataTableColumnHeader.tsx create mode 100644 packages/client/src/utils/logger.ts create mode 100644 packages/client/tsconfig.test.json diff --git a/api/models/Conversation.js b/api/models/Conversation.js index 13c329aa4a..6428d3970a 100644 --- a/api/models/Conversation.js +++ b/api/models/Conversation.js @@ -28,7 +28,7 @@ const getConvo = async (user, conversationId) => { return await Conversation.findOne({ user, conversationId }).lean(); } catch (error) { logger.error('[getConvo] Error getting single conversation', error); - return { message: 'Error getting single conversation' }; + throw new Error('Error getting single conversation'); } }; @@ -151,13 +151,21 @@ module.exports = { const result = await Conversation.bulkWrite(bulkOps); return result; } catch (error) { - logger.error('[saveBulkConversations] Error saving conversations in bulk', error); + logger.error('[bulkSaveConvos] Error saving conversations in bulk', error); throw new Error('Failed to save conversations in bulk.'); } }, getConvosByCursor: async ( user, - { cursor, limit = 25, isArchived = false, tags, search, order = 'desc' } = {}, + { + cursor, + limit = 25, + isArchived = false, + tags, + search, + sortBy = 'createdAt', + sortDirection = 'desc', + } = {}, ) => { const filters = [{ user }]; if (isArchived) { @@ -184,35 +192,77 @@ module.exports = { filters.push({ conversationId: { $in: matchingIds } }); } catch (error) { logger.error('[getConvosByCursor] Error during meiliSearch', error); - return { message: 'Error during meiliSearch' }; + throw new Error('Error during meiliSearch'); } } + const validSortFields = ['title', 'createdAt', 'updatedAt']; + if (!validSortFields.includes(sortBy)) { + throw new Error( + `Invalid sortBy field: ${sortBy}. Must be one of ${validSortFields.join(', ')}`, + ); + } + const finalSortBy = sortBy; + const finalSortDirection = sortDirection === 'asc' ? 'asc' : 'desc'; + + let cursorFilter = null; if (cursor) { - filters.push({ updatedAt: { $lt: new Date(cursor) } }); + try { + const decoded = JSON.parse(Buffer.from(cursor, 'base64').toString()); + const { primary, secondary } = decoded; + const primaryValue = finalSortBy === 'title' ? primary : new Date(primary); + const secondaryValue = new Date(secondary); + const op = finalSortDirection === 'asc' ? '$gt' : '$lt'; + + cursorFilter = { + $or: [ + { [finalSortBy]: { [op]: primaryValue } }, + { + [finalSortBy]: primaryValue, + updatedAt: { [op]: secondaryValue }, + }, + ], + }; + } catch (err) { + logger.warn('[getConvosByCursor] Invalid cursor format, starting from beginning'); + } + if (cursorFilter) { + filters.push(cursorFilter); + } } const query = filters.length === 1 ? filters[0] : { $and: filters }; try { + const sortOrder = finalSortDirection === 'asc' ? 1 : -1; + const sortObj = { [finalSortBy]: sortOrder }; + + if (finalSortBy !== 'updatedAt') { + sortObj.updatedAt = sortOrder; + } + const convos = await Conversation.find(query) .select( 'conversationId endpoint title createdAt updatedAt user model agent_id assistant_id spec iconURL', ) - .sort({ updatedAt: order === 'asc' ? 1 : -1 }) + .sort(sortObj) .limit(limit + 1) .lean(); let nextCursor = null; if (convos.length > limit) { const lastConvo = convos.pop(); - nextCursor = lastConvo.updatedAt.toISOString(); + const primaryValue = lastConvo[finalSortBy]; + const primaryStr = finalSortBy === 'title' ? primaryValue : primaryValue.toISOString(); + const secondaryStr = lastConvo.updatedAt.toISOString(); + const composite = { primary: primaryStr, secondary: secondaryStr }; + nextCursor = Buffer.from(JSON.stringify(composite)).toString('base64'); } return { conversations: convos, nextCursor }; } catch (error) { logger.error('[getConvosByCursor] Error getting conversations', error); - return { message: 'Error getting conversations' }; + throw new Error('Error getting conversations'); } }, getConvosQueried: async (user, convoIds, cursor = null, limit = 25) => { @@ -252,7 +302,7 @@ module.exports = { return { conversations: limited, nextCursor, convoMap }; } catch (error) { logger.error('[getConvosQueried] Error getting conversations', error); - return { message: 'Error fetching conversations' }; + throw new Error('Error fetching conversations'); } }, getConvo, @@ -269,7 +319,7 @@ module.exports = { } } catch (error) { logger.error('[getConvoTitle] Error getting conversation title', error); - return { message: 'Error getting conversation title' }; + throw new Error('Error getting conversation title'); } }, /** diff --git a/api/server/routes/convos.js b/api/server/routes/convos.js index 766b2a21b0..ad82ede10a 100644 --- a/api/server/routes/convos.js +++ b/api/server/routes/convos.js @@ -31,7 +31,8 @@ router.get('/', async (req, res) => { const cursor = req.query.cursor; const isArchived = isEnabled(req.query.isArchived); const search = req.query.search ? decodeURIComponent(req.query.search) : undefined; - const order = req.query.order || 'desc'; + const sortBy = req.query.sortBy || 'createdAt'; + const sortDirection = req.query.sortDirection || 'desc'; let tags; if (req.query.tags) { @@ -45,7 +46,8 @@ router.get('/', async (req, res) => { isArchived, tags, search, - order, + sortBy, + sortDirection, }); res.status(200).json(result); } catch (error) { diff --git a/client/src/components/Chat/Input/Artifacts.tsx b/client/src/components/Chat/Input/Artifacts.tsx index 2840fc2653..6df404f451 100644 --- a/client/src/components/Chat/Input/Artifacts.tsx +++ b/client/src/components/Chat/Input/Artifacts.tsx @@ -1,4 +1,4 @@ -import React, { memo, useState, useCallback, useMemo } from 'react'; +import React, { memo, useState, useCallback, useMemo, useEffect } from 'react'; import * as Ariakit from '@ariakit/react'; import { CheckboxButton } from '@librechat/client'; import { ArtifactModes } from 'librechat-data-provider'; @@ -18,6 +18,7 @@ function Artifacts() { const { toggleState, debouncedChange, isPinned } = artifacts; const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [isButtonExpanded, setIsButtonExpanded] = useState(false); const currentState = useMemo(() => { if (typeof toggleState === 'string' && toggleState) { @@ -33,11 +34,26 @@ function Artifacts() { const handleToggle = useCallback(() => { if (isEnabled) { debouncedChange({ value: '' }); + setIsButtonExpanded(false); } else { debouncedChange({ value: ArtifactModes.DEFAULT }); } }, [isEnabled, debouncedChange]); + const handleMenuButtonClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + setIsButtonExpanded(!isButtonExpanded); + }, + [isButtonExpanded], + ); + + useEffect(() => { + if (!isPopoverOpen) { + setIsButtonExpanded(false); + } + }, [isPopoverOpen]); + const handleShadcnToggle = useCallback(() => { if (isShadcnEnabled) { debouncedChange({ value: ArtifactModes.DEFAULT }); @@ -77,21 +93,24 @@ function Artifacts() { 'border-amber-600/40 bg-amber-500/10 hover:bg-amber-700/10', 'transition-colors', )} - onClick={(e) => e.stopPropagation()} + onClick={handleMenuButtonClick} > -