mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-31 23:58:50 +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
|
|
@ -141,7 +141,6 @@ function ConvoOptions({
|
|||
/>
|
||||
{showShareDialog && (
|
||||
<ShareButton
|
||||
title={title ?? ''}
|
||||
conversationId={conversationId ?? ''}
|
||||
open={showShareDialog}
|
||||
onOpenChange={setShowShareDialog}
|
||||
|
|
|
|||
|
|
@ -1,112 +1,102 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { OGDialog } from '~/components/ui';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import type { TSharedLink } from 'librechat-data-provider';
|
||||
import { useCreateSharedLinkMutation } from '~/data-provider';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { Copy, CopyCheck } from 'lucide-react';
|
||||
import { useGetSharedLinkQuery } from 'librechat-data-provider/react-query';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { useLocalize, useCopyToClipboard } from '~/hooks';
|
||||
import { Button, Spinner, OGDialog } from '~/components';
|
||||
import SharedLinkButton from './SharedLinkButton';
|
||||
import { NotificationSeverity } from '~/common';
|
||||
import { Spinner } from '~/components/svg';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
export default function ShareButton({
|
||||
conversationId,
|
||||
title,
|
||||
open,
|
||||
onOpenChange,
|
||||
triggerRef,
|
||||
children,
|
||||
}: {
|
||||
conversationId: string;
|
||||
title: string;
|
||||
open: boolean;
|
||||
onOpenChange: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
triggerRef?: React.RefObject<HTMLButtonElement>;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
const { mutate, isLoading } = useCreateSharedLinkMutation();
|
||||
const [share, setShare] = useState<TSharedLink | null>(null);
|
||||
const [isUpdated, setIsUpdated] = useState(false);
|
||||
const [isNewSharedLink, setIsNewSharedLink] = useState(false);
|
||||
const [showQR, setShowQR] = useState(false);
|
||||
const [sharedLink, setSharedLink] = useState('');
|
||||
const [isCopying, setIsCopying] = useState(false);
|
||||
const { data: share, isLoading } = useGetSharedLinkQuery(conversationId);
|
||||
const copyLink = useCopyToClipboard({ text: sharedLink });
|
||||
|
||||
useEffect(() => {
|
||||
if (!open && triggerRef && triggerRef.current) {
|
||||
triggerRef.current.focus();
|
||||
if (share?.shareId !== undefined) {
|
||||
const link = `${window.location.protocol}//${window.location.host}/share/${share.shareId}`;
|
||||
setSharedLink(link);
|
||||
}
|
||||
}, [open, triggerRef]);
|
||||
}, [share]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || share) {
|
||||
return;
|
||||
}
|
||||
const data = {
|
||||
conversationId,
|
||||
title,
|
||||
isAnonymous: true,
|
||||
};
|
||||
const button =
|
||||
isLoading === true ? null : (
|
||||
<SharedLinkButton
|
||||
share={share}
|
||||
conversationId={conversationId}
|
||||
setShareDialogOpen={onOpenChange}
|
||||
showQR={showQR}
|
||||
setShowQR={setShowQR}
|
||||
setSharedLink={setSharedLink}
|
||||
/>
|
||||
);
|
||||
|
||||
mutate(data, {
|
||||
onSuccess: (result) => {
|
||||
setShare(result);
|
||||
setIsNewSharedLink(!result.isPublic);
|
||||
},
|
||||
onError: () => {
|
||||
showToast({
|
||||
message: localize('com_ui_share_error'),
|
||||
severity: NotificationSeverity.ERROR,
|
||||
showIcon: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// mutation.mutate should only be called once
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
if (!conversationId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const buttons = share && (
|
||||
<SharedLinkButton
|
||||
share={share}
|
||||
conversationId={conversationId}
|
||||
setShare={setShare}
|
||||
isUpdated={isUpdated}
|
||||
setIsUpdated={setIsUpdated}
|
||||
/>
|
||||
);
|
||||
const shareId = share?.shareId ?? '';
|
||||
|
||||
return (
|
||||
<OGDialog open={open} onOpenChange={onOpenChange} triggerRef={triggerRef}>
|
||||
{children}
|
||||
<OGDialogTemplate
|
||||
buttons={buttons}
|
||||
buttons={button}
|
||||
showCloseButton={true}
|
||||
showCancelButton={false}
|
||||
title={localize('com_ui_share_link_to_chat')}
|
||||
className="max-w-[550px]"
|
||||
main={
|
||||
<div>
|
||||
<div className="h-full py-2 text-gray-400 dark:text-gray-200">
|
||||
<div className="h-full py-2 text-text-primary">
|
||||
{(() => {
|
||||
if (isLoading) {
|
||||
if (isLoading === true) {
|
||||
return <Spinner className="m-auto h-14 animate-spin" />;
|
||||
}
|
||||
|
||||
if (isUpdated) {
|
||||
return isNewSharedLink
|
||||
? localize('com_ui_share_created_message')
|
||||
: localize('com_ui_share_updated_message');
|
||||
}
|
||||
|
||||
return share?.isPublic === true
|
||||
return share?.success === true
|
||||
? localize('com_ui_share_update_message')
|
||||
: localize('com_ui_share_create_message');
|
||||
})()}
|
||||
</div>
|
||||
<div className="relative items-center rounded-lg p-2">
|
||||
{showQR && (
|
||||
<div className="mb-4 flex flex-col items-center">
|
||||
<QRCodeSVG value={sharedLink} size={200} marginSize={2} className="rounded-2xl" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shareId && (
|
||||
<div className="flex items-center gap-2 rounded-md bg-surface-secondary p-2">
|
||||
<div className="flex-1 break-all text-sm text-text-secondary">{sharedLink}</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (isCopying) {
|
||||
return;
|
||||
}
|
||||
copyLink(setIsCopying);
|
||||
}}
|
||||
className={cn('shrink-0', isCopying ? 'cursor-default' : '')}
|
||||
>
|
||||
{isCopying ? <CopyCheck className="size-4" /> : <Copy className="size-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,31 +1,38 @@
|
|||
import { useState } from 'react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { Copy, Link } from 'lucide-react';
|
||||
import type { TSharedLink } from 'librechat-data-provider';
|
||||
import { useUpdateSharedLinkMutation } from '~/data-provider';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { QrCode, RotateCw, Trash2 } from 'lucide-react';
|
||||
import type { TSharedLinkGetResponse } from 'librechat-data-provider';
|
||||
import {
|
||||
useCreateSharedLinkMutation,
|
||||
useUpdateSharedLinkMutation,
|
||||
useDeleteSharedLinkMutation,
|
||||
} from '~/data-provider';
|
||||
import { Button, OGDialog, Spinner, TooltipAnchor, Label } from '~/components';
|
||||
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
|
||||
import { NotificationSeverity } from '~/common';
|
||||
import { useToastContext } from '~/Providers';
|
||||
import { Spinner } from '~/components/svg';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
export default function SharedLinkButton({
|
||||
conversationId,
|
||||
share,
|
||||
setShare,
|
||||
isUpdated,
|
||||
setIsUpdated,
|
||||
conversationId,
|
||||
setShareDialogOpen,
|
||||
showQR,
|
||||
setShowQR,
|
||||
setSharedLink,
|
||||
}: {
|
||||
share: TSharedLinkGetResponse | undefined;
|
||||
conversationId: string;
|
||||
share: TSharedLink;
|
||||
setShare: (share: TSharedLink) => void;
|
||||
isUpdated: boolean;
|
||||
setIsUpdated: (isUpdated: boolean) => void;
|
||||
setShareDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
showQR: boolean;
|
||||
setShowQR: (showQR: boolean) => void;
|
||||
setSharedLink: (sharedLink: string) => void;
|
||||
}) {
|
||||
const localize = useLocalize();
|
||||
const { showToast } = useToastContext();
|
||||
const [isCopying, setIsCopying] = useState(false);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const shareId = share?.shareId ?? '';
|
||||
|
||||
const { mutateAsync, isLoading } = useUpdateSharedLinkMutation({
|
||||
const { mutateAsync: mutate, isLoading: isCreateLoading } = useCreateSharedLinkMutation({
|
||||
onError: () => {
|
||||
showToast({
|
||||
message: localize('com_ui_share_error'),
|
||||
|
|
@ -35,92 +42,145 @@ export default function SharedLinkButton({
|
|||
},
|
||||
});
|
||||
|
||||
const copyLink = () => {
|
||||
if (!share) {
|
||||
return;
|
||||
}
|
||||
setIsCopying(true);
|
||||
const sharedLink =
|
||||
window.location.protocol + '//' + window.location.host + '/share/' + share.shareId;
|
||||
copy(sharedLink);
|
||||
setTimeout(() => {
|
||||
setIsCopying(false);
|
||||
}, 1500);
|
||||
};
|
||||
const { mutateAsync, isLoading: isUpdateLoading } = useUpdateSharedLinkMutation({
|
||||
onError: () => {
|
||||
showToast({
|
||||
message: localize('com_ui_share_error'),
|
||||
severity: NotificationSeverity.ERROR,
|
||||
showIcon: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useDeleteSharedLinkMutation({
|
||||
onSuccess: async () => {
|
||||
setShowDeleteDialog(false);
|
||||
setShareDialogOpen(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Delete error:', error);
|
||||
showToast({
|
||||
message: localize('com_ui_share_delete_error'),
|
||||
severity: NotificationSeverity.ERROR,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const generateShareLink = useCallback((shareId: string) => {
|
||||
return `${window.location.protocol}//${window.location.host}/share/${shareId}`;
|
||||
}, []);
|
||||
|
||||
const updateSharedLink = async () => {
|
||||
if (!share) {
|
||||
if (!shareId) {
|
||||
return;
|
||||
}
|
||||
const result = await mutateAsync({
|
||||
shareId: share.shareId,
|
||||
conversationId: conversationId,
|
||||
isPublic: true,
|
||||
isVisible: true,
|
||||
isAnonymous: true,
|
||||
});
|
||||
|
||||
if (result) {
|
||||
setShare(result);
|
||||
setIsUpdated(true);
|
||||
copyLink();
|
||||
}
|
||||
};
|
||||
const getHandler = () => {
|
||||
if (isUpdated) {
|
||||
return {
|
||||
handler: () => {
|
||||
copyLink();
|
||||
},
|
||||
label: (
|
||||
<>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
{localize('com_ui_copy_link')}
|
||||
</>
|
||||
),
|
||||
};
|
||||
}
|
||||
if (share.isPublic) {
|
||||
return {
|
||||
handler: async () => {
|
||||
await updateSharedLink();
|
||||
},
|
||||
|
||||
label: (
|
||||
<>
|
||||
<Link className="mr-2 h-4 w-4" />
|
||||
{localize('com_ui_update_link')}
|
||||
</>
|
||||
),
|
||||
};
|
||||
}
|
||||
return {
|
||||
handler: updateSharedLink,
|
||||
label: (
|
||||
<>
|
||||
<Link className="mr-2 h-4 w-4" />
|
||||
{localize('com_ui_create_link')}
|
||||
</>
|
||||
),
|
||||
};
|
||||
const updateShare = await mutateAsync({ shareId });
|
||||
const newLink = generateShareLink(updateShare.shareId);
|
||||
setSharedLink(newLink);
|
||||
};
|
||||
|
||||
const createShareLink = async () => {
|
||||
const share = await mutate({ conversationId });
|
||||
const newLink = generateShareLink(share.shareId);
|
||||
setSharedLink(newLink);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!shareId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteMutation.mutateAsync({ shareId });
|
||||
showToast({
|
||||
message: localize('com_ui_shared_link_delete_success'),
|
||||
severity: NotificationSeverity.SUCCESS,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to delete shared link:', error);
|
||||
showToast({
|
||||
message: localize('com_ui_share_delete_error'),
|
||||
severity: NotificationSeverity.ERROR,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handlers = getHandler();
|
||||
return (
|
||||
<button
|
||||
disabled={isLoading || isCopying}
|
||||
onClick={() => {
|
||||
handlers.handler();
|
||||
}}
|
||||
className="btn btn-primary flex items-center justify-center"
|
||||
>
|
||||
{isCopying && (
|
||||
<>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
{localize('com_ui_copied')}
|
||||
</>
|
||||
)}
|
||||
{!isCopying && !isLoading && handlers.label}
|
||||
{!isCopying && isLoading && <Spinner className="h-4 w-4" />}
|
||||
</button>
|
||||
<>
|
||||
<div className="flex gap-2">
|
||||
{!shareId && (
|
||||
<Button disabled={isCreateLoading} variant="submit" onClick={createShareLink}>
|
||||
{!isCreateLoading && localize('com_ui_create_link')}
|
||||
{isCreateLoading && <Spinner className="size-4" />}
|
||||
</Button>
|
||||
)}
|
||||
{shareId && (
|
||||
<div className="flex items-center gap-2">
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_refresh_link')}
|
||||
render={(props) => (
|
||||
<Button
|
||||
{...props}
|
||||
onClick={() => updateSharedLink()}
|
||||
variant="outline"
|
||||
disabled={isUpdateLoading}
|
||||
>
|
||||
{isUpdateLoading ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<RotateCw className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
|
||||
<TooltipAnchor
|
||||
description={showQR ? localize('com_ui_hide_qr') : localize('com_ui_show_qr')}
|
||||
render={(props) => (
|
||||
<Button {...props} onClick={() => setShowQR(!showQR)} variant="outline">
|
||||
<QrCode className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
|
||||
<TooltipAnchor
|
||||
description={localize('com_ui_delete')}
|
||||
render={(props) => (
|
||||
<Button {...props} onClick={() => setShowDeleteDialog(true)} variant="destructive">
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<OGDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<OGDialogTemplate
|
||||
showCloseButton={false}
|
||||
title={localize('com_ui_delete_shared_link')}
|
||||
className="max-w-[450px]"
|
||||
main={
|
||||
<>
|
||||
<div className="flex w-full flex-col items-center gap-2">
|
||||
<div className="grid w-full items-center gap-2">
|
||||
<Label
|
||||
htmlFor="dialog-confirm-delete"
|
||||
className="text-left text-sm font-medium"
|
||||
>
|
||||
{localize('com_ui_delete_confirm')} <strong>"{shareId}"</strong>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
selection={{
|
||||
selectHandler: handleDelete,
|
||||
selectClasses:
|
||||
'bg-red-700 dark:bg-red-600 hover:bg-red-800 dark:hover:bg-red-800 text-white',
|
||||
selectText: localize('com_ui_delete'),
|
||||
}}
|
||||
/>
|
||||
</OGDialog>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue