Bulk document removal from workspace

* wip improve remove document ux

* fix border ui bugs when adding files to workspace

* sort workspacedirectory put adding files at top

* fix workspace file row ui shifting

* fix selected items bug when adding another item with items already selected on workspace

* fix tooltip

* lint

* refactor

* fix bug where unadding single item while selected would stay selected

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>
This commit is contained in:
Sean Hatfield 2024-09-25 13:04:42 -07:00 committed by GitHub
parent 4ebc37b4e3
commit 074088d3cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 206 additions and 37 deletions

View File

@ -19,8 +19,13 @@ export default function WorkspaceFileRow({
fetchKeys, fetchKeys,
hasChanges, hasChanges,
movedItems, movedItems,
selected,
toggleSelection,
disableSelection,
setSelectedItems,
}) { }) {
const onRemoveClick = async () => { const onRemoveClick = async (e) => {
e.stopPropagation();
setLoading(true); setLoading(true);
try { try {
@ -33,24 +38,49 @@ export default function WorkspaceFileRow({
} catch (error) { } catch (error) {
console.error("Failed to remove document:", error); console.error("Failed to remove document:", error);
} }
setSelectedItems({});
setLoadingMessage(""); setLoadingMessage("");
setLoading(false); 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); const isMovedItem = movedItems?.some((movedItem) => movedItem.id === item.id);
return ( return (
<div <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 ${ className={`items-center h-[34px] text-white/80 text-xs grid grid-cols-12 py-2 pl-3.5 pr-8 ${
isMovedItem ? "bg-green-800/40" : "file-row" !disableSelection ? "hover:bg-sky-500/20 cursor-pointer" : ""
}`} } ${isMovedItem ? "bg-green-800/40" : "file-row"} ${selected ? "selected" : ""}`}
onClick={toggleRowSelection}
> >
<div <div
className="col-span-10 w-fit flex gap-x-[2px] items-center relative"
data-tooltip-id={`ws-directory-item-${item.url}`} 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 <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" weight="fill"
/> />
<p className="whitespace-nowrap overflow-hidden text-ellipsis"> <p className="whitespace-nowrap overflow-hidden text-ellipsis">
@ -105,8 +135,9 @@ const PinItemToWorkspace = memo(({ workspace, docPath, item }) => {
const [hover, setHover] = useState(false); const [hover, setHover] = useState(false);
const pinEvent = new CustomEvent("pinned_document"); const pinEvent = new CustomEvent("pinned_document");
const updatePinStatus = async () => { const updatePinStatus = async (e) => {
try { try {
e.stopPropagation();
if (!pinned) window.dispatchEvent(pinEvent); if (!pinned) window.dispatchEvent(pinEvent);
const success = await Workspace.setPinForDocument( const success = await Workspace.setPinForDocument(
workspace.slug, workspace.slug,

View File

@ -7,6 +7,7 @@ import { Eye, PushPin } from "@phosphor-icons/react";
import { SEEN_DOC_PIN_ALERT, SEEN_WATCH_ALERT } from "@/utils/constants"; import { SEEN_DOC_PIN_ALERT, SEEN_WATCH_ALERT } from "@/utils/constants";
import paths from "@/utils/paths"; import paths from "@/utils/paths";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import Workspace from "@/models/workspace";
function WorkspaceDirectory({ function WorkspaceDirectory({
workspace, workspace,
@ -22,6 +23,66 @@ function WorkspaceDirectory({
embeddingCosts, embeddingCosts,
movedItems, 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) { if (loading) {
return ( return (
<div className="px-8"> <div className="px-8">
@ -31,11 +92,14 @@ function WorkspaceDirectory({
</h3> </h3>
</div> </div>
<div className="relative w-[560px] h-[445px] bg-zinc-900 rounded-2xl mt-5"> <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"> <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">
<p className="col-span-5">Name</p> <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" /> <p className="col-span-2" />
</div> </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 /> <PreLoader />
<p className="text-white/80 text-sm font-semibold animate-pulse text-center w-1/3"> <p className="text-white/80 text-sm font-semibold animate-pulse text-center w-1/3">
{loadingMessage} {loadingMessage}
@ -54,24 +118,50 @@ function WorkspaceDirectory({
{workspace.name} {workspace.name}
</h3> </h3>
</div> </div>
<div <div className="relative w-[560px] h-[445px] mt-5">
className={`relative w-[560px] h-[445px] bg-zinc-900 rounded-2xl mt-5 overflow-y-auto border-4 ${ <div
highlightWorkspace ? "border-cyan-300/80" : "border-transparent" className={`absolute inset-0 rounded-2xl ${
}`} highlightWorkspace ? "border-4 border-cyan-300/80 z-[999]" : ""
> }`}
<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> <div className="relative w-full h-full bg-zinc-900 rounded-2xl overflow-hidden">
<p className="col-span-2" /> <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> <div className="col-span-10 flex items-center gap-x-[4px]">
<div className="w-full h-full flex flex-col z-0"> {!hasChanges &&
{Object.values(files.items).some( files.items.some((folder) => folder.items.length > 0) ? (
(folder) => folder.items.length > 0 <div
) || movedItems.length > 0 ? ( className="shrink-0 w-3 h-3 rounded border-[1px] border-white flex justify-center items-center cursor-pointer"
<> role="checkbox"
{files.items.map((folder) => aria-checked={
folder.items.map((item, index) => ( 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 <WorkspaceFileRow
key={index} key={item.id}
item={item} item={item}
folderName={folder.name} folderName={folder.name}
workspace={workspace} workspace={workspace}
@ -80,15 +170,45 @@ function WorkspaceDirectory({
fetchKeys={fetchKeys} fetchKeys={fetchKeys}
hasChanges={hasChanges} hasChanges={hasChanges}
movedItems={movedItems} movedItems={movedItems}
selected={selectedItems[item.id]}
toggleSelection={() => toggleSelection(item)}
disableSelection={hasChanges}
setSelectedItems={setSelectedItems}
/> />
)) )}
)} </RenderFileRows>
</> ) : (
) : ( <div className="w-full h-full flex items-center justify-center">
<div className="w-full h-full flex items-center justify-center"> <p className="text-white text-opacity-40 text-sm font-medium">
<p className="text-white text-opacity-40 text-sm font-medium"> No Documents
No Documents </p>
</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>
)} )}
</div> </div>
@ -111,7 +231,7 @@ function WorkspaceDirectory({
</div> </div>
<button <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" 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 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); export default memo(WorkspaceDirectory);