🔗 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

@ -14,10 +14,20 @@ export const messages = (conversationId: string, messageId?: string) =>
const shareRoot = '/api/share';
export const shareMessages = (shareId: string) => `${shareRoot}/${shareId}`;
export const getSharedLinks = (pageNumber: string, isPublic: boolean) =>
`${shareRoot}?pageNumber=${pageNumber}&isPublic=${isPublic}`;
export const createSharedLink = shareRoot;
export const updateSharedLink = shareRoot;
export const getSharedLink = (conversationId: string) => `${shareRoot}/link/${conversationId}`;
export const getSharedLinks = (
pageSize: number,
isPublic: boolean,
sortBy: 'title' | 'createdAt',
sortDirection: 'asc' | 'desc',
search?: string,
cursor?: string,
) =>
`${shareRoot}?pageSize=${pageSize}&isPublic=${isPublic}&sortBy=${sortBy}&sortDirection=${sortDirection}${
search ? `&search=${search}` : ''
}${cursor ? `&cursor=${cursor}` : ''}`;
export const createSharedLink = (conversationId: string) => `${shareRoot}/${conversationId}`;
export const updateSharedLink = (shareId: string) => `${shareRoot}/${shareId}`;
const keysEndpoint = '/api/keys';

View file

@ -41,27 +41,29 @@ export function getSharedMessages(shareId: string): Promise<t.TSharedMessagesRes
return request.get(endpoints.shareMessages(shareId));
}
export const listSharedLinks = (
params?: q.SharedLinkListParams,
export const listSharedLinks = async (
params: q.SharedLinksListParams,
): Promise<q.SharedLinksResponse> => {
const pageNumber = (params?.pageNumber ?? '1') || '1'; // Default to page 1 if not provided
const isPublic = params?.isPublic ?? true; // Default to true if not provided
return request.get(endpoints.getSharedLinks(pageNumber, isPublic));
const { pageSize, isPublic, sortBy, sortDirection, search, cursor } = params;
return request.get(
endpoints.getSharedLinks(pageSize, isPublic, sortBy, sortDirection, search, cursor),
);
};
export function getSharedLink(shareId: string): Promise<t.TSharedLinkResponse> {
return request.get(endpoints.shareMessages(shareId));
export function getSharedLink(conversationId: string): Promise<t.TSharedLinkGetResponse> {
return request.get(endpoints.getSharedLink(conversationId));
}
export function createSharedLink(payload: t.TSharedLinkRequest): Promise<t.TSharedLinkResponse> {
return request.post(endpoints.createSharedLink, payload);
export function createSharedLink(conversationId: string): Promise<t.TSharedLinkResponse> {
return request.post(endpoints.createSharedLink(conversationId));
}
export function updateSharedLink(payload: t.TSharedLinkRequest): Promise<t.TSharedLinkResponse> {
return request.patch(endpoints.updateSharedLink, payload);
export function updateSharedLink(shareId: string): Promise<t.TSharedLinkResponse> {
return request.patch(endpoints.updateSharedLink(shareId));
}
export function deleteSharedLink(shareId: string): Promise<t.TDeleteSharedLinkResponse> {
export function deleteSharedLink(shareId: string): Promise<m.TDeleteSharedLinkResponse> {
return request.delete(endpoints.shareMessages(shareId));
}

View file

@ -75,6 +75,29 @@ export const useGetSharedMessages = (
);
};
export const useGetSharedLinkQuery = (
conversationId: string,
config?: UseQueryOptions<t.TSharedLinkGetResponse>,
): QueryObserverResult<t.TSharedLinkGetResponse> => {
const queryClient = useQueryClient();
return useQuery<t.TSharedLinkGetResponse>(
[QueryKeys.sharedLinks, conversationId],
() => dataService.getSharedLink(conversationId),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
onSuccess: (data) => {
queryClient.setQueryData([QueryKeys.sharedLinks, conversationId], {
conversationId: data.conversationId,
shareId: data.shareId,
});
},
...config,
},
);
};
export const useGetUserBalance = (
config?: UseQueryOptions<string>,
): QueryObserverResult<string> => {
@ -306,15 +329,18 @@ export const useRegisterUserMutation = (
options?: m.RegistrationOptions,
): UseMutationResult<t.TError, unknown, t.TRegisterUser, unknown> => {
const queryClient = useQueryClient();
return useMutation((payload: t.TRegisterUser) => dataService.register(payload), {
...options,
onSuccess: (...args) => {
queryClient.invalidateQueries([QueryKeys.user]);
if (options?.onSuccess) {
options.onSuccess(...args);
}
return useMutation<t.TRegisterUserResponse, t.TError, t.TRegisterUser>(
(payload: t.TRegisterUser) => dataService.register(payload),
{
...options,
onSuccess: (...args) => {
queryClient.invalidateQueries([QueryKeys.user]);
if (options?.onSuccess) {
options.onSuccess(...args);
}
},
},
});
);
};
export const useRefreshTokenMutation = (): UseMutationResult<

View file

@ -719,13 +719,12 @@ export const tSharedLinkSchema = z.object({
conversationId: z.string(),
shareId: z.string(),
messages: z.array(z.string()),
isAnonymous: z.boolean(),
isPublic: z.boolean(),
isVisible: z.boolean(),
title: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
});
export type TSharedLink = z.infer<typeof tSharedLinkSchema>;
export const tConversationTagSchema = z.object({

View file

@ -170,15 +170,17 @@ export type TArchiveConversationResponse = TConversation;
export type TSharedMessagesResponse = Omit<TSharedLink, 'messages'> & {
messages: TMessage[];
};
export type TSharedLinkRequest = Partial<
Omit<TSharedLink, 'messages' | 'createdAt' | 'updatedAt'>
> & {
conversationId: string;
};
export type TSharedLinkResponse = TSharedLink;
export type TSharedLinksResponse = TSharedLink[];
export type TDeleteSharedLinkResponse = TSharedLink;
export type TCreateShareLinkRequest = Pick<TConversation, 'conversationId'>;
export type TUpdateShareLinkRequest = Pick<TSharedLink, 'shareId'>;
export type TSharedLinkResponse = Pick<TSharedLink, 'shareId'> &
Pick<TConversation, 'conversationId'>;
export type TSharedLinkGetResponse = TSharedLinkResponse & {
success: boolean;
};
// type for getting conversation tags
export type TConversationTagsResponse = TConversationTag[];
@ -203,12 +205,10 @@ export type TDuplicateConvoRequest = {
conversationId?: string;
};
export type TDuplicateConvoResponse =
| {
conversation: TConversation;
messages: TMessage[];
}
| undefined;
export type TDuplicateConvoResponse = {
conversation: TConversation;
messages: TMessage[];
};
export type TForkConvoRequest = {
messageId: string;

View file

@ -24,6 +24,12 @@ export type MutationOptions<
onSuccess?: (data: Response, variables: Request, context?: Context) => void;
onMutate?: (variables: Request) => Snapshot | Promise<Snapshot>;
onError?: (error: Error, variables: Request, context?: Context, snapshot?: Snapshot) => void;
onSettled?: (
data: Response | undefined,
error: Error | null,
variables: Request,
context?: Context,
) => void;
};
export type TGenTitleRequest = {
@ -186,7 +192,12 @@ export type ArchiveConvoOptions = MutationOptions<
types.TArchiveConversationRequest
>;
export type DeleteSharedLinkOptions = MutationOptions<types.TSharedLink, { shareId: string }>;
export type DeleteSharedLinkContext = { previousQueries?: Map<string, TDeleteSharedLinkResponse> };
export type DeleteSharedLinkOptions = MutationOptions<
TDeleteSharedLinkResponse,
{ shareId: string },
DeleteSharedLinkContext
>;
export type TUpdatePromptContext =
| {
@ -298,3 +309,9 @@ export type ToolCallMutationOptions<T extends ToolId> = MutationOptions<
ToolCallResponse,
ToolParams<T>
>;
export type TDeleteSharedLinkResponse = {
success: boolean;
shareId: string;
message: string;
};

View file

@ -41,23 +41,34 @@ export type ConversationUpdater = (
export type SharedMessagesResponse = Omit<s.TSharedLink, 'messages'> & {
messages: s.TMessage[];
};
export type SharedLinkListParams = Omit<ConversationListParams, 'isArchived' | 'conversationId'> & {
isPublic?: boolean;
export interface SharedLinksListParams {
pageSize: number;
isPublic: boolean;
sortBy: 'title' | 'createdAt';
sortDirection: 'asc' | 'desc';
search?: string;
cursor?: string;
}
export type SharedLinkItem = {
shareId: string;
title: string;
isPublic: boolean;
createdAt: Date;
conversationId: string;
};
export type SharedLinksResponse = Omit<ConversationListResponse, 'conversations' | 'messages'> & {
sharedLinks: s.TSharedLink[];
};
export interface SharedLinksResponse {
links: SharedLinkItem[];
nextCursor: string | null;
hasNextPage: boolean;
}
// Type for the response from the conversation list API
export type SharedLinkListResponse = {
sharedLinks: s.TSharedLink[];
pageNumber: string;
pageSize: string | number;
pages: string | number;
};
export type SharedLinkListData = InfiniteData<SharedLinkListResponse>;
export interface SharedLinkQueryData {
pages: SharedLinksResponse[];
pageParams: (string | null)[];
}
export type AllPromptGroupsFilterRequest = {
category: string;