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,
|
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,
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user