Skip to content

Commit

Permalink
Bulk document removal from workspace
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
shatfield4 and timothycarambat authored Sep 25, 2024
1 parent 4ebc37b commit 074088d
Show file tree
Hide file tree
Showing 2 changed files with 206 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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">
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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">
Expand All @@ -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}
Expand All @@ -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}
Expand All @@ -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>
Expand All @@ -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
Expand Down Expand Up @@ -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);

0 comments on commit 074088d

Please sign in to comment.