mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
feat: Implement error handling for favorites limit and consolidate marketplace access logic
This commit is contained in:
parent
1ed0ae9de4
commit
92f80f3aaa
8 changed files with 80 additions and 72 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export { default as useNavScrolling } from './useNavScrolling';
|
||||
export { default as useShowMarketplace } from './useShowMarketplace';
|
||||
export * from './useNavHelpers';
|
||||
|
|
|
|||
37
client/src/hooks/Nav/useShowMarketplace.ts
Normal file
37
client/src/hooks/Nav/useShowMarketplace.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue