Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 61 additions & 19 deletions frontend/src/components/CollectionNavigation.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import React from "react"
import { ChevronRight } from "lucide-react"
import { ChevronRight, AlertOctagon } from "lucide-react"
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"
import { api } from "@/api"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"

interface CollectionNavigationProps {
navigationPath: Array<{
Expand Down Expand Up @@ -49,6 +55,28 @@ interface CollectionNavigationProps {
navigateToCl: (clId: string, clName: string) => Promise<void>
}

// Helper function to check if an item should be non-selectable based on upload status
function isItemNonSelectable(item: { uploadStatus?: string }): boolean {
const uploadStatus = item.uploadStatus
return uploadStatus === "pending" || uploadStatus === "processing" || uploadStatus === "failed"
}

// Reusable indexing tooltip component
const IndexingTooltip = () => {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<AlertOctagon className="w-4 h-4 ml-2 text-gray-500" />
</TooltipTrigger>
<TooltipContent>
<p>Indexing is in progress</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}

// Utility function to check if an item is selected either directly or through parent inheritance
function isItemSelectedWithInheritance(
item: any,
Expand Down Expand Up @@ -198,10 +226,14 @@ export const CollectionNavigation: React.FC<CollectionNavigationProps> = ({
)

const isInherited = isSelected && !isDirectlySelected
const isNonSelectable = isItemNonSelectable(result)

const handleResultSelect = () => {
// Don't allow selection changes for inherited items
if (isInherited) return

// For non-selectable items, prevent all interactions (no navigation or selection)
if (isNonSelectable) return

if (result.type === "collection") {
// Toggle collection selection
Expand Down Expand Up @@ -262,25 +294,25 @@ export const CollectionNavigation: React.FC<CollectionNavigationProps> = ({
return (
<div
key={result.id}
onClick={isInherited ? undefined : handleResultSelect}
onClick={isInherited || isNonSelectable ? undefined : handleResultSelect}
className={`flex items-center px-4 py-2 text-sm ${
isInherited
? "cursor-default opacity-75"
isInherited || isNonSelectable
? "cursor-not-allowed opacity-50"
: "cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800"
}`}
>
<input
type="checkbox"
checked={isSelected || false}
disabled={isInherited}
disabled={isInherited || isNonSelectable}
onChange={() => {}}
className={`w-4 h-4 mr-3 ${isInherited ? "opacity-60" : ""}`}
className={`w-4 h-4 mr-3 ${isInherited || isNonSelectable ? "opacity-60" : ""}`}
/>
<div className="flex-1 min-w-0">
<div className="flex items-center">
<span className="text-gray-700 dark:text-gray-200 truncate">
{result.name}
</span>
<span className={`truncate ${isNonSelectable ? "text-gray-400 dark:text-gray-500" : "text-gray-700 dark:text-gray-200"}`}>
{result.name}
</span>
<span className="text-xs text-gray-500 dark:text-gray-400 ml-2 px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded">
{result.type}
</span>
Expand All @@ -289,6 +321,7 @@ export const CollectionNavigation: React.FC<CollectionNavigationProps> = ({
Selected
</span>
)}
{isNonSelectable && <IndexingTooltip />}
</div>
{result.collectionName &&
result.type !== "collection" && (
Expand Down Expand Up @@ -370,13 +403,20 @@ export const CollectionNavigation: React.FC<CollectionNavigationProps> = ({
Loading...
</div>
) : currentItems.length > 0 ? (
currentItems.map((item: any) => (
currentItems.map((item: any) => {
const isNonSelectable = isItemNonSelectable(item)

return (
<div
key={item.id}
className="flex items-center px-4 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800"
className={`flex items-center px-4 py-2 text-sm ${
isNonSelectable
? "cursor-not-allowed opacity-50"
: "cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800"
}`}
onClick={() => {
if (item.type === "folder") {
// When navigating to a folder, if it's selected, auto-select all children
if (item.type === "folder" && !isNonSelectable) {
// Only allow navigation to folder if it's selectable
navigateToFolder(item.id, item.name)
}
}}
Expand Down Expand Up @@ -425,7 +465,7 @@ export const CollectionNavigation: React.FC<CollectionNavigationProps> = ({
isSelected || isInheritedFromParent,
)
const isDisabled: boolean = Boolean(
isInheritedFromParent && !isSelected,
(isInheritedFromParent && !isSelected) || isNonSelectable,
)

return (
Expand All @@ -435,7 +475,7 @@ export const CollectionNavigation: React.FC<CollectionNavigationProps> = ({
disabled={isDisabled}
onChange={(e) => {
e.stopPropagation()
if (isDisabled) return // Prevent changes if inherited from parent
if (isDisabled) return // Prevent changes if inherited from parent or non-selectable

const isCurrentlySelected = selectedSet.has(item.id)

Expand Down Expand Up @@ -557,19 +597,21 @@ export const CollectionNavigation: React.FC<CollectionNavigationProps> = ({
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="mr-2 text-gray-800"
className={`mr-2 ${isNonSelectable ? "text-gray-400" : "text-gray-800"}`}
>
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
</svg>
)}
<span className="text-gray-700 dark:text-gray-200 truncate flex-1">
<span className={`truncate flex-1 ${isNonSelectable ? "text-gray-400 dark:text-gray-500" : "text-gray-700 dark:text-gray-200"}`}>
{item.name}
</span>
{item.type === "folder" && (
{isNonSelectable && <IndexingTooltip />}
{item.type === "folder" && !isNonSelectable && (
<ChevronRight className="h-4 w-4 text-gray-400 ml-2" />
)}
</div>
))
)
})
) : (
<div className="px-4 py-8 text-sm text-gray-500 dark:text-gray-400 text-center">
No items found
Expand Down
78 changes: 78 additions & 0 deletions frontend/src/components/FileTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import {
Plus,
Download,
Trash2,
Check,
Loader2,
AlertOctagon,
RotateCcw,
} from "lucide-react"
import {
Tooltip,
Expand Down Expand Up @@ -51,13 +55,48 @@ const truncateEmail = (email: string, maxLength: number = 20): string => {
return email.substring(0, maxLength - 3) + "..."
}

// Reusable upload status indicator component
const UploadStatusIndicator = ({
uploadStatus,
statusMessage
}: {
uploadStatus: string
statusMessage?: string
}) => {
return (
<div className="flex-shrink-0">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
{uploadStatus === "completed" && (
<Check size={14} className="text-green-600 dark:text-green-400" />
)}
{(uploadStatus === "processing" || uploadStatus === "pending") && (
<Loader2 size={14} className="text-black dark:text-white animate-spin" />
)}
{uploadStatus === "failed" && (
<AlertOctagon size={14} className="text-red-500" />
)}
</div>
</TooltipTrigger>
<TooltipContent>
<p>{statusMessage || uploadStatus}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
)
}

interface FileTreeProps {
items: FileNode[]
onAddFiles: (node: FileNode, path: string) => void
onDelete: (node: FileNode, path: string) => void
onToggle: (node: FileNode) => void
onFileClick: (node: FileNode) => void
onDownload?: (node: FileNode, path: string) => void
onRetry?: (node: FileNode, path: string) => void
}

const FileTree = ({
Expand All @@ -67,6 +106,7 @@ const FileTree = ({
onToggle,
onFileClick,
onDownload,
onRetry,
}: FileTreeProps) => {
return (
<div className="mt-2">
Expand All @@ -79,6 +119,7 @@ const FileTree = ({
onToggle={onToggle}
onFileClick={onFileClick}
onDownload={onDownload}
onRetry={onRetry}
/>
))}
</div>
Expand All @@ -94,6 +135,7 @@ const FileNodeComponent = ({
onToggle,
onFileClick,
onDownload,
onRetry,
}: {
node: FileNode
level?: number
Expand All @@ -103,6 +145,7 @@ const FileNodeComponent = ({
onToggle: (node: FileNode) => void
onFileClick: (node: FileNode) => void
onDownload?: (node: FileNode, path: string) => void
onRetry?: (node: FileNode, path: string) => void
}) => {
const [isHovered, setIsHovered] = useState(false)

Expand Down Expand Up @@ -136,6 +179,13 @@ const FileNodeComponent = ({
>
{node.name}
</span>
{/* Upload status indicator for folders */}
{node.uploadStatus && (
<UploadStatusIndicator
uploadStatus={node.uploadStatus}
statusMessage={node.statusMessage}
/>
)}
</div>
) : (
<div
Expand All @@ -149,6 +199,13 @@ const FileNodeComponent = ({
>
{node.name}
</span>
{/* Upload status indicator */}
{node.uploadStatus && (
<UploadStatusIndicator
uploadStatus={node.uploadStatus}
statusMessage={node.statusMessage}
/>
)}
</div>
)}
</div>
Expand Down Expand Up @@ -176,6 +233,26 @@ const FileNodeComponent = ({
}}
/>
)}
{(node.retryCount ?? 0) > 3 && onRetry && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<RotateCcw
size={16}
className="cursor-pointer text-gray-700 dark:text-gray-200 flex-shrink-0"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
onRetry(node, currentPath)
}}
/>
</TooltipTrigger>
<TooltipContent>
<p>Retry</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<Trash2
size={16}
className="cursor-pointer"
Expand Down Expand Up @@ -238,6 +315,7 @@ const FileNodeComponent = ({
onToggle={onToggle}
onFileClick={onFileClick}
onDownload={onDownload}
onRetry={onRetry}
/>
))}
</div>
Expand Down
Loading