mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-03 17:18:51 +01:00
🔗 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:
parent
460cde0c0b
commit
fa9e778399
55 changed files with 1779 additions and 1975 deletions
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue