mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-11-19 12:40:09 +01:00
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:
parent
4ebc37b4e3
commit
074088d3cb
@ -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,
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user