diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FileRow/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FileRow/index.jsx index 976c6598..a8526908 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FileRow/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FileRow/index.jsx @@ -4,44 +4,12 @@ import { getFileExtension, middleTruncate, } from "@/utils/directories"; -import { File, Trash } from "@phosphor-icons/react"; -import System from "@/models/system"; +import { File } from "@phosphor-icons/react"; import debounce from "lodash.debounce"; -export default function FileRow({ - item, - folderName, - selected, - toggleSelection, - fetchKeys, - setLoading, - setLoadingMessage, -}) { +export default function FileRow({ item, selected, toggleSelection }) { const [showTooltip, setShowTooltip] = useState(false); - const onTrashClick = async (event) => { - event.stopPropagation(); - if ( - !window.confirm( - "Are you sure you want to delete this document?\nThis will require you to re-upload and re-embed it.\nThis document will be removed from any workspace that is currently referencing it.\nThis action is not reversible." - ) - ) { - return false; - } - - try { - setLoading(true); - setLoadingMessage("This may take a while for large documents"); - await System.deleteDocument(`${folderName}/${item.name}`); - await fetchKeys(true); - } catch (error) { - console.error("Failed to delete the document:", error); - } - - if (selected) toggleSelection(item); - setLoading(false); - }; - const handleShowTooltip = () => { setShowTooltip(true); }; @@ -60,7 +28,7 @@ export default function FileRow({ selected ? "selected" : "" }`} > -
+
{getFileExtension(item.url)}

-
+
{item?.cached && (

Cached

)} -
); diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderRow/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderRow/index.jsx index 48953ab1..909b5fed 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderRow/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderRow/index.jsx @@ -1,8 +1,7 @@ import { useState } from "react"; import FileRow from "../FileRow"; -import { CaretDown, FolderNotch, Trash } from "@phosphor-icons/react"; +import { CaretDown, FolderNotch } from "@phosphor-icons/react"; import { middleTruncate } from "@/utils/directories"; -import System from "@/models/system"; export default function FolderRow({ item, @@ -10,36 +9,10 @@ export default function FolderRow({ onRowClick, toggleSelection, isSelected, - fetchKeys, - setLoading, - setLoadingMessage, autoExpanded = false, }) { const [expanded, setExpanded] = useState(autoExpanded); - const onTrashClick = async (event) => { - event.stopPropagation(); - if ( - !window.confirm( - "Are you sure you want to delete this folder?\nThis will require you to re-upload and re-embed it.\nAny documents in this folder will be removed from any workspace that is currently referencing it.\nThis action is not reversible." - ) - ) { - return false; - } - - try { - setLoading(true); - setLoadingMessage("This may take a while for large folders"); - await System.deleteFolder(item.name); - await fetchKeys(true); - } catch (error) { - console.error("Failed to delete the document:", error); - } - - if (selected) toggleSelection(item); - setLoading(false); - }; - const handleExpandClick = (event) => { event.stopPropagation(); setExpanded(!expanded); @@ -49,7 +22,7 @@ export default function FolderRow({ <> @@ -59,6 +32,10 @@ export default function FolderRow({ role="checkbox" aria-checked={selected} tabIndex={0} + onClick={(event) => { + event.stopPropagation(); + toggleSelection(item); + }} > {selected &&
}
@@ -75,19 +52,11 @@ export default function FolderRow({ weight="fill" />

- {middleTruncate(item.name, 40)} + {middleTruncate(item.name, 35)}

-

- {item.name !== "custom-documents" && ( - - )} -
{expanded && (
@@ -95,12 +64,8 @@ export default function FolderRow({ ))}
diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderSelectionPopup/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderSelectionPopup/index.jsx new file mode 100644 index 00000000..2ebfcde2 --- /dev/null +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/FolderSelectionPopup/index.jsx @@ -0,0 +1,24 @@ +import { middleTruncate } from "@/utils/directories"; + +export default function FolderSelectionPopup({ folders, onSelect, onClose }) { + const handleFolderSelect = (folder) => { + onSelect(folder); + onClose(); + }; + + return ( +
+
    + {folders.map((folder) => ( +
  • handleFolderSelect(folder)} + className="px-4 py-2 text-xs text-gray-700 hover:bg-gray-200 rounded-lg cursor-pointer whitespace-nowrap" + > + {middleTruncate(folder.name, 25)} +
  • + ))} +
+
+ ); +} diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/MoveToFolderIcon.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/MoveToFolderIcon.jsx new file mode 100644 index 00000000..3916fc77 --- /dev/null +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/MoveToFolderIcon.jsx @@ -0,0 +1,44 @@ +export default function MoveToFolderIcon({ + className, + width = 18, + height = 18, +}) { + return ( + + + + + + + ); +} diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/index.jsx index 95a1ecd0..44d65448 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/Directory/index.jsx @@ -2,11 +2,16 @@ import UploadFile from "../UploadFile"; import PreLoader from "@/components/Preloader"; import { memo, useEffect, useState } from "react"; import FolderRow from "./FolderRow"; -import pluralize from "pluralize"; import System from "@/models/system"; +import { Plus, Trash } from "@phosphor-icons/react"; +import Document from "@/models/document"; +import showToast from "@/utils/toast"; +import FolderSelectionPopup from "./FolderSelectionPopup"; +import MoveToFolderIcon from "./MoveToFolderIcon"; function Directory({ files, + setFiles, loading, setLoading, workspace, @@ -19,12 +24,19 @@ function Directory({ loadingMessage, }) { const [amountSelected, setAmountSelected] = useState(0); + const [newFolderName, setNewFolderName] = useState(""); + const [showNewFolderInput, setShowNewFolderInput] = useState(false); + const [showFolderSelection, setShowFolderSelection] = useState(false); + + useEffect(() => { + setAmountSelected(Object.keys(selectedItems).length); + }, [selectedItems]); const deleteFiles = async (event) => { event.stopPropagation(); if ( !window.confirm( - "Are you sure you want to delete these files?\nThis will remove the files from the system and remove them from any existing workspaces automatically.\nThis action is not reversible." + "Are you sure you want to delete these files and folders?\nThis will remove the files from the system and remove them from any existing workspaces automatically.\nThis action is not reversible." ) ) { return false; @@ -32,6 +44,8 @@ function Directory({ try { const toRemove = []; + const foldersToRemove = []; + for (const itemId of Object.keys(selectedItems)) { for (const folder of files.items) { const foundItem = folder.items.find((file) => file.id === itemId); @@ -41,13 +55,29 @@ function Directory({ } } } + for (const folder of files.items) { + if (folder.name === "custom-documents") { + continue; + } + + if (isSelected(folder.id, folder)) { + foldersToRemove.push(folder.name); + } + } + setLoading(true); - setLoadingMessage(`Removing ${toRemove.length} documents. Please wait.`); + setLoadingMessage( + `Removing ${toRemove.length} documents and ${foldersToRemove.length} folders. Please wait.` + ); await System.deleteDocuments(toRemove); + for (const folderName of foldersToRemove) { + await System.deleteFolder(folderName); + } + await fetchKeys(true); setSelectedItems({}); } catch (error) { - console.error("Failed to delete the document:", error); + console.error("Failed to delete files and folders:", error); } finally { setLoading(false); setSelectedItems({}); @@ -59,10 +89,11 @@ function Directory({ const newSelectedItems = { ...prevSelectedItems }; if (item.type === "folder") { - const isCurrentlySelected = isFolderCompletelySelected(item); - if (isCurrentlySelected) { + if (newSelectedItems[item.name]) { + delete newSelectedItems[item.name]; item.items.forEach((file) => delete newSelectedItems[file.id]); } else { + newSelectedItems[item.name] = true; item.items.forEach((file) => (newSelectedItems[file.id] = true)); } } else { @@ -78,7 +109,7 @@ function Directory({ }; const isFolderCompletelySelected = (folder) => { - if (folder.items.length === 0) { + if (!selectedItems[folder.name]) { return false; } return folder.items.every((file) => selectedItems[file.id]); @@ -92,23 +123,108 @@ function Directory({ return !!selectedItems[id]; }; - useEffect(() => { - setAmountSelected(Object.keys(selectedItems).length); - }, [selectedItems]); + const createNewFolder = () => { + setShowNewFolderInput(true); + }; + + const confirmNewFolder = async () => { + if (newFolderName.trim() !== "") { + const newFolder = { + name: newFolderName, + type: "folder", + items: [], + }; + + // If folder failed to create - silently fail. + const { success } = await Document.createFolder(newFolderName); + if (success) { + setFiles({ + ...files, + items: [...files.items, newFolder], + }); + } + + setNewFolderName(""); + setShowNewFolderInput(false); + } + }; + + const moveToFolder = async (folder) => { + const toMove = []; + for (const itemId of Object.keys(selectedItems)) { + for (const currentFolder of files.items) { + const foundItem = currentFolder.items.find( + (file) => file.id === itemId + ); + if (foundItem) { + toMove.push({ ...foundItem, folderName: currentFolder.name }); + break; + } + } + } + setLoading(true); + setLoadingMessage(`Moving ${toMove.length} documents. Please wait.`); + const { success, message } = await Document.moveToFolder( + toMove, + folder.name + ); + if (!success) { + showToast(`Error moving files: ${message}`, "error"); + setLoading(false); + return; + } + + if (success && message) { + showToast(message, "info"); + } else { + showToast(`Successfully moved ${toMove.length} documents.`, "success"); + } + await fetchKeys(true); + setSelectedItems({}); + setLoading(false); + }; return (
-
+

My Documents

+ {showNewFolderInput ? ( +
+ setNewFolderName(e.target.value)} + className="bg-zinc-900 text-white placeholder-white/20 text-sm rounded-md p-2.5 w-[150px] h-[32px]" + /> +
+ +
+
+ ) : ( + + )}
-

Name

+

Name

Date

Kind

-

Cached

- ) : !!files.items ? ( + ) : files.items ? ( files.items.map( (item, index) => - (item.name === "custom-documents" || - (item.type === "folder" && item.items.length > 0)) && ( + item.type === "folder" && ( toggleSelection(item)} toggleSelection={toggleSelection} isSelected={isSelected} - setLoading={setLoading} - setLoadingMessage={setLoadingMessage} autoExpanded={index === 0} /> ) @@ -154,24 +266,41 @@ function Directory({
{amountSelected !== 0 && ( -
-
+
+
+ +
+ + {showFolderSelection && ( + item.type === "folder" + )} + onSelect={moveToFolder} + onClose={() => setShowFolderSelection(false)} + /> + )} +
+
-
)}
diff --git a/frontend/src/components/Modals/MangeWorkspace/Documents/index.jsx b/frontend/src/components/Modals/MangeWorkspace/Documents/index.jsx index 736a1476..5a5cf69d 100644 --- a/frontend/src/components/Modals/MangeWorkspace/Documents/index.jsx +++ b/frontend/src/components/Modals/MangeWorkspace/Documents/index.jsx @@ -191,9 +191,10 @@ export default function DocumentSettings({ workspace, systemSettings }) { }; return ( -
+
{ return (
-
+
-
+