✨ feat: Enhance favorites management by adding loading and error states, and refactor related hooks and components
This commit is contained in:
@@ -32,7 +32,11 @@ jest.mock('~/hooks', () => ({
|
||||
addFavoriteModel: jest.fn(),
|
||||
removeFavoriteModel: jest.fn(),
|
||||
reorderFavorites: jest.fn(),
|
||||
persistFavorites: jest.fn(),
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
isUpdating: false,
|
||||
fetchError: null,
|
||||
updateError: null,
|
||||
})),
|
||||
}));
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ const Conversations: FC<ConversationsProps> = ({
|
||||
setIsChatsExpanded,
|
||||
}) => {
|
||||
const localize = useLocalize();
|
||||
const { favorites } = useFavorites();
|
||||
const { favorites, isLoading: isFavoritesLoading } = useFavorites();
|
||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||
const convoHeight = isSmallScreen ? 44 : 34;
|
||||
|
||||
@@ -152,13 +152,13 @@ const Conversations: FC<ConversationsProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (cache) {
|
||||
// Only clear the favorites row
|
||||
// Clear the favorites row when favorites change or finish loading
|
||||
cache.clear(0, 0);
|
||||
if (containerRef.current && 'recomputeRowHeights' in containerRef.current) {
|
||||
containerRef.current.recomputeRowHeights(0);
|
||||
}
|
||||
}
|
||||
}, [favorites.length, cache, containerRef]);
|
||||
}, [favorites.length, isFavoritesLoading, cache, containerRef]);
|
||||
|
||||
const rowRenderer = useCallback(
|
||||
({ index, key, parent, style }) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useRef, useCallback, useMemo, useContext } from 'react';
|
||||
import React, { useRef, useCallback, useMemo, useContext, useEffect } from 'react';
|
||||
import { LayoutGrid } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
@@ -100,7 +100,7 @@ export default function FavoritesList({
|
||||
const localize = useLocalize();
|
||||
const queryClient = useQueryClient();
|
||||
const authContext = useContext(AuthContext);
|
||||
const { favorites, reorderFavorites, persistFavorites } = useFavorites();
|
||||
const { favorites, reorderFavorites, isLoading: isFavoritesLoading } = useFavorites();
|
||||
|
||||
const hasAccessToAgents = useHasAccess({
|
||||
permissionType: PermissionTypes.AGENTS,
|
||||
@@ -171,19 +171,33 @@ export default function FavoritesList({
|
||||
return map;
|
||||
}, [agentQueries, queryClient]);
|
||||
|
||||
const draggedFavoritesRef = useRef(favorites);
|
||||
|
||||
const moveItem = useCallback(
|
||||
(dragIndex: number, hoverIndex: number) => {
|
||||
const newFavorites = [...favorites];
|
||||
const newFavorites = [...draggedFavoritesRef.current];
|
||||
const [draggedItem] = newFavorites.splice(dragIndex, 1);
|
||||
newFavorites.splice(hoverIndex, 0, draggedItem);
|
||||
reorderFavorites(newFavorites);
|
||||
draggedFavoritesRef.current = newFavorites;
|
||||
reorderFavorites(newFavorites, false);
|
||||
},
|
||||
[favorites, reorderFavorites],
|
||||
[reorderFavorites],
|
||||
);
|
||||
|
||||
const handleDrop = useCallback(() => {
|
||||
persistFavorites(favorites);
|
||||
}, [favorites, persistFavorites]);
|
||||
// Persist the final order using the ref which has the latest state
|
||||
reorderFavorites(draggedFavoritesRef.current, true);
|
||||
}, [reorderFavorites]);
|
||||
|
||||
// Keep ref in sync when favorites change from external sources
|
||||
useEffect(() => {
|
||||
draggedFavoritesRef.current = favorites;
|
||||
}, [favorites]);
|
||||
|
||||
// Show nothing while favorites are loading to prevent layout shifts
|
||||
if (isFavoritesLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If no favorites and no marketplace to show, return null
|
||||
if (favorites.length === 0 && !showAgentMarketplace) {
|
||||
@@ -193,7 +207,7 @@ export default function FavoritesList({
|
||||
return (
|
||||
<div className="mb-2 flex flex-col pb-2">
|
||||
<div className="mt-1 flex flex-col gap-1">
|
||||
{/* Agent Marketplace button - identical styling to favorite items */}
|
||||
{/* Agent Marketplace button */}
|
||||
{showAgentMarketplace && (
|
||||
<div
|
||||
className="group relative flex w-full cursor-pointer items-center justify-between rounded-lg px-3 py-2 text-sm text-text-primary hover:bg-surface-active-alt"
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import type { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { dataService } from 'librechat-data-provider';
|
||||
import type { FavoritesState } from '~/store/favorites';
|
||||
|
||||
export const useGetFavoritesQuery = (config?: any) => {
|
||||
export const useGetFavoritesQuery = (
|
||||
config?: Omit<UseQueryOptions<FavoritesState, Error>, 'queryKey' | 'queryFn'>,
|
||||
) => {
|
||||
return useQuery<FavoritesState>(
|
||||
['favorites'],
|
||||
() => dataService.getFavorites() as Promise<FavoritesState>,
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import type { Favorite } from '~/store/favorites';
|
||||
import store from '~/store';
|
||||
import { useGetFavoritesQuery, useUpdateFavoritesMutation } from '~/data-provider';
|
||||
|
||||
/**
|
||||
* Hook for managing user favorites (pinned agents and models).
|
||||
*
|
||||
* Favorites are synchronized with the server via `/api/user/settings/favorites`.
|
||||
* Each favorite is either:
|
||||
* - An agent: `{ agentId: string }`
|
||||
* - A model: `{ model: string, endpoint: string }`
|
||||
*
|
||||
* @returns Object containing favorites state and helper methods for
|
||||
* adding, removing, toggling, reordering, and checking favorites.
|
||||
*/
|
||||
export default function useFavorites() {
|
||||
const [favorites, setFavorites] = useRecoilState(store.favorites);
|
||||
const getFavoritesQuery = useGetFavoritesQuery();
|
||||
@@ -12,18 +22,8 @@ export default function useFavorites() {
|
||||
useEffect(() => {
|
||||
if (getFavoritesQuery.data) {
|
||||
if (Array.isArray(getFavoritesQuery.data)) {
|
||||
const mapped = getFavoritesQuery.data.map(
|
||||
(f: Favorite & { type?: string; id?: string }) => {
|
||||
if (f.agentId || (f.model && f.endpoint)) return f;
|
||||
if (f.type === 'agent' && f.id) return { agentId: f.id };
|
||||
// Drop label and map legacy model format
|
||||
if (f.type === 'model') return { model: f.model, endpoint: f.endpoint };
|
||||
return f;
|
||||
},
|
||||
);
|
||||
setFavorites(mapped);
|
||||
setFavorites(getFavoritesQuery.data);
|
||||
} else {
|
||||
// Handle legacy format or invalid data
|
||||
setFavorites([]);
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,10 @@ export default function useFavorites() {
|
||||
saveFavorites(newFavorites);
|
||||
};
|
||||
|
||||
const isFavoriteAgent = (agentId: string) => {
|
||||
const isFavoriteAgent = (agentId: string | undefined | null) => {
|
||||
if (!agentId) {
|
||||
return false;
|
||||
}
|
||||
return favorites.some((f) => f.agentId === agentId);
|
||||
};
|
||||
|
||||
@@ -85,12 +88,25 @@ export default function useFavorites() {
|
||||
}
|
||||
};
|
||||
|
||||
const reorderFavorites = (newFavorites: typeof favorites) => {
|
||||
setFavorites(newFavorites);
|
||||
};
|
||||
|
||||
const persistFavorites = (newFavorites: typeof favorites) => {
|
||||
updateFavoritesMutation.mutate(newFavorites);
|
||||
/**
|
||||
* Reorder favorites and persist the new order to the server.
|
||||
* This combines state update and persistence to avoid race conditions
|
||||
* where the closure captures stale state.
|
||||
*/
|
||||
const reorderFavorites = (newFavorites: typeof favorites, persist = false) => {
|
||||
const cleaned = newFavorites.map((f) => {
|
||||
if (f.agentId) {
|
||||
return { agentId: f.agentId };
|
||||
}
|
||||
if (f.model && f.endpoint) {
|
||||
return { model: f.model, endpoint: f.endpoint };
|
||||
}
|
||||
return f;
|
||||
});
|
||||
setFavorites(cleaned);
|
||||
if (persist) {
|
||||
updateFavoritesMutation.mutate(cleaned);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -104,6 +120,15 @@ export default function useFavorites() {
|
||||
toggleFavoriteAgent,
|
||||
toggleFavoriteModel,
|
||||
reorderFavorites,
|
||||
persistFavorites,
|
||||
/** Whether the favorites query is currently loading */
|
||||
isLoading: getFavoritesQuery.isLoading,
|
||||
/** Whether there was an error fetching favorites */
|
||||
isError: getFavoritesQuery.isError,
|
||||
/** Whether the update mutation is in progress */
|
||||
isUpdating: updateFavoritesMutation.isLoading,
|
||||
/** Error from fetching favorites, if any */
|
||||
fetchError: getFavoritesQuery.error,
|
||||
/** Error from updating favorites, if any */
|
||||
updateError: updateFavoritesMutation.error,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user