Merge pull request #518 from kubet/fix/pointer-native-event

Fix/pointer native event
This commit is contained in:
kubet 2025-05-26 19:52:34 +02:00 committed by GitHub
commit 606bff01f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 92 additions and 75 deletions

View File

@ -61,38 +61,38 @@ export function NavAgents() {
const { performDelete } = useDeleteOperation(); const { performDelete } = useDeleteOperation();
const isPerformingActionRef = useRef(false); const isPerformingActionRef = useRef(false);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [selectedThreads, setSelectedThreads] = useState<Set<string>>(new Set()); const [selectedThreads, setSelectedThreads] = useState<Set<string>>(new Set());
const [deleteProgress, setDeleteProgress] = useState(0); const [deleteProgress, setDeleteProgress] = useState(0);
const [totalToDelete, setTotalToDelete] = useState(0); const [totalToDelete, setTotalToDelete] = useState(0);
const { const {
data: projects = [], data: projects = [],
isLoading: isProjectsLoading, isLoading: isProjectsLoading,
error: projectsError error: projectsError
} = useProjects(); } = useProjects();
const { const {
data: threads = [], data: threads = [],
isLoading: isThreadsLoading, isLoading: isThreadsLoading,
error: threadsError error: threadsError
} = useThreads(); } = useThreads();
const { mutate: deleteThreadMutation, isPending: isDeletingSingle } = useDeleteThread(); const { mutate: deleteThreadMutation, isPending: isDeletingSingle } = useDeleteThread();
const { const {
mutate: deleteMultipleThreadsMutation, mutate: deleteMultipleThreadsMutation,
isPending: isDeletingMultiple isPending: isDeletingMultiple
} = useDeleteMultipleThreads(); } = useDeleteMultipleThreads();
const combinedThreads: ThreadWithProject[] = const combinedThreads: ThreadWithProject[] =
!isProjectsLoading && !isThreadsLoading ? !isProjectsLoading && !isThreadsLoading ?
processThreadsWithProjects(threads, projects) : []; processThreadsWithProjects(threads, projects) : [];
const handleDeletionProgress = (completed: number, total: number) => { const handleDeletionProgress = (completed: number, total: number) => {
const percentage = (completed / total) * 100; const percentage = (completed / total) * 100;
setDeleteProgress(percentage); setDeleteProgress(percentage);
}; };
useEffect(() => { useEffect(() => {
const handleProjectUpdate = (event: Event) => { const handleProjectUpdate = (event: Event) => {
const customEvent = event as CustomEvent; const customEvent = event as CustomEvent;
@ -145,7 +145,7 @@ export function NavAgents() {
e.preventDefault(); e.preventDefault();
return; return;
} }
e.preventDefault() e.preventDefault()
setLoadingThreadId(threadId) setLoadingThreadId(threadId)
router.push(url) router.push(url)
@ -157,7 +157,7 @@ export function NavAgents() {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
} }
setSelectedThreads(prev => { setSelectedThreads(prev => {
const newSelection = new Set(prev); const newSelection = new Set(prev);
if (newSelection.has(threadId)) { if (newSelection.has(threadId)) {
@ -189,18 +189,18 @@ export function NavAgents() {
// Function to handle multi-delete // Function to handle multi-delete
const handleMultiDelete = () => { const handleMultiDelete = () => {
if (selectedThreads.size === 0) return; if (selectedThreads.size === 0) return;
// Get thread names for confirmation dialog // Get thread names for confirmation dialog
const threadsToDelete = combinedThreads.filter(t => selectedThreads.has(t.threadId)); const threadsToDelete = combinedThreads.filter(t => selectedThreads.has(t.threadId));
const threadNames = threadsToDelete.map(t => t.projectName).join(", "); const threadNames = threadsToDelete.map(t => t.projectName).join(", ");
setThreadToDelete({ setThreadToDelete({
id: "multiple", id: "multiple",
name: selectedThreads.size > 3 name: selectedThreads.size > 3
? `${selectedThreads.size} conversations` ? `${selectedThreads.size} conversations`
: threadNames : threadNames
}); });
setTotalToDelete(selectedThreads.size); setTotalToDelete(selectedThreads.size);
setDeleteProgress(0); setDeleteProgress(0);
setIsDeleteDialogOpen(true); setIsDeleteDialogOpen(true);
@ -261,10 +261,10 @@ export function NavAgents() {
// Multi-thread deletion // Multi-thread deletion
const threadIdsToDelete = Array.from(selectedThreads); const threadIdsToDelete = Array.from(selectedThreads);
const isActiveThreadIncluded = threadIdsToDelete.some(id => pathname?.includes(id)); const isActiveThreadIncluded = threadIdsToDelete.some(id => pathname?.includes(id));
// Show initial toast // Show initial toast
toast.info(`Deleting ${threadIdsToDelete.length} conversations...`); toast.info(`Deleting ${threadIdsToDelete.length} conversations...`);
try { try {
// If the active thread is included, handle navigation first // If the active thread is included, handle navigation first
if (isActiveThreadIncluded) { if (isActiveThreadIncluded) {
@ -272,14 +272,14 @@ export function NavAgents() {
isNavigatingRef.current = true; isNavigatingRef.current = true;
document.body.style.pointerEvents = 'none'; document.body.style.pointerEvents = 'none';
router.push('/dashboard'); router.push('/dashboard');
// Wait a moment for navigation to start // Wait a moment for navigation to start
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => setTimeout(resolve, 100));
} }
// Use the mutation for bulk deletion // Use the mutation for bulk deletion
deleteMultipleThreadsMutation( deleteMultipleThreadsMutation(
{ {
threadIds: threadIdsToDelete, threadIds: threadIdsToDelete,
onProgress: handleDeletionProgress onProgress: handleDeletionProgress
}, },
@ -287,15 +287,15 @@ export function NavAgents() {
onSuccess: (data) => { onSuccess: (data) => {
// Invalidate queries to refresh the list // Invalidate queries to refresh the list
queryClient.invalidateQueries({ queryKey: threadKeys.lists() }); queryClient.invalidateQueries({ queryKey: threadKeys.lists() });
// Show success message // Show success message
toast.success(`Successfully deleted ${data.successful.length} conversations`); toast.success(`Successfully deleted ${data.successful.length} conversations`);
// If some deletions failed, show warning // If some deletions failed, show warning
if (data.failed.length > 0) { if (data.failed.length > 0) {
toast.warning(`Failed to delete ${data.failed.length} conversations`); toast.warning(`Failed to delete ${data.failed.length} conversations`);
} }
// Reset states // Reset states
setSelectedThreads(new Set()); setSelectedThreads(new Set());
setDeleteProgress(0); setDeleteProgress(0);
@ -316,7 +316,7 @@ export function NavAgents() {
} catch (err) { } catch (err) {
console.error('Error initiating bulk deletion:', err); console.error('Error initiating bulk deletion:', err);
toast.error('Error initiating deletion process'); toast.error('Error initiating deletion process');
// Reset states // Reset states
setSelectedThreads(new Set()); setSelectedThreads(new Set());
setThreadToDelete(null); setThreadToDelete(null);
@ -330,7 +330,7 @@ export function NavAgents() {
// Loading state or error handling // Loading state or error handling
const isLoading = isProjectsLoading || isThreadsLoading; const isLoading = isProjectsLoading || isThreadsLoading;
const hasError = projectsError || threadsError; const hasError = projectsError || threadsError;
if (hasError) { if (hasError) {
console.error('Error loading data:', { projectsError, threadsError }); console.error('Error loading data:', { projectsError, threadsError });
} }
@ -343,16 +343,16 @@ export function NavAgents() {
<div className="flex items-center space-x-1"> <div className="flex items-center space-x-1">
{selectedThreads.size > 0 ? ( {selectedThreads.size > 0 ? (
<> <>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={deselectAllThreads} onClick={deselectAllThreads}
className="h-7 w-7" className="h-7 w-7"
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={selectAllThreads} onClick={selectAllThreads}
disabled={selectedThreads.size === combinedThreads.length} disabled={selectedThreads.size === combinedThreads.length}
@ -360,8 +360,8 @@ export function NavAgents() {
> >
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={handleMultiDelete} onClick={handleMultiDelete}
className="h-7 w-7 text-destructive" className="h-7 w-7 text-destructive"
@ -436,8 +436,8 @@ export function NavAgents() {
<SidebarMenuButton <SidebarMenuButton
asChild asChild
className={ className={
isActive ? 'bg-accent text-accent-foreground' : isActive ? 'bg-accent text-accent-foreground' :
isSelected ? 'bg-primary/10' : '' isSelected ? 'bg-primary/10' : ''
} }
> >
<Link <Link
@ -462,13 +462,12 @@ export function NavAgents() {
<div className="relative"> <div className="relative">
<SidebarMenuButton <SidebarMenuButton
asChild asChild
className={`relative ${ className={`relative ${isActive
isActive ? 'bg-accent text-accent-foreground font-medium'
? 'bg-accent text-accent-foreground font-medium' : isSelected
: isSelected ? 'bg-primary/10'
? 'bg-primary/10'
: '' : ''
}`} }`}
> >
<Link <Link
href={thread.url} href={thread.url}
@ -484,27 +483,24 @@ export function NavAgents() {
) : ( ) : (
<> <>
{/* MessagesSquare icon - hidden on hover if not selected */} {/* MessagesSquare icon - hidden on hover if not selected */}
<MessagesSquare <MessagesSquare
className={`h-4 w-4 transition-opacity duration-150 ${ className={`h-4 w-4 transition-opacity duration-150 ${isSelected ? 'opacity-0' : 'opacity-100 group-hover/icon:opacity-0'
isSelected ? 'opacity-0' : 'opacity-100 group-hover/icon:opacity-0' }`}
}`}
/> />
{/* Checkbox - appears on hover or when selected */} {/* Checkbox - appears on hover or when selected */}
<div <div
className={`absolute inset-0 flex items-center justify-center transition-opacity duration-150 ${ className={`absolute inset-0 flex items-center justify-center transition-opacity duration-150 ${isSelected
isSelected ? 'opacity-100'
? 'opacity-100' : 'opacity-0 group-hover/icon:opacity-100'
: 'opacity-0 group-hover/icon:opacity-100' }`}
}`}
onClick={(e) => toggleThreadSelection(thread.threadId, e)} onClick={(e) => toggleThreadSelection(thread.threadId, e)}
> >
<div <div
className={`h-4 w-4 border rounded cursor-pointer hover:bg-muted/50 transition-colors flex items-center justify-center ${ className={`h-4 w-4 border rounded cursor-pointer hover:bg-muted/50 transition-colors flex items-center justify-center ${isSelected
isSelected ? 'bg-primary border-primary'
? 'bg-primary border-primary' : 'border-muted-foreground/30 bg-background'
: 'border-muted-foreground/30 bg-background' }`}
}`}
> >
{isSelected && <Check className="h-3 w-3 text-primary-foreground" />} {isSelected && <Check className="h-3 w-3 text-primary-foreground" />}
</div> </div>
@ -520,7 +516,14 @@ export function NavAgents() {
{state !== 'collapsed' && !isSelected && ( {state !== 'collapsed' && !isSelected && (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<SidebarMenuAction showOnHover className="group-hover:opacity-100"> <SidebarMenuAction
showOnHover
className="group-hover:opacity-100"
onClick={() => {
// Ensure pointer events are enabled when dropdown opens
document.body.style.pointerEvents = 'auto';
}}
>
<MoreHorizontal /> <MoreHorizontal />
<span className="sr-only">More</span> <span className="sr-only">More</span>
</SidebarMenuAction> </SidebarMenuAction>
@ -575,21 +578,21 @@ export function NavAgents() {
</SidebarMenuItem> </SidebarMenuItem>
)} )}
</SidebarMenu> </SidebarMenu>
{(isDeletingSingle || isDeletingMultiple) && totalToDelete > 0 && ( {(isDeletingSingle || isDeletingMultiple) && totalToDelete > 0 && (
<div className="mt-2 px-2"> <div className="mt-2 px-2">
<div className="text-xs text-muted-foreground mb-1"> <div className="text-xs text-muted-foreground mb-1">
Deleting {deleteProgress > 0 ? `(${Math.floor(deleteProgress)}%)` : '...'} Deleting {deleteProgress > 0 ? `(${Math.floor(deleteProgress)}%)` : '...'}
</div> </div>
<div className="w-full bg-secondary h-1 rounded-full overflow-hidden"> <div className="w-full bg-secondary h-1 rounded-full overflow-hidden">
<div <div
className="bg-primary h-1 transition-all duration-300 ease-in-out" className="bg-primary h-1 transition-all duration-300 ease-in-out"
style={{ width: `${deleteProgress}%` }} style={{ width: `${deleteProgress}%` }}
/> />
</div> </div>
</div> </div>
)} )}
<ShareModal <ShareModal
isOpen={showShareModal} isOpen={showShareModal}
onClose={() => setShowShareModal(false)} onClose={() => setShowShareModal(false)}

View File

@ -28,7 +28,14 @@ export function ShareModal({ isOpen, onClose, threadId, projectId }: ShareModalP
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isChecking, setIsChecking] = useState(false); const [isChecking, setIsChecking] = useState(false);
const [isCopying, setIsCopying] = useState(false); const [isCopying, setIsCopying] = useState(false);
// Reset pointer events when modal opens
useEffect(() => {
if (isOpen) {
document.body.style.pointerEvents = 'auto';
}
}, [isOpen]);
const updateThreadMutation = useUpdateThreadMutation(); const updateThreadMutation = useUpdateThreadMutation();
useEffect(() => { useEffect(() => {
@ -105,7 +112,7 @@ export function ShareModal({ isOpen, onClose, threadId, projectId }: ShareModalP
const updatePublicStatus = async (isPublic: boolean) => { const updatePublicStatus = async (isPublic: boolean) => {
console.log("Updating public status for thread:", threadId, "and project:", projectId, "to", isPublic); console.log("Updating public status for thread:", threadId, "and project:", projectId, "to", isPublic);
if (!threadId) return; if (!threadId) return;
await updateProject(projectId, { is_public: isPublic }); await updateProject(projectId, { is_public: isPublic });
await updateThreadMutation.mutateAsync({ await updateThreadMutation.mutateAsync({
threadId, threadId,

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import React from 'react'; import React, { useEffect } from 'react';
import { Loader2 } from 'lucide-react'; import { Loader2 } from 'lucide-react';
import { import {
@ -32,6 +32,13 @@ export function DeleteConfirmationDialog({
threadName, threadName,
isDeleting, isDeleting,
}: DeleteConfirmationDialogProps) { }: DeleteConfirmationDialogProps) {
// Reset pointer events when dialog opens
useEffect(() => {
if (isOpen) {
document.body.style.pointerEvents = 'auto';
}
}, [isOpen]);
return ( return (
<AlertDialog open={isOpen} onOpenChange={onClose}> <AlertDialog open={isOpen} onOpenChange={onClose}>
<AlertDialogContent> <AlertDialogContent>

View File

@ -96,7 +96,7 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
}, [value, ref]); }, [value, ref]);
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
e.preventDefault(); e.preventDefault();
if ( if (
(value.trim() || uploadedFiles.length > 0) && (value.trim() || uploadedFiles.length > 0) &&