diff --git a/frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx b/frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx index 177d9b823..cc267170b 100644 --- a/frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx +++ b/frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/WorkspaceFileRow/index.jsx @@ -19,8 +19,13 @@ export default function WorkspaceFileRow({ fetchKeys, hasChanges, movedItems, + selected, + toggleSelection, + disableSelection, + setSelectedItems, }) { - const onRemoveClick = async () => { + const onRemoveClick = async (e) => { + e.stopPropagation(); setLoading(true); try { @@ -33,24 +38,49 @@ export default function WorkspaceFileRow({ } catch (error) { console.error("Failed to remove document:", error); } - + setSelectedItems({}); setLoadingMessage(""); setLoading(false); }; + function toggleRowSelection(e) { + if (disableSelection) return; + e.stopPropagation(); + toggleSelection(); + } + + function handleRowSelection(e) { + e.stopPropagation(); + toggleSelection(); + } + const isMovedItem = movedItems?.some((movedItem) => movedItem.id === item.id); return ( <div - className={`items-center text-white/80 text-xs grid grid-cols-12 py-2 pl-3.5 pr-8 hover:bg-sky-500/20 cursor-pointer ${ - isMovedItem ? "bg-green-800/40" : "file-row" - }`} + className={`items-center h-[34px] text-white/80 text-xs grid grid-cols-12 py-2 pl-3.5 pr-8 ${ + !disableSelection ? "hover:bg-sky-500/20 cursor-pointer" : "" + } ${isMovedItem ? "bg-green-800/40" : "file-row"} ${selected ? "selected" : ""}`} + onClick={toggleRowSelection} > <div + className="col-span-10 w-fit flex gap-x-[2px] items-center relative" data-tooltip-id={`ws-directory-item-${item.url}`} - className="col-span-10 w-fit flex gap-x-[4px] items-center relative" > + <div className="shrink-0 w-3 h-3"> + {!disableSelection ? ( + <div + className="w-full h-full rounded border-[1px] border-white flex justify-center items-center cursor-pointer" + role="checkbox" + aria-checked={selected} + tabIndex={0} + onClick={handleRowSelection} + > + {selected && <div className="w-2 h-2 bg-white rounded-[2px]" />} + </div> + ) : null} + </div> <File - className="shrink-0 text-base font-bold w-4 h-4 mr-[3px] ml-3" + className="shrink-0 text-base font-bold w-4 h-4 mr-[3px] ml-1" weight="fill" /> <p className="whitespace-nowrap overflow-hidden text-ellipsis"> @@ -105,8 +135,9 @@ const PinItemToWorkspace = memo(({ workspace, docPath, item }) => { const [hover, setHover] = useState(false); const pinEvent = new CustomEvent("pinned_document"); - const updatePinStatus = async () => { + const updatePinStatus = async (e) => { try { + e.stopPropagation(); if (!pinned) window.dispatchEvent(pinEvent); const success = await Workspace.setPinForDocument( workspace.slug, diff --git a/frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/index.jsx b/frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/index.jsx index 4ec3a86ed..e9864f12c 100644 --- a/frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/index.jsx +++ b/frontend/src/components/Modals/ManageWorkspace/Documents/WorkspaceDirectory/index.jsx @@ -7,6 +7,7 @@ import { Eye, PushPin } from "@phosphor-icons/react"; import { SEEN_DOC_PIN_ALERT, SEEN_WATCH_ALERT } from "@/utils/constants"; import paths from "@/utils/paths"; import { Link } from "react-router-dom"; +import Workspace from "@/models/workspace"; function WorkspaceDirectory({ workspace, @@ -22,6 +23,66 @@ function WorkspaceDirectory({ embeddingCosts, movedItems, }) { + const [selectedItems, setSelectedItems] = useState({}); + + const toggleSelection = (item) => { + setSelectedItems((prevSelectedItems) => { + const newSelectedItems = { ...prevSelectedItems }; + if (newSelectedItems[item.id]) { + delete newSelectedItems[item.id]; + } else { + newSelectedItems[item.id] = true; + } + return newSelectedItems; + }); + }; + + const toggleSelectAll = () => { + const allItems = files.items.flatMap((folder) => folder.items); + const allSelected = allItems.every((item) => selectedItems[item.id]); + if (allSelected) { + setSelectedItems({}); + } else { + const newSelectedItems = {}; + allItems.forEach((item) => { + newSelectedItems[item.id] = true; + }); + setSelectedItems(newSelectedItems); + } + }; + + const removeSelectedItems = async () => { + setLoading(true); + setLoadingMessage("Removing selected files from workspace"); + + const itemsToRemove = Object.keys(selectedItems).map((itemId) => { + const folder = files.items.find((f) => + f.items.some((i) => i.id === itemId) + ); + const item = folder.items.find((i) => i.id === itemId); + return `${folder.name}/${item.name}`; + }); + + try { + await Workspace.modifyEmbeddings(workspace.slug, { + adds: [], + deletes: itemsToRemove, + }); + await fetchKeys(true); + setSelectedItems({}); + } catch (error) { + console.error("Failed to remove documents:", error); + } + + setLoadingMessage(""); + setLoading(false); + }; + + const handleSaveChanges = (e) => { + setSelectedItems({}); + saveChanges(e); + }; + if (loading) { return ( <div className="px-8"> @@ -31,11 +92,14 @@ function WorkspaceDirectory({ </h3> </div> <div className="relative w-[560px] h-[445px] bg-zinc-900 rounded-2xl mt-5"> - <div className="text-white/80 text-xs grid grid-cols-12 py-2 px-8"> - <p className="col-span-5">Name</p> + <div className="text-white/80 text-xs grid grid-cols-12 py-2 px-3.5 border-b border-white/20 bg-zinc-900 sticky top-0 z-10 rounded-t-2xl"> + <div className="col-span-10 flex items-center gap-x-[4px]"> + <div className="shrink-0 w-3 h-3" /> + <p className="ml-[7px]">Name</p> + </div> <p className="col-span-2" /> </div> - <div className="w-full h-full flex items-center justify-center flex-col gap-y-5"> + <div className="w-full h-[calc(100%-40px)] flex items-center justify-center flex-col gap-y-5"> <PreLoader /> <p className="text-white/80 text-sm font-semibold animate-pulse text-center w-1/3"> {loadingMessage} @@ -54,24 +118,50 @@ function WorkspaceDirectory({ {workspace.name} </h3> </div> - <div - className={`relative w-[560px] h-[445px] bg-zinc-900 rounded-2xl mt-5 overflow-y-auto border-4 ${ - highlightWorkspace ? "border-cyan-300/80" : "border-transparent" - }`} - > - <div className="text-white/80 text-xs grid grid-cols-12 py-2 px-8 border-b border-white/20 bg-zinc-900 sticky top-0 z-10"> - <p className="col-span-5">Name</p> - <p className="col-span-2" /> - </div> - <div className="w-full h-full flex flex-col z-0"> - {Object.values(files.items).some( - (folder) => folder.items.length > 0 - ) || movedItems.length > 0 ? ( - <> - {files.items.map((folder) => - folder.items.map((item, index) => ( + <div className="relative w-[560px] h-[445px] mt-5"> + <div + className={`absolute inset-0 rounded-2xl ${ + highlightWorkspace ? "border-4 border-cyan-300/80 z-[999]" : "" + }`} + /> + <div className="relative w-full h-full bg-zinc-900 rounded-2xl overflow-hidden"> + <div className="text-white/80 text-xs grid grid-cols-12 py-2 px-3.5 border-b border-white/20 bg-zinc-900 sticky top-0 z-10"> + <div className="col-span-10 flex items-center gap-x-[4px]"> + {!hasChanges && + files.items.some((folder) => folder.items.length > 0) ? ( + <div + className="shrink-0 w-3 h-3 rounded border-[1px] border-white flex justify-center items-center cursor-pointer" + role="checkbox" + aria-checked={ + Object.keys(selectedItems).length === + files.items.reduce( + (sum, folder) => sum + folder.items.length, + 0 + ) + } + tabIndex={0} + onClick={toggleSelectAll} + > + {Object.keys(selectedItems).length === + files.items.reduce( + (sum, folder) => sum + folder.items.length, + 0 + ) && <div className="w-2 h-2 bg-white rounded-[2px]" />} + </div> + ) : ( + <div className="shrink-0 w-3 h-3" /> + )} + <p className="ml-[7px]">Name</p> + </div> + <p className="col-span-2" /> + </div> + <div className="overflow-y-auto h-[calc(100%-40px)]"> + {files.items.some((folder) => folder.items.length > 0) || + movedItems.length > 0 ? ( + <RenderFileRows files={files} movedItems={movedItems}> + {({ item, folder }) => ( <WorkspaceFileRow - key={index} + key={item.id} item={item} folderName={folder.name} workspace={workspace} @@ -80,15 +170,45 @@ function WorkspaceDirectory({ fetchKeys={fetchKeys} hasChanges={hasChanges} movedItems={movedItems} + selected={selectedItems[item.id]} + toggleSelection={() => toggleSelection(item)} + disableSelection={hasChanges} + setSelectedItems={setSelectedItems} /> - )) - )} - </> - ) : ( - <div className="w-full h-full flex items-center justify-center"> - <p className="text-white text-opacity-40 text-sm font-medium"> - No Documents - </p> + )} + </RenderFileRows> + ) : ( + <div className="w-full h-full flex items-center justify-center"> + <p className="text-white text-opacity-40 text-sm font-medium"> + No Documents + </p> + </div> + )} + </div> + {Object.keys(selectedItems).length > 0 && !hasChanges && ( + <div className="absolute bottom-[12px] left-0 right-0 flex justify-center pointer-events-none"> + <div className="mx-auto bg-white/40 rounded-lg py-1 px-2 pointer-events-auto"> + <div className="flex flex-row items-center gap-x-2"> + <button + onClick={toggleSelectAll} + className="border-none text-sm font-semibold bg-white h-[30px] px-2.5 rounded-lg hover:text-white hover:bg-neutral-800/80" + > + {Object.keys(selectedItems).length === + files.items.reduce( + (sum, folder) => sum + folder.items.length, + 0 + ) + ? "Deselect All" + : "Select All"} + </button> + <button + onClick={removeSelectedItems} + className="border-none text-sm font-semibold bg-white h-[30px] px-2.5 rounded-lg hover:text-white hover:bg-neutral-800/80" + > + Remove Selected + </button> + </div> + </div> </div> )} </div> @@ -111,7 +231,7 @@ function WorkspaceDirectory({ </div> <button - onClick={saveChanges} + onClick={(e) => handleSaveChanges(e)} className="border border-slate-200 px-5 py-2.5 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800" > Save and Embed @@ -258,4 +378,22 @@ const DocumentWatchAlert = memo(() => { ); }); +function RenderFileRows({ files, movedItems, children }) { + function sortMovedItemsAndFiles(a, b) { + const aIsMovedItem = movedItems.some((movedItem) => movedItem.id === a.id); + const bIsMovedItem = movedItems.some((movedItem) => movedItem.id === b.id); + if (aIsMovedItem && !bIsMovedItem) return -1; + if (!aIsMovedItem && bIsMovedItem) return 1; + return 0; + } + + return files.items + .flatMap((folder) => folder.items) + .sort(sortMovedItemsAndFiles) + .map((item) => { + const folder = files.items.find((f) => f.items.includes(item)); + return children({ item, folder }); + }); +} + export default memo(WorkspaceDirectory);