feat: Implement error handling for favorites limit and consolidate marketplace access logic

This commit is contained in:
Marco Beretta 2025-12-03 22:17:44 +01:00
parent 1ed0ae9de4
commit 92f80f3aaa
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
8 changed files with 80 additions and 72 deletions

View file

@ -17,7 +17,11 @@ const updateFavoritesController = async (req, res) => {
}
if (favorites.length > MAX_FAVORITES) {
return res.status(400).json({ message: `Maximum ${MAX_FAVORITES} favorites allowed` });
return res.status(400).json({
code: 'MAX_FAVORITES_EXCEEDED',
message: `Maximum ${MAX_FAVORITES} favorites allowed`,
limit: MAX_FAVORITES,
});
}
for (const fav of favorites) {

View file

@ -1,11 +1,11 @@
import { useMemo, memo, type FC, useCallback, useEffect, useRef, useContext } from 'react';
import { useMemo, memo, type FC, useCallback, useEffect, useRef } from 'react';
import throttle from 'lodash/throttle';
import { ChevronRight } from 'lucide-react';
import { useRecoilValue } from 'recoil';
import { Spinner, useMediaQuery } from '@librechat/client';
import { List, AutoSizer, CellMeasurer, CellMeasurerCache } from 'react-virtualized';
import { TConversation, PermissionTypes, Permissions } from 'librechat-data-provider';
import { useLocalize, TranslationKeys, useFavorites, useHasAccess, AuthContext } from '~/hooks';
import { TConversation } from 'librechat-data-provider';
import { useLocalize, TranslationKeys, useFavorites, useShowMarketplace } from '~/hooks';
import FavoritesList from '~/components/Nav/Favorites/FavoritesList';
import { groupConversationsByDate, cn } from '~/utils';
import Convo from './Convo';
@ -90,29 +90,11 @@ const Conversations: FC<ConversationsProps> = ({
setIsChatsExpanded,
}) => {
const localize = useLocalize();
const authContext = useContext(AuthContext);
const search = useRecoilValue(store.search);
const { favorites, isLoading: isFavoritesLoading } = useFavorites();
const isSmallScreen = useMediaQuery('(max-width: 768px)');
const convoHeight = isSmallScreen ? 44 : 34;
const hasAccessToAgents = useHasAccess({
permissionType: PermissionTypes.AGENTS,
permission: Permissions.USE,
});
const hasAccessToMarketplace = useHasAccess({
permissionType: PermissionTypes.MARKETPLACE,
permission: Permissions.USE,
});
// Check if auth is ready (avoid race conditions)
const authReady =
authContext?.isAuthenticated !== undefined &&
(authContext?.isAuthenticated === false || authContext?.user !== undefined);
// Show agent marketplace when marketplace permission is enabled, auth is ready, and user has access to agents
const showAgentMarketplace = authReady && hasAccessToAgents && hasAccessToMarketplace;
const showAgentMarketplace = useShowMarketplace();
// Determine if FavoritesList will render content
const shouldShowFavorites =

View file

@ -1,9 +1,8 @@
import React, { useCallback, useContext } from 'react';
import React, { useCallback } from 'react';
import { LayoutGrid } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { TooltipAnchor, Button } from '@librechat/client';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import { useLocalize, useHasAccess, AuthContext } from '~/hooks';
import { useLocalize, useShowMarketplace } from '~/hooks';
interface AgentMarketplaceButtonProps {
isSmallScreen?: boolean;
@ -16,17 +15,7 @@ export default function AgentMarketplaceButton({
}: AgentMarketplaceButtonProps) {
const navigate = useNavigate();
const localize = useLocalize();
const authContext = useContext(AuthContext);
const hasAccessToAgents = useHasAccess({
permissionType: PermissionTypes.AGENTS,
permission: Permissions.USE,
});
const hasAccessToMarketplace = useHasAccess({
permissionType: PermissionTypes.MARKETPLACE,
permission: Permissions.USE,
});
const showAgentMarketplace = useShowMarketplace();
const handleAgentMarketplace = useCallback(() => {
navigate('/agents');
@ -35,14 +24,6 @@ export default function AgentMarketplaceButton({
}
}, [navigate, isSmallScreen, toggleNav]);
// Check if auth is ready (avoid race conditions)
const authReady =
authContext?.isAuthenticated !== undefined &&
(authContext?.isAuthenticated === false || authContext?.user !== undefined);
// Show agent marketplace when marketplace permission is enabled, auth is ready, and user has access to agents
const showAgentMarketplace = authReady && hasAccessToAgents && hasAccessToMarketplace;
if (!showAgentMarketplace) {
return null;
}

View file

@ -1,14 +1,14 @@
import React, { useRef, useCallback, useMemo, useEffect, useContext } from 'react';
import React, { useRef, useCallback, useMemo, useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { LayoutGrid } from 'lucide-react';
import { useDrag, useDrop } from 'react-dnd';
import { Skeleton } from '@librechat/client';
import { useNavigate } from 'react-router-dom';
import { QueryKeys, dataService } from 'librechat-data-provider';
import { useQueries, useQueryClient } from '@tanstack/react-query';
import { QueryKeys, dataService, PermissionTypes, Permissions } from 'librechat-data-provider';
import type { InfiniteData } from '@tanstack/react-query';
import type t from 'librechat-data-provider';
import { useFavorites, useLocalize, useHasAccess, AuthContext } from '~/hooks';
import { useFavorites, useLocalize, useShowMarketplace } from '~/hooks';
import FavoriteItem from './FavoriteItem';
import store from '~/store';
@ -119,27 +119,9 @@ export default function FavoritesList({
const navigate = useNavigate();
const localize = useLocalize();
const queryClient = useQueryClient();
const authContext = useContext(AuthContext);
const search = useRecoilValue(store.search);
const { favorites, reorderFavorites, isLoading: isFavoritesLoading } = useFavorites();
const hasAccessToAgents = useHasAccess({
permissionType: PermissionTypes.AGENTS,
permission: Permissions.USE,
});
const hasAccessToMarketplace = useHasAccess({
permissionType: PermissionTypes.MARKETPLACE,
permission: Permissions.USE,
});
// Check if auth is ready (avoid race conditions)
const authReady =
authContext?.isAuthenticated !== undefined &&
(authContext?.isAuthenticated === false || authContext?.user !== undefined);
// Show agent marketplace when marketplace permission is enabled, auth is ready, and user has access to agents
const showAgentMarketplace = authReady && hasAccessToAgents && hasAccessToMarketplace;
const showAgentMarketplace = useShowMarketplace();
const handleAgentMarketplace = useCallback(() => {
navigate('/agents');

View file

@ -1,2 +1,3 @@
export { default as useNavScrolling } from './useNavScrolling';
export { default as useShowMarketplace } from './useShowMarketplace';
export * from './useNavHelpers';

View file

@ -0,0 +1,37 @@
import { useContext, useMemo } from 'react';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import { useHasAccess, AuthContext } from '~/hooks';
/**
* Hook to determine if the Agent Marketplace should be shown.
* Consolidates the logic for checking:
* - Auth readiness (avoid race conditions)
* - Access to Agents permission
* - Access to Marketplace permission
*
* @returns Whether the Agent Marketplace should be displayed
*/
export default function useShowMarketplace(): boolean {
const authContext = useContext(AuthContext);
const hasAccessToAgents = useHasAccess({
permissionType: PermissionTypes.AGENTS,
permission: Permissions.USE,
});
const hasAccessToMarketplace = useHasAccess({
permissionType: PermissionTypes.MARKETPLACE,
permission: Permissions.USE,
});
// Check if auth is ready (avoid race conditions)
const authReady = useMemo(
() =>
authContext?.isAuthenticated !== undefined &&
(authContext?.isAuthenticated === false || authContext?.user !== undefined),
[authContext?.isAuthenticated, authContext?.user],
);
// Show agent marketplace when marketplace permission is enabled, auth is ready, and user has access to agents
return authReady && hasAccessToAgents && hasAccessToMarketplace;
}

View file

@ -7,6 +7,9 @@ import { favoritesAtom } from '~/store';
import { useLocalize } from '~/hooks';
import { logger } from '~/utils';
/** Maximum number of favorites allowed (must match backend MAX_FAVORITES) */
const MAX_FAVORITES = 50;
/**
* Hook for managing user favorites (pinned agents and models).
*
@ -61,6 +64,23 @@ export default function useFavorites() {
}
}, [getFavoritesQuery.data, setFavorites, updateFavoritesMutation.isLoading]);
const getErrorMessage = useCallback(
(error: unknown): string => {
if (error && typeof error === 'object' && 'response' in error) {
const axiosError = error as {
response?: { data?: { code?: string; limit?: number } };
};
const { code, limit } = axiosError.response?.data ?? {};
if (code === 'MAX_FAVORITES_EXCEEDED') {
return localize('com_ui_max_favorites_reached', { 0: String(limit ?? MAX_FAVORITES) });
}
}
return localize('com_ui_error');
},
[localize],
);
const saveFavorites = useCallback(
async (newFavorites: typeof favorites) => {
const cleaned = cleanFavorites(newFavorites);
@ -70,7 +90,7 @@ export default function useFavorites() {
await updateFavoritesMutation.mutateAsync(cleaned);
} catch (error) {
logger.error('Error updating favorites:', error);
showToast({ message: localize('com_ui_error'), status: 'error' });
showToast({ message: getErrorMessage(error), status: 'error' });
// Refetch to resync state with server
getFavoritesQuery.refetch();
} finally {
@ -81,7 +101,7 @@ export default function useFavorites() {
}, 100);
}
},
[setFavorites, updateFavoritesMutation, showToast, localize, getFavoritesQuery],
[setFavorites, updateFavoritesMutation, showToast, getErrorMessage, getFavoritesQuery],
);
const addFavoriteAgent = (agentId: string) => {
@ -147,8 +167,8 @@ export default function useFavorites() {
try {
await updateFavoritesMutation.mutateAsync(cleaned);
} catch (error) {
console.error('Error reordering favorites:', error);
showToast({ message: localize('com_ui_error'), status: 'error' });
logger.error('Error reordering favorites:', error);
showToast({ message: getErrorMessage(error), status: 'error' });
// Refetch to resync state with server
getFavoritesQuery.refetch();
} finally {
@ -158,7 +178,7 @@ export default function useFavorites() {
}
}
},
[setFavorites, updateFavoritesMutation, showToast, localize, getFavoritesQuery],
[setFavorites, updateFavoritesMutation, showToast, getErrorMessage, getFavoritesQuery],
);
return {

View file

@ -1034,6 +1034,7 @@
"com_ui_manage": "Manage",
"com_ui_marketplace": "Marketplace",
"com_ui_marketplace_allow_use": "Allow using Marketplace",
"com_ui_max_favorites_reached": "Maximum pinned items reached ({{0}}). Unpin an item to add more.",
"com_ui_max_file_size": "PNG, JPG or JPEG (max {{0}})",
"com_ui_max_tags": "Maximum number allowed is {{0}}, using latest values.",
"com_ui_mcp_authenticated_success": "MCP server '{{0}}' authenticated successfully",