🔗 feat: Enhance Share Functionality, Optimize DataTable & Fix Critical Bugs (#5220)

* 🔄 refactor: frontend and backend share link logic; feat: qrcode for share link; feat: refresh link

* 🐛 fix: Conditionally render shared link and refactor share link creation logic

* 🐛 fix: Correct conditional check for shareId in ShareButton component

* 🔄 refactor: Update shared links API and data handling; improve query parameters and response structure

* 🔄 refactor: Update shared links pagination and response structure; replace pageNumber with cursor for improved data fetching

* 🔄 refactor: DataTable performance optimization

* fix: delete shared link cache update

* 🔄 refactor: Enhance shared links functionality; add conversationId to shared link model and update related components

* 🔄 refactor: Add delete functionality to SharedLinkButton; integrate delete mutation and confirmation dialog

* 🔄 feat: Add AnimatedSearchInput component with gradient animations and search functionality; update search handling in API and localization

* 🔄 refactor: Improve SharedLinks component; enhance delete functionality and loading states, optimize AnimatedSearchInput, and refine DataTable scrolling behavior

* fix: mutation type issues with deleted shared link mutation

* fix: MutationOptions types

* fix: Ensure only public shared links are retrieved in getSharedLink function

* fix: `qrcode.react` install location

* fix: ensure non-public shared links are not fetched when checking for existing shared links, and remove deprecated .exec() method for queries

* fix: types and import order

* refactor: cleanup share button UI logic, make more intuitive

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Marco Beretta 2025-01-21 15:31:05 +01:00 committed by GitHub
parent 460cde0c0b
commit fa9e778399
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 1779 additions and 1975 deletions

View file

@ -1,26 +1,18 @@
import {
Constants,
InfiniteCollections,
defaultAssistantsVersion,
ConversationListResponse,
} from 'librechat-data-provider';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { dataService, MutationKeys, QueryKeys, defaultOrderQuery } from 'librechat-data-provider';
import type * as t from 'librechat-data-provider';
import type { InfiniteData, UseMutationResult } from '@tanstack/react-query';
import type * as t from 'librechat-data-provider';
import { useConversationTagsQuery, useConversationsInfiniteQuery } from './queries';
import useUpdateTagsInConvo from '~/hooks/Conversations/useUpdateTagsInConvo';
import { updateConversationTag } from '~/utils/conversationTags';
import { normalizeData } from '~/utils/collection';
import {
useConversationTagsQuery,
useConversationsInfiniteQuery,
useSharedLinksInfiniteQuery,
} from './queries';
import {
logger,
/* Shared Links */
addSharedLink,
deleteSharedLink,
/* Conversations */
addConversation,
updateConvoFields,
@ -244,120 +236,126 @@ export const useArchiveConvoMutation = (options?: t.ArchiveConvoOptions) => {
};
export const useCreateSharedLinkMutation = (
options?: t.CreateSharedLinkOptions,
): UseMutationResult<t.TSharedLinkResponse, unknown, t.TSharedLinkRequest, unknown> => {
options?: t.MutationOptions<t.TCreateShareLinkRequest, { conversationId: string }>,
): UseMutationResult<t.TSharedLinkResponse, unknown, { conversationId: string }, unknown> => {
const queryClient = useQueryClient();
const { refetch } = useSharedLinksInfiniteQuery();
const { onSuccess, ..._options } = options || {};
return useMutation((payload: t.TSharedLinkRequest) => dataService.createSharedLink(payload), {
onSuccess: (_data, vars, context) => {
if (!vars.conversationId) {
return;
return useMutation(
({ conversationId }: { conversationId: string }) => {
if (!conversationId) {
throw new Error('Conversation ID is required');
}
const isPublic = vars.isPublic === true;
queryClient.setQueryData<t.SharedLinkListData>([QueryKeys.sharedLinks], (sharedLink) => {
if (!sharedLink) {
return sharedLink;
}
const pageSize = sharedLink.pages[0].pageSize as number;
return normalizeData(
// If the shared link is public, add it to the shared links cache list
isPublic ? addSharedLink(sharedLink, _data) : deleteSharedLink(sharedLink, _data.shareId),
InfiniteCollections.SHARED_LINKS,
pageSize,
);
});
queryClient.setQueryData([QueryKeys.sharedLinks, _data.shareId], _data);
if (!isPublic) {
const current = queryClient.getQueryData<t.ConversationData>([QueryKeys.sharedLinks]);
refetch({
refetchPage: (page, index) => index === ((current?.pages.length ?? 0) || 1) - 1,
});
}
onSuccess?.(_data, vars, context);
return dataService.createSharedLink(conversationId);
},
..._options,
});
{
onSuccess: (_data: t.TSharedLinkResponse, vars, context) => {
queryClient.setQueryData([QueryKeys.sharedLinks, _data.conversationId], _data);
onSuccess?.(_data, vars, context);
},
..._options,
},
);
};
export const useUpdateSharedLinkMutation = (
options?: t.UpdateSharedLinkOptions,
): UseMutationResult<t.TSharedLinkResponse, unknown, t.TSharedLinkRequest, unknown> => {
options?: t.MutationOptions<t.TUpdateShareLinkRequest, { shareId: string }>,
): UseMutationResult<t.TSharedLinkResponse, unknown, { shareId: string }, unknown> => {
const queryClient = useQueryClient();
const { refetch } = useSharedLinksInfiniteQuery();
const { onSuccess, ..._options } = options || {};
return useMutation((payload: t.TSharedLinkRequest) => dataService.updateSharedLink(payload), {
onSuccess: (_data, vars, context) => {
if (!vars.conversationId) {
return;
return useMutation(
({ shareId }) => {
if (!shareId) {
throw new Error('Share ID is required');
}
const isPublic = vars.isPublic === true;
queryClient.setQueryData<t.SharedLinkListData>([QueryKeys.sharedLinks], (sharedLink) => {
if (!sharedLink) {
return sharedLink;
}
return normalizeData(
// If the shared link is public, add it to the shared links cache list.
isPublic
? // Even if the SharedLink data exists in the database, it is not registered in the cache when isPublic is false.
// Therefore, when isPublic is true, use addSharedLink instead of updateSharedLink.
addSharedLink(sharedLink, _data)
: deleteSharedLink(sharedLink, _data.shareId),
InfiniteCollections.SHARED_LINKS,
sharedLink.pages[0].pageSize as number,
);
});
queryClient.setQueryData([QueryKeys.sharedLinks, _data.shareId], _data);
if (!isPublic) {
const current = queryClient.getQueryData<t.ConversationData>([QueryKeys.sharedLinks]);
refetch({
refetchPage: (page, index) => index === ((current?.pages.length ?? 0) || 1) - 1,
});
}
onSuccess?.(_data, vars, context);
return dataService.updateSharedLink(shareId);
},
..._options,
});
{
onSuccess: (_data: t.TSharedLinkResponse, vars, context) => {
queryClient.setQueryData([QueryKeys.sharedLinks, _data.conversationId], _data);
onSuccess?.(_data, vars, context);
},
..._options,
},
);
};
export const useDeleteSharedLinkMutation = (
options?: t.DeleteSharedLinkOptions,
): UseMutationResult<t.TDeleteSharedLinkResponse, unknown, { shareId: string }, unknown> => {
): UseMutationResult<
t.TDeleteSharedLinkResponse,
unknown,
{ shareId: string },
t.DeleteSharedLinkContext
> => {
const queryClient = useQueryClient();
const { refetch } = useSharedLinksInfiniteQuery();
const { onSuccess, ..._options } = options || {};
return useMutation(({ shareId }) => dataService.deleteSharedLink(shareId), {
onSuccess: (_data, vars, context) => {
if (!vars.shareId) {
return;
const { onSuccess } = options || {};
return useMutation((vars) => dataService.deleteSharedLink(vars.shareId), {
onMutate: async (vars) => {
await queryClient.cancelQueries({
queryKey: [QueryKeys.sharedLinks],
exact: false,
});
const previousQueries = new Map();
const queryKeys = queryClient.getQueryCache().findAll([QueryKeys.sharedLinks]);
queryKeys.forEach((query) => {
const previousData = queryClient.getQueryData(query.queryKey);
previousQueries.set(query.queryKey, previousData);
queryClient.setQueryData<t.SharedLinkQueryData>(query.queryKey, (old) => {
if (!old?.pages) {
return old;
}
const updatedPages = old.pages.map((page) => ({
...page,
links: page.links.filter((link) => link.shareId !== vars.shareId),
}));
const nonEmptyPages = updatedPages.filter((page) => page.links.length > 0);
return {
...old,
pages: nonEmptyPages,
};
});
});
return { previousQueries };
},
onError: (_err, _vars, context) => {
if (context?.previousQueries) {
context.previousQueries.forEach((prevData: unknown, prevQueryKey: unknown) => {
queryClient.setQueryData(prevQueryKey as string[], prevData);
});
}
},
onSettled: () => {
queryClient.invalidateQueries({
queryKey: [QueryKeys.sharedLinks],
exact: false,
});
},
onSuccess: (data, variables) => {
if (onSuccess) {
onSuccess(data, variables);
}
queryClient.setQueryData([QueryKeys.sharedMessages, vars.shareId], null);
queryClient.setQueryData<t.SharedLinkListData>([QueryKeys.sharedLinks], (data) => {
if (!data) {
return data;
}
return normalizeData(
deleteSharedLink(data, vars.shareId),
InfiniteCollections.SHARED_LINKS,
data.pages[0].pageSize as number,
);
queryClient.refetchQueries({
queryKey: [QueryKeys.sharedLinks],
exact: true,
});
const current = queryClient.getQueryData<t.ConversationData>([QueryKeys.sharedLinks]);
refetch({
refetchPage: (page, index) => index === (current?.pages.length ?? 1) - 1,
});
onSuccess?.(_data, vars, context);
},
..._options,
});
};
@ -575,36 +573,33 @@ export const useDuplicateConversationMutation = (
): UseMutationResult<t.TDuplicateConvoResponse, unknown, t.TDuplicateConvoRequest, unknown> => {
const queryClient = useQueryClient();
const { onSuccess, ..._options } = options ?? {};
return useMutation(
(payload: t.TDuplicateConvoRequest) => dataService.duplicateConversation(payload),
{
onSuccess: (data, vars, context) => {
const originalId = vars.conversationId ?? '';
if (originalId.length === 0) {
return;
return useMutation((payload) => dataService.duplicateConversation(payload), {
onSuccess: (data, vars, context) => {
const originalId = vars.conversationId ?? '';
if (originalId.length === 0) {
return;
}
if (data == null) {
return;
}
queryClient.setQueryData(
[QueryKeys.conversation, data.conversation.conversationId],
data.conversation,
);
queryClient.setQueryData<t.ConversationData>([QueryKeys.allConversations], (convoData) => {
if (!convoData) {
return convoData;
}
if (data == null) {
return;
}
queryClient.setQueryData(
[QueryKeys.conversation, data.conversation.conversationId],
data.conversation,
);
queryClient.setQueryData<t.ConversationData>([QueryKeys.allConversations], (convoData) => {
if (!convoData) {
return convoData;
}
return addConversation(convoData, data.conversation);
});
queryClient.setQueryData<t.TMessage[]>(
[QueryKeys.messages, data.conversation.conversationId],
data.messages,
);
onSuccess?.(data, vars, context);
},
..._options,
return addConversation(convoData, data.conversation);
});
queryClient.setQueryData<t.TMessage[]>(
[QueryKeys.messages, data.conversation.conversationId],
data.messages,
);
onSuccess?.(data, vars, context);
},
);
..._options,
});
};
export const useForkConvoMutation = (

View file

@ -24,7 +24,7 @@ import type {
AssistantDocument,
TEndpointsConfig,
TCheckUserKeyResponse,
SharedLinkListParams,
SharedLinksListParams,
SharedLinksResponse,
} from 'librechat-data-provider';
import { findPageForConversation } from '~/utils';
@ -139,31 +139,29 @@ export const useConversationsInfiniteQuery = (
);
};
export const useSharedLinksInfiniteQuery = (
params?: SharedLinkListParams,
export const useSharedLinksQuery = (
params: SharedLinksListParams,
config?: UseInfiniteQueryOptions<SharedLinksResponse, unknown>,
) => {
return useInfiniteQuery<SharedLinksResponse, unknown>(
[QueryKeys.sharedLinks],
({ pageParam = '' }) =>
const { pageSize, isPublic, search, sortBy, sortDirection } = params;
return useInfiniteQuery<SharedLinksResponse>({
queryKey: [QueryKeys.sharedLinks, { pageSize, isPublic, search, sortBy, sortDirection }],
queryFn: ({ pageParam }) =>
dataService.listSharedLinks({
...params,
pageNumber: pageParam?.toString(),
isPublic: params?.isPublic || true,
cursor: pageParam?.toString(),
pageSize,
isPublic,
search,
sortBy,
sortDirection,
}),
{
getNextPageParam: (lastPage) => {
const currentPageNumber = Number(lastPage.pageNumber);
const totalPages = Number(lastPage.pages); // Convert totalPages to a number
// If the current page number is less than total pages, return the next page number
return currentPageNumber < totalPages ? currentPageNumber + 1 : undefined;
},
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
},
);
getNextPageParam: (lastPage) => lastPage?.nextCursor ?? undefined,
keepPreviousData: true,
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 30 * 60 * 1000, // 30 minutes
...config,
});
};
export const useConversationTagsQuery = (