added multiselect logic, implemented trash and multi trash

This commit is contained in:
subnub
2024-06-27 23:31:45 -04:00
parent 99201318b5
commit 9f0821253d
28 changed files with 707 additions and 63 deletions
+42
View File
@@ -486,6 +486,27 @@ class FileController {
}
};
trashFile = async (req: RequestType, res: Response) => {
if (!req.user) {
return;
}
try {
const userID = req.user._id;
const fileID = req.body.id;
const trashedFile = await fileService.trashFile(userID, fileID);
res.send(trashedFile.toObject());
} catch (e: unknown) {
if (e instanceof Error) {
console.log("\nTrash File Error File Route:", e.message);
}
res.status(500).send("Server error trashing file");
}
};
deleteFile = async (req: RequestType, res: Response) => {
if (!req.user) {
return;
@@ -508,6 +529,27 @@ class FileController {
res.status(500).send("Server error deleting file");
}
};
trashMulti = async (req: RequestType, res: Response) => {
if (!req.user) {
return;
}
try {
const userID = req.user._id;
const items = req.body.items;
await this.chunkService.trashMulti(userID, items);
res.send();
} catch (e: unknown) {
if (e instanceof Error) {
console.log("\nTrash Multi Error File Route:", e.message);
}
res.status(500).send("Server error trashing multi");
}
};
}
export default FileController;
+21
View File
@@ -220,6 +220,27 @@ class FolderController {
}
};
trashFolder = async (req: RequestType, res: Response) => {
if (!req.user) {
return;
}
try {
const userID = req.user._id;
const folderID = req.body.id;
await folderService.trashFolder(userID, folderID);
res.send();
} catch (e: unknown) {
if (e instanceof Error) {
console.log("\nTrash Folder Error Folder Route:", e.message);
}
res.status(500).send("Server error trashing folder");
}
};
renameFolder = async (req: RequestType, res: Response) => {
if (!req.user) {
return;
+31 -1
View File
@@ -72,6 +72,21 @@ class DbUtil {
return file;
};
trashFile = async (fileID: string, userID: string) => {
const result = await File.findByIdAndUpdate(
{
_id: new ObjectId(fileID),
"metadata.owner": userID,
},
{
$set: {
"metadata.trashed": true,
},
}
);
return result;
};
getFileInfo = async (fileID: string, userID: string) => {
//TODO: Using mongoose like this causes the object to be returned in raw form
// const file = await File.findOne({
@@ -87,7 +102,7 @@ class DbUtil {
};
getQuickList = async (userID: string, limit: number) => {
let query: any = { "metadata.owner": userID };
let query: any = { "metadata.owner": userID, "metadata.trashed": null };
const fileList = (await conn.db
.collection("fs.files")
@@ -133,6 +148,21 @@ class DbUtil {
return fileList;
};
trashFilesByParent = async (parentList: string, userID: string) => {
const result = await File.updateMany(
{
"metadata.owner": userID,
"metadata.parentList": { $regex: `.*${parentList}.*` }, // REGEX
},
{
$set: {
"metadata.trashed": true,
},
}
);
return result;
};
renameFile = async (fileID: string, userID: string, title: string) => {
const updateFileResponse = await File.findOneAndUpdate(
{ _id: new ObjectId(fileID), "metadata.owner": userID },
+23 -1
View File
@@ -30,7 +30,8 @@ class DbUtil {
s3Enabled: boolean,
type: string,
storageType: string,
itemType: string
itemType: string,
trashMode: boolean
) => {
let query: any = { owner: userID, parent: parent };
@@ -52,6 +53,12 @@ class DbUtil {
query = { ...query, personalFolder: null };
}
if (trashMode) {
query = { ...query, trashed: true };
} else {
query = { ...query, trashed: null };
}
const folderList = (await Folder.find(query).sort(
sortBy
)) as FolderInterface[];
@@ -136,6 +143,21 @@ class DbUtil {
return folderList;
};
trashFoldersByParent = async (parentList: string[], userID: string) => {
const result = await Folder.updateMany(
{
owner: userID,
parentList: { $all: parentList },
},
{
$set: {
trashed: true,
},
}
);
return result;
};
}
export default DbUtil;
+4
View File
@@ -99,6 +99,10 @@ router.patch("/file-service/rename", auth, fileController.renameFile);
router.patch("/file-service/move", auth, fileController.moveFile);
router.patch("/file-service/trash", auth, fileController.trashFile);
router.patch("/file-service/trash-multi", auth, fileController.trashMulti);
router.delete("/file-service/remove-link/:id", auth, fileController.removeLink);
router.delete(
+2
View File
@@ -37,6 +37,8 @@ router.patch("/folder-service/rename", auth, folderController.renameFolder);
router.patch("/folder-service/move", auth, folderController.moveFolder);
router.patch("/folder-service/trash", auth, folderController.trashFolder);
router.get(
"/folder-service/subfolder-list-full",
auth,
+2
View File
@@ -53,6 +53,7 @@ const fileSchema = new mongoose.Schema({
filePath: String,
s3ID: String,
personalFile: Boolean,
trashed: Boolean,
},
required: true,
},
@@ -79,6 +80,7 @@ export interface FileInterface
filePath?: string;
s3ID?: string;
personalFile?: boolean;
trashed?: boolean;
};
}
+2
View File
@@ -22,6 +22,7 @@ const folderSchema = new mongoose.Schema(
required: true,
},
personalFolder: Boolean,
trashed: Boolean,
},
{
timestamps: true,
@@ -38,6 +39,7 @@ export interface FolderInterface
parentList: string[];
_doc?: any;
personalFolder?: boolean;
trashed?: boolean;
}
const Folder = mongoose.model<FolderInterface>("Folder", folderSchema);
@@ -29,9 +29,13 @@ import addToStoageSize from "./utils/addToStorageSize";
import subtractFromStorageSize from "./utils/subtractFromStorageSize";
import ForbiddenError from "../../utils/ForbiddenError";
import { ObjectId } from "mongodb";
import FolderService from "../FolderService";
import MongoFileService from "../FileService";
const dbUtilsFile = new DbUtilFile();
const dbUtilsFolder = new DbUtilFolder();
const folderService = new FolderService();
const fileService = new MongoFileService();
class FileSystemService implements ChunkInterface {
constructor() {}
@@ -466,6 +470,33 @@ class FileSystemService implements ChunkInterface {
}
};
trashMulti = async (
userID: string,
items: {
type: "file" | "folder" | "quick-item";
id: string;
file?: FileInterface;
folder?: FolderInterface;
}[]
) => {
const fileList = items.filter(
(item) => item.type === "file" || item.type === "quick-item"
);
const folderList = items
.filter((item) => item.type === "folder")
.sort((a, b) => {
if (!a.folder || !b.folder) return 0;
return b.folder.parentList.length - a.folder.parentList.length;
});
for (const file of fileList) {
await fileService.trashFile(userID, file.id);
}
for (const folder of folderList) {
await folderService.trashFolder(userID, folder.id);
}
};
deleteFile = async (userID: string, fileID: string) => {
const file = await dbUtilsFile.getFileInfo(fileID, userID);
@@ -604,6 +604,16 @@ class S3Service implements ChunkInterface {
}
}
};
trashMulti = async (
userID: string,
items: {
type: "file" | "folder" | "quick-item";
id: string;
file?: FileInterface;
folder?: FolderInterface;
}[]
) => {};
}
export default S3Service;
@@ -1,15 +1,39 @@
import { UserInterface } from "../../../models/user";
import { FileInterface } from "../../../models/file";
import { Request, Response } from "express";
import { FolderInterface } from "../../../models/folder";
interface ChunkInterface {
uploadFile: (user: UserInterface, busboy: any, req: Request) => Promise<FileInterface>;
downloadFile: (user: UserInterface, fileID: string, res: Response) => void;
getThumbnail: (user: UserInterface, id: string) => Promise<Buffer>;
getFullThumbnail: (user: UserInterface, fileID: string, res: Response) => void;
getPublicDownload: (fileID: string, tempToken: any, res: Response) => void;
streamVideo: (user: UserInterface, fileID: string, headers: any, res: Response, req: Request) => void;
getFileReadStream: (user: UserInterface, fileID: string) => any
uploadFile: (
user: UserInterface,
busboy: any,
req: Request
) => Promise<FileInterface>;
downloadFile: (user: UserInterface, fileID: string, res: Response) => void;
getThumbnail: (user: UserInterface, id: string) => Promise<Buffer>;
getFullThumbnail: (
user: UserInterface,
fileID: string,
res: Response
) => void;
getPublicDownload: (fileID: string, tempToken: any, res: Response) => void;
streamVideo: (
user: UserInterface,
fileID: string,
headers: any,
res: Response,
req: Request
) => void;
getFileReadStream: (user: UserInterface, fileID: string) => any;
trashMulti: (
userID: string,
items: {
type: "file" | "folder";
id: string;
file?: FileInterface;
folder?: FolderInterface;
}[]
) => Promise<void>;
}
export default ChunkInterface;
export default ChunkInterface;
+10 -2
View File
@@ -2,7 +2,7 @@ import NotAuthorizedError from "../../utils/NotAuthorizedError";
import NotFoundError from "../../utils/NotFoundError";
import env from "../../enviroment/env";
import jwt from "jsonwebtoken";
import Folder from "../../models/folder";
import Folder, { FolderInterface } from "../../models/folder";
import sortBySwitch from "../../utils/sortBySwitch";
import createQuery from "../../utils/createQuery";
import DbUtilFile from "../../db/utils/fileUtils/index";
@@ -127,6 +127,7 @@ class MongoFileService {
const startAtName = query.startAtName || "";
const storageType = query.storageType || undefined;
const folderSearch = query.folder_search || undefined;
const trashMode = query.trashMode === "true";
sortBy = sortBySwitch(sortBy);
limit = parseInt(limit);
console.log("sortBy", sortBy, query.sortBy);
@@ -143,7 +144,8 @@ class MongoFileService {
s3Enabled,
startAtName,
storageType,
folderSearch
folderSearch,
trashMode
);
const fileList = await dbUtilsFile.getList(queryObj, sortBy, limit);
@@ -219,6 +221,12 @@ class MongoFileService {
};
};
trashFile = async (userID: string, fileID: string) => {
const trashedFile = await dbUtilsFile.trashFile(fileID, userID);
if (!trashedFile) throw new NotFoundError("Trash File Not Found Error");
return trashedFile;
};
renameFile = async (userID: string, fileID: string, title: string) => {
const file = await dbUtilsFile.renameFile(fileID, userID, title);
+18 -1
View File
@@ -122,6 +122,7 @@ class FolderService {
const storageType = query.storageType || undefined;
const folderSearch = query.folder_search || undefined;
const itemType = query.itemType || undefined;
const trashMode = query.trashMode === "true";
sortBy = sortBySwitch(sortBy);
const s3Enabled = user.s3Enabled ? true : false;
@@ -134,7 +135,8 @@ class FolderService {
s3Enabled,
type,
storageType,
itemType
itemType,
trashMode
);
if (!folderList) throw new NotFoundError("Folder List Not Found Error");
@@ -207,6 +209,21 @@ class FolderService {
return folderList;
};
trashFolder = async (userID: string, folderID: string) => {
const folder = await utilsFolder.getFolderInfo(folderID, userID);
if (!folder) throw new NotFoundError("Trash Folder Not Found Error");
folder.trashed = true;
await folder.save();
const parentList = [...folder.parentList, folder._id!.toString()];
await utilsFolder.trashFoldersByParent(parentList, userID);
await utilsFile.trashFilesByParent(parentList.toString(), userID);
};
moveFolder = async (userID: string, folderID: string, parentID: string) => {
let parentList = ["/"];
+9 -1
View File
@@ -16,6 +16,7 @@ export interface QueryInterface {
$gt?: Date;
};
"metadata.personalFile"?: boolean | null;
"metadata.trashed"?: boolean | null;
}
const createQuery = (
@@ -28,7 +29,8 @@ const createQuery = (
s3Enabled: boolean,
startAtName: string,
storageType: string,
folderSearch: boolean
folderSearch: boolean,
trashMode: boolean
) => {
let query: QueryInterface = { "metadata.owner": owner };
@@ -63,6 +65,12 @@ const createQuery = (
query = { ...query, "metadata.personalFile": null };
}
if (trashMode) {
query = { ...query, "metadata.trashed": true };
} else {
query = { ...query, "metadata.trashed": null };
}
// if (storageType === "s3") {
// query = {...query, "metadata.personalFile": true}
// }
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><path d="M19,19H5V5H15V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V11H19M7.91,10.08L6.5,11.5L11,16L21,6L19.59,4.58L11,13.17L7.91,10.08Z" fill="#919EAB"/></svg>

After

Width:  |  Height:  |  Size: 246 B

+18 -1
View File
@@ -10,6 +10,7 @@ interface QueryKeyParams {
startAtDate?: string;
startAtName?: string;
startAt?: boolean;
trashMode?: boolean;
}
// GET
@@ -20,7 +21,7 @@ export const getFilesListAPI = async ({
}: QueryFunctionContext<[string, QueryKeyParams]>) => {
const [
_key,
{ parent = "/", search = "", sortBy = "date_desc", limit = 50 },
{ parent = "/", search = "", sortBy = "date_desc", limit = 50, trashMode },
] = queryKey;
const queryParams: QueryKeyParams = {
@@ -28,6 +29,7 @@ export const getFilesListAPI = async ({
search,
sortBy,
limit,
trashMode,
};
if (pageParam?.startAtDate && pageParam?.startAtName) {
@@ -93,6 +95,21 @@ export const getSuggestedListAPI = async ({
};
// PATCH
export const trashFileAPI = async (fileID: string) => {
const response = await axios.patch(`/file-service/trash`, {
id: fileID,
});
return response.data;
};
export const trashMultiAPI = async (items: any) => {
const response = await axios.patch(`/file-service/trash-multi`, {
items,
});
return response.data;
};
export const renameFileAPI = async (fileID: string, name: string) => {
const response = await axios.patch(`/file-service/rename`, {
id: fileID,
+10 -1
View File
@@ -6,6 +6,7 @@ interface QueryKeyParams {
search?: string;
sortBy?: string;
limit?: number;
trashMode?: boolean;
}
// GET
@@ -13,13 +14,14 @@ interface QueryKeyParams {
export const getFoldersListAPI = async ({
queryKey,
}: QueryFunctionContext<[string, QueryKeyParams]>) => {
const [_key, { parent, search, sortBy, limit }] = queryKey;
const [_key, { parent, search, sortBy, limit, trashMode }] = queryKey;
const response = await axios.get(`/folder-service/list`, {
params: {
parent,
search,
sortBy,
limit,
trashMode,
},
});
return response.data;
@@ -54,6 +56,13 @@ export const renameFolder = async (folderID: string, name: string) => {
return response.data;
};
export const trashFolderAPI = (folderID: string) => {
const response = axios.patch("/folder-service/trash", {
id: folderID,
});
return response.data;
};
// DELETE
export const deleteFolder = async (folderID: string, parentList: string[]) => {
+54 -12
View File
@@ -5,24 +5,31 @@ import {
deleteFileAPI,
renameFileAPI,
downloadFileAPI,
trashFileAPI,
} from "../../api/filesAPI";
import { useFilesClient, useQuickFilesClient } from "../../hooks/files";
import { useFoldersClient } from "../../hooks/folders";
import { useDispatch } from "react-redux";
import { setMoverID } from "../../actions/mover";
import { setShareSelected } from "../../actions/selectedItem";
import { deleteFolder, renameFolder } from "../../api/foldersAPI";
import {
deleteFolder,
renameFolder,
trashFolderAPI,
} from "../../api/foldersAPI";
import { useClickOutOfBounds } from "../../hooks/utils";
import { useAppDispatch } from "../../hooks/store";
import { setMultiSelectMode } from "../../reducers/selected";
const ContextMenu = (props) => {
const { invalidateFilesCache } = useFilesClient();
const { invalidateFoldersCache } = useFoldersClient();
const { invalidateQuickFilesCache } = useQuickFilesClient();
const { wrapperRef } = useClickOutOfBounds(props.closeContext);
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const liClassname =
"flex w-full px-[20px] py-[12px] items-center font-normal text-[#637381] justify-start no-underline transition-all duration-400 ease-in-out text- hover:bg-[#f6f5fd] hover:text-[#3c85ee] hover:font-medium";
const spanClassname = "inline-flex mr-[18px]";
const spanClassname = "flex items-center justify-center mr-[18px]";
const renameItem = async () => {
props.closeContext();
@@ -67,32 +74,32 @@ const ContextMenu = (props) => {
props.closeContext();
if (!props.folderMode) {
const result = await Swal.fire({
title: "Confirm Deletion",
text: "You cannot undo this action",
title: "Move to trash?",
text: "Items in the trash will eventually be deleted.",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "Yes, delete",
confirmButtonText: "Yes",
});
if (result.value) {
await deleteFileAPI(props.file._id);
await trashFileAPI(props.file._id);
invalidateFilesCache();
invalidateQuickFilesCache();
}
} else {
const result = await Swal.fire({
title: "Confirm Deletion",
text: "You cannot undo this action",
title: "Move to trash?",
text: "Items in the trash will eventually be deleted.",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "Yes, delete",
confirmButtonText: "Yes",
});
if (result.value) {
await deleteFolder(props.folder._id, props.folder.parentList);
await trashFolderAPI(props.folder._id);
invalidateFoldersCache();
}
}
@@ -117,6 +124,29 @@ const ContextMenu = (props) => {
downloadFileAPI(props.file._id);
};
const selectItemMultiSelect = () => {
props.closeContext();
if (props.folderMode) {
dispatch(
setMultiSelectMode({
type: "folder",
id: props.folder._id,
file: null,
folder: props.folder,
})
);
} else {
dispatch(
setMultiSelectMode({
type: props.quickItemMode ? "quick-item" : "file",
id: props.file._id,
file: props.file,
folder: null,
})
);
}
};
return (
<div
onClick={props.stopPropagation}
@@ -136,6 +166,18 @@ const ContextMenu = (props) => {
}
>
<ul className="p-0 list-none m-0 ">
<li onClick={selectItemMultiSelect} className={liClassname}>
<a className="flex">
<span className={spanClassname}>
<img
src="/assets/checkbox-multiple-outline.svg"
alt="multiselect"
className="w-[19px] h-[20px]"
/>
</span>
Multi select
</a>
</li>
<li onClick={renameItem} className={liClassname}>
<a className="flex">
<span className={spanClassname}>
@@ -180,7 +222,7 @@ const ContextMenu = (props) => {
<span className={spanClassname}>
<img src="/assets/filesetting5.svg" alt="setting" />
</span>
Delete
Trash
</a>
</li>
</ul>
+36 -2
View File
@@ -3,9 +3,15 @@ import Folders from "../Folders";
import { useFiles } from "../../hooks/files";
import { useInfiniteScroll } from "../../hooks/infiniteScroll";
import Files from "../Files";
import { memo, useEffect, useState } from "react";
import { memo, useCallback, useEffect, useRef, useState } from "react";
import SpinnerPage from "../SpinnerPage";
import SearchBar from "../SearchBar";
import { useAppDispatch } from "../../hooks/store";
import { startAddFile } from "../../actions/files";
import { useParams } from "react-router-dom";
import classNames from "classnames";
import { useDragAndDrop } from "../../hooks/utils";
import MultiSelectBar from "../MultiSelectBar";
const DataForm = memo(() => {
const {
@@ -13,8 +19,10 @@ const DataForm = memo(() => {
isFetchingNextPage,
data: fileList,
} = useFiles();
const dispatch = useAppDispatch();
const { sentinelRef, reachedIntersect } = useInfiniteScroll();
const [initialLoad, setInitialLoad] = useState(true);
const params = useParams();
useEffect(() => {
if (initialLoad) {
@@ -28,8 +36,34 @@ const DataForm = memo(() => {
}
}, [reachedIntersect, initialLoad, isFetchingNextPage]);
const addFile = useCallback(
(file) => {
dispatch(startAddFile(file, params.id));
},
[params.id]
);
const {
isDraggingFile,
onDragDropEvent,
onDragEvent,
onDragEnterEvent,
stopDrag,
} = useDragAndDrop(addFile);
return (
<div className="w-full p-[65px_40px] overflow-y-scroll">
<div
className={classNames("w-full p-[17px_40px] overflow-y-scroll", {
"opacity-50": isDraggingFile,
})}
onDrop={onDragDropEvent}
onDragOver={onDragEvent}
onDragLeave={onDragEvent}
onDragEnter={onDragEnterEvent}
onMouseLeave={stopDrag}
>
<MultiSelectBar />
<QuickAccess />
<Folders />
+37 -9
View File
@@ -12,7 +12,7 @@ import { startSetSelectedItem } from "../../actions/selectedItem";
import { setPopupFile } from "../../actions/popupFile";
import bytes from "bytes";
import { useAppDispatch, useAppSelector } from "../../hooks/store";
import { setMainSelect } from "../../reducers/selected";
import { setMainSelect, setMultiSelectMode } from "../../reducers/selected";
const FileItem = React.memo((props) => {
const { file } = props;
@@ -20,6 +20,14 @@ const FileItem = React.memo((props) => {
if (state.selected.mainSection.type !== "file") return false;
return state.selected.mainSection.id === file._id;
});
const elementMultiSelected = useAppSelector((state) => {
if (!state.selected.multiSelectMode) return false;
const selected = state.selected.multiSelectMap[file._id];
return selected && selected.type === "file";
});
const multiSelectMode = useAppSelector(
(state) => state.selected.multiSelectMode
);
const listView = useSelector((state) => state.filter.listView);
const { image, hasThumbnail, imageOnError } = useThumbnail(
file.metadata.hasThumbnail,
@@ -58,6 +66,18 @@ const FileItem = React.memo((props) => {
// TODO: See if we can memoize this
const fileClick = () => {
if (multiSelectMode) {
dispatch(
setMultiSelectMode({
type: "file",
id: file._id,
file: file,
folder: null,
})
);
return;
}
const currentDate = Date.now();
if (!elementSelected) {
@@ -82,7 +102,7 @@ const FileItem = React.memo((props) => {
<tr
className={classNames(
"text-[14px] font-normal border-y",
!elementSelected
!elementSelected && !elementMultiSelected
? "text-[#212b36] hover:bg-[#f6f5fd]"
: "bg-[#3c85ee] animate text-white"
)}
@@ -137,7 +157,9 @@ const FileItem = React.memo((props) => {
<svg
className={classNames(
"w-4 h-4",
elementSelected ? "text-white" : "text-[#919eab]"
elementSelected || elementMultiSelected
? "text-white"
: "text-[#919eab]"
)}
aria-hidden="true"
focusable="false"
@@ -162,8 +184,10 @@ const FileItem = React.memo((props) => {
return (
<div
className={classNames(
"border rounded-md o transition-all duration-400 ease-in-out cursor-pointer flex items-center justify-center flex-col h-[125px] sm:h-[150px] animiate hover:border-[#3c85ee] overflow-hidden",
elementSelected ? "border-[#3c85ee]" : "border-[#ebe9f9]"
"border rounded-md o transition-all duration-400 ease-in-out cursor-pointer flex items-center justify-center flex-col h-[125px] sm:h-[150px] animiate hover:border-[#3c85ee] overflow-hidden bg-white",
elementSelected || elementMultiSelected
? "border-[#3c85ee]"
: "border-[#ebe9f9]"
)}
onClick={fileClick}
onContextMenu={onContextMenu}
@@ -175,7 +199,7 @@ const FileItem = React.memo((props) => {
<div onClick={clickStopPropagation}>
<ContextMenu
gridMode={true}
quickItemMode={true}
quickItemMode={false}
contextSelected={contextMenuState}
closeContext={closeContextMenu}
file={file}
@@ -221,7 +245,7 @@ const FileItem = React.memo((props) => {
<div
className={classNames(
"p-3 overflow-hidden text-ellipsis block w-full animate",
elementSelected
elementSelected || elementMultiSelected
? "bg-[#3c85ee] text-white"
: "bg-white text-[#637381]"
)}
@@ -229,7 +253,9 @@ const FileItem = React.memo((props) => {
<p
className={classNames(
"m-0 text-[14px] leading-[16px] font-normal max-w-full overflow-hidden text-ellipsis whitespace-nowrap animate",
elementSelected ? "text-white" : "text-[#212b36]"
elementSelected || elementMultiSelected
? "text-white"
: "text-[#212b36]"
)}
>
{formattedFilename}
@@ -237,7 +263,9 @@ const FileItem = React.memo((props) => {
<span
className={classNames(
"m-0 text-[#637381] font-normal max-w-full whitespace-nowrap text-xs animate hidden sm:block mt-1",
elementSelected ? "text-white" : "text-[#637381]"
elementSelected || elementMultiSelected
? "text-white"
: "text-[#637381]"
)}
>
Created {formattedCreatedDate}
+32 -7
View File
@@ -8,7 +8,7 @@ import { startSetSelectedItem } from "../../actions/selectedItem";
import mobilecheck from "../../utils/mobileCheck";
import moment from "moment";
import { useAppDispatch, useAppSelector } from "../../hooks/store";
import { setMainSelect } from "../../reducers/selected";
import { setMainSelect, setMultiSelectMode } from "../../reducers/selected";
const FolderItem = React.memo((props) => {
const { folder } = props;
@@ -16,6 +16,13 @@ const FolderItem = React.memo((props) => {
if (state.selected.mainSection.type !== "folder") return false;
return state.selected.mainSection.id === folder._id;
});
const elementMultiSelected = useAppSelector((state) => {
if (!state.selected.multiSelectMode) return false;
return state.selected.multiSelectMap[folder._id];
});
const multiSelectMode = useAppSelector(
(state) => state.selected.multiSelectMode
);
const lastSelected = useRef(0);
const navigate = useNavigate();
const dispatch = useAppDispatch();
@@ -30,6 +37,17 @@ const FolderItem = React.memo((props) => {
} = useContextMenu();
const folderClick = useCallback(() => {
if (multiSelectMode) {
dispatch(
setMultiSelectMode({
type: "folder",
id: folder._id,
file: null,
folder: folder,
})
);
return;
}
const currentDate = Date.now();
if (!elementSelected) {
@@ -58,14 +76,15 @@ const FolderItem = React.memo((props) => {
navigate,
folder._id,
elementSelected,
multiSelectMode,
]);
return (
<div
className={classNames(
"p-[12px] border border-[#ebe9f9] rounded-[4px] overflow-hidden cursor-pointer animate ",
"p-[12px] border border-[#ebe9f9] rounded-[4px] overflow-hidden cursor-pointer animate hover:border-[#3c85ee]",
{
"bg-[#3c85ee]": elementSelected,
"bg-[#3c85ee]": elementSelected || elementMultiSelected,
}
)}
onClick={folderClick}
@@ -91,7 +110,9 @@ const FolderItem = React.memo((props) => {
<svg
className={classNames(
"w-[40px] h-[40px]",
elementSelected ? "text-white" : "text-[#3c85ee]"
elementSelected || elementMultiSelected
? "text-white"
: "text-[#3c85ee]"
)}
aria-hidden="true"
focusable="false"
@@ -111,7 +132,7 @@ const FolderItem = React.memo((props) => {
<div
className={classNames(
"overflow-hidden text-ellipsis block w-full animate mt-2",
elementSelected
elementSelected || elementMultiSelected
? "bg-[#3c85ee] text-white"
: "bg-white text-[#637381]"
)}
@@ -119,7 +140,9 @@ const FolderItem = React.memo((props) => {
<p
className={classNames(
"m-0 text-[14px] leading-[16px] font-normal max-w-full overflow-hidden text-ellipsis whitespace-nowrap animate",
elementSelected ? "text-white" : "text-black"
elementSelected || elementMultiSelected
? "text-white"
: "text-black"
)}
>
{folder.name}
@@ -127,7 +150,9 @@ const FolderItem = React.memo((props) => {
<span
className={classNames(
"m-0 font-normal max-w-full whitespace-nowrap text-xs animate hidden sm:block mt-1",
elementSelected ? "text-white" : "text-[#637381]"
elementSelected || elementMultiSelected
? "text-white"
: "text-[#637381]"
)}
>
Created {moment(folder.createdAt).format("MM/DD/YY hh:mma")}
+120
View File
@@ -0,0 +1,120 @@
import { useMemo } from "react";
import { useAppDispatch, useAppSelector } from "../../hooks/store";
import { resetMultiSelect } from "../../reducers/selected";
import Swal from "sweetalert2";
import { trashMultiAPI } from "../../api/filesAPI";
import { useFilesClient, useQuickFilesClient } from "../../hooks/files";
import { useFoldersClient } from "../../hooks/folders";
const MultiSelectBar = () => {
const dispatch = useAppDispatch();
const multiSelectMode = useAppSelector(
(state) => state.selected.multiSelectMode
);
const multiSelectMap = useAppSelector(
(state) => state.selected.multiSelectMap
);
const multiSelectCount = useAppSelector(
(state) => state.selected.multiSelectCount
);
const { invalidateFilesCache } = useFilesClient();
const { invalidateFoldersCache } = useFoldersClient();
const { invalidateQuickFilesCache } = useQuickFilesClient();
const closeMultiSelect = () => {
dispatch(resetMultiSelect());
};
const deleteItems = async () => {
const result = await Swal.fire({
title: "Move to trash?",
text: "Items in the trash will eventually be deleted.",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "Yes",
});
if (result.value) {
const itemsToTrash = Object.values(multiSelectMap);
await trashMultiAPI(itemsToTrash);
invalidateFilesCache();
invalidateFoldersCache();
invalidateQuickFilesCache();
closeMultiSelect();
}
};
if (!multiSelectMode) return <div></div>;
return (
<div className="border border-[#ebe9f9] bg-[#ebe9f9] rounded-full p-2 px-5 text-black text-sm mb-4">
<div className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center">
<img
className="w-[22px] h-[22px] cursor-pointer"
src="/images/close_icon.png"
onClick={closeMultiSelect}
/>
<p className="ml-4">{multiSelectCount} selected</p>
</div>
<div className="flex flex-row items-center">
<svg
width="17"
height="18"
viewBox="0 0 17 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="ml-4 cursor-pointer"
onClick={deleteItems}
>
<g id="trash">
<path
id="Shape"
fill-rule="evenodd"
clip-rule="evenodd"
d="M16.0694 2.57072H11.5707V1.92803C11.5707 0.863209 10.7075 0 9.64265 0H7.07192C6.0071 0 5.14389 0.863209 5.14389 1.92803V2.57072H0.645132C0.290178 2.57072 0.00244141 2.85846 0.00244141 3.21341C0.00244141 3.56837 0.290215 3.85607 0.645132 3.85607H1.34371L2.57317 17.4108C2.60348 17.7427 2.88255 17.9963 3.21586 17.995H13.4987C13.832 17.9964 14.1111 17.7427 14.1414 17.4108L15.3708 3.85607H16.0694C16.4244 3.85607 16.7121 3.56833 16.7121 3.21338C16.7121 2.85842 16.4244 2.57072 16.0694 2.57072ZM6.42923 1.92803C6.42923 1.57308 6.71697 1.28534 7.07192 1.28534H9.64265C9.9976 1.28534 10.2853 1.57308 10.2853 1.92803V2.57072H6.42927V1.92803H6.42923ZM3.80263 16.7096H12.9119L14.0803 3.85607H5.78658H2.63745L3.80263 16.7096Z"
fill="currentColor"
/>
<path
id="Path"
d="M6.42938 14.7385C6.4293 14.7376 6.42926 14.7367 6.42919 14.7358L5.7865 5.73834C5.76131 5.38339 5.45312 5.1161 5.09821 5.14129C4.74325 5.16649 4.47596 5.47467 4.50116 5.82959L5.14385 14.8271C5.16783 15.1641 5.44868 15.425 5.78654 15.4241H5.83282C6.1869 15.3995 6.45401 15.0925 6.42938 14.7385Z"
fill="currentColor"
/>
<path
id="Path_2"
d="M8.35729 5.1416C8.00234 5.1416 7.7146 5.42934 7.7146 5.78429V14.7818C7.7146 15.1367 8.00234 15.4245 8.35729 15.4245C8.71224 15.4245 8.99998 15.1367 8.99998 14.7818V5.78429C8.99998 5.42934 8.71224 5.1416 8.35729 5.1416Z"
fill="currentColor"
/>
<path
id="Path_3"
d="M11.6164 5.14129C11.2615 5.1161 10.9533 5.38339 10.9281 5.73834L10.2854 14.7358C10.2594 15.0898 10.5253 15.3979 10.8793 15.4239C10.8804 15.424 10.8814 15.424 10.8825 15.4241H10.9281C11.266 15.425 11.5468 15.1641 11.5708 14.8271L12.2135 5.82959C12.2387 5.47467 11.9714 5.16652 11.6164 5.14129Z"
fill="currentColor"
/>
</g>
</svg>
<svg
width="19"
height="16"
viewBox="0 0 19 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="ml-4 cursor-pointer"
>
<path
id="Combined Shape"
fill-rule="evenodd"
clip-rule="evenodd"
d="M7.63107 0C7.83994 0 8.03661 0.0983374 8.16193 0.265429L9.95357 2.65429H17.9164C18.2829 2.65429 18.58 2.95138 18.58 3.31786V15.2621C18.58 15.6286 18.2829 15.9257 17.9164 15.9257H0.663571C0.297091 15.9257 0 15.6286 0 15.2621V0.663571C0 0.297091 0.297091 0 0.663571 0H7.63107ZM7.212 1.5H1.5V14.425H17.08V4.154L9.20357 4.15429L7.212 1.5ZM9.96369 9.51348C10.0439 9.43969 10.0892 9.33783 10.0892 9.23078C10.0892 9.12373 10.0439 9.02188 9.96369 8.9486L7.2712 6.4802C7.15381 6.37263 6.98149 6.34301 6.8334 6.40433C6.68532 6.46565 6.58893 6.60648 6.58893 6.76238V7.93162H4.30032C3.92929 7.93162 3.62719 8.22315 3.62719 8.5812V9.88036C3.62719 10.2384 3.92929 10.5299 4.30032 10.5299H6.58893V11.6992C6.58893 11.8551 6.68478 11.9964 6.8334 12.0577C6.98203 12.1191 7.15435 12.0889 7.2712 11.9819L9.96369 9.51348Z"
fill="currentColor"
/>
</svg>
</div>
</div>
</div>
);
};
export default MultiSelectBar;
+33 -8
View File
@@ -12,7 +12,7 @@ import { startSetSelectedItem } from "../../actions/selectedItem";
import { setPopupFile } from "../../actions/popupFile";
import { FileInterface } from "../../types/file";
import { useAppDispatch, useAppSelector } from "../../hooks/store";
import { setMainSelect } from "../../reducers/selected";
import { setMainSelect, setMultiSelectMode } from "../../reducers/selected";
interface QuickAccessItemProps {
file: FileInterface;
@@ -24,10 +24,18 @@ const QuickAccessItem = memo((props: QuickAccessItemProps) => {
if (state.selected.mainSection.type !== "quick-item") return false;
return state.selected.mainSection.id === file._id;
});
console.log("ele selected 2", elementSelected);
const elementMultiSelected = useAppSelector((state) => {
if (!state.selected.multiSelectMode) return false;
const selected = state.selected.multiSelectMap[file._id];
return selected && selected.type === "quick-item";
});
const multiSelectMode = useAppSelector(
(state) => state.selected.multiSelectMode
);
const { image, hasThumbnail, imageOnError } = useThumbnail(
file.metadata.hasThumbnail,
file.metadata.thumbnailID
file.metadata.thumbnailID,
true
);
const dispatch = useAppDispatch();
const lastSelected = useRef(0);
@@ -54,6 +62,17 @@ const QuickAccessItem = memo((props: QuickAccessItemProps) => {
// TODO: See if we can memoize this
const quickItemClick = () => {
if (multiSelectMode) {
dispatch(
setMultiSelectMode({
type: "quick-item",
id: file._id,
file: file,
folder: null,
})
);
return;
}
const currentDate = Date.now();
if (!elementSelected) {
@@ -77,8 +96,10 @@ const QuickAccessItem = memo((props: QuickAccessItemProps) => {
return (
<div
className={classNames(
"border rounded-md o transition-all duration-400 ease-in-out cursor-pointer flex items-center justify-center flex-col h-[125px] sm:h-[150px] animiate hover:border-[#3c85ee] overflow-hidden",
elementSelected ? "border-[#3c85ee]" : "border-[#ebe9f9]"
"border rounded-md o transition-all duration-400 ease-in-out cursor-pointer flex items-center justify-center flex-col h-[125px] sm:h-[150px] animiate hover:border-[#3c85ee] overflow-hidden bg-white",
elementSelected || elementMultiSelected
? "border-[#3c85ee]"
: "border-[#ebe9f9]"
)}
onClick={quickItemClick}
onContextMenu={onContextMenu}
@@ -136,7 +157,7 @@ const QuickAccessItem = memo((props: QuickAccessItemProps) => {
<div
className={classNames(
"p-3 overflow-hidden text-ellipsis block w-full animate",
elementSelected
elementSelected || elementMultiSelected
? "bg-[#3c85ee] text-white"
: "bg-white text-[#637381]"
)}
@@ -144,7 +165,9 @@ const QuickAccessItem = memo((props: QuickAccessItemProps) => {
<p
className={classNames(
"m-0 text-[14px] leading-[16px] font-normal max-w-full overflow-hidden text-ellipsis whitespace-nowrap animate",
elementSelected ? "text-white" : "text-[#212b36]"
elementSelected || elementMultiSelected
? "text-white"
: "text-[#212b36]"
)}
>
{capitalize(file.filename)}
@@ -152,7 +175,9 @@ const QuickAccessItem = memo((props: QuickAccessItemProps) => {
<span
className={classNames(
"text-[#637381] font-normal max-w-full whitespace-nowrap text-xs animate hidden sm:block mt-1",
elementSelected ? "text-white" : "text-[#637381]"
elementSelected || elementMultiSelected
? "text-white"
: "text-[#637381]"
)}
>
Created {moment(file.uploadDate).format("MM/DD/YY hh:mma")}
+3 -3
View File
@@ -26,7 +26,7 @@ const SearchBarItem = (props: SearchBarItemProps) => {
if (type === "folder" && folder) {
return (
<div
className="flex flex-row items-center py-2 px-4 overflow-hidden text-ellipsis hover:bg-[#f6f5fd] cursor-pointer"
className="flex flex-row items-center py-2 px-4 overflow-hidden text-ellipsis hover:bg-[#f6f5fd] cursor-pointer border-y mx-4"
key={folder._id}
onClick={() => folderClick(folder)}
>
@@ -48,7 +48,7 @@ const SearchBarItem = (props: SearchBarItemProps) => {
></path>
</svg>
</div>
<span className="text-sm ml-4 text-ellipsis overflow-hidden whitespace-nowrap">
<span className="text-sm ml-4 text-ellipsis overflow-hidden whitespace-nowrap max-w-[50%]">
{folder.name}
</span>
</div>
@@ -56,7 +56,7 @@ const SearchBarItem = (props: SearchBarItemProps) => {
} else if (type === "file" && file) {
return (
<div
className="flex flex-row items-center py-2 px-4 overflow-hidden text-ellipsis hover:bg-[#f6f5fd] cursor-pointer"
className="flex flex-row items-center py-2 px-4 overflow-hidden text-ellipsis hover:bg-[#f6f5fd] cursor-pointer border-y mx-4"
key={file._id}
onClick={() => fileClick(file)}
>
+27 -4
View File
@@ -11,13 +11,16 @@ import {
getQuickFilesListAPI,
getSuggestedListAPI,
} from "../api/filesAPI";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useSelector } from "react-redux";
import { useAppSelector } from "./store";
import { useUtils } from "./utils";
export const useFiles = () => {
const params = useParams();
// TODO: Remove any
const sortBy = useSelector((state: any) => state.filter.sortBy);
const trashMode = false;
const filesReactQuery = useInfiniteQuery(
[
"files",
@@ -26,6 +29,7 @@ export const useFiles = () => {
search: params.query || "",
sortBy,
limit: undefined,
trashMode,
},
],
getFilesListAPI,
@@ -53,6 +57,7 @@ export const useFilesClient = () => {
// TODO: Remove any
const sortBy = useSelector((state: any) => state.filter.sortBy);
const filesReactClientQuery = useQueryClient();
const trashMode = false;
const invalidateFilesCache = () => {
filesReactClientQuery.invalidateQueries({
@@ -63,6 +68,7 @@ export const useFilesClient = () => {
search: "",
sortBy,
limit: undefined,
trashMode,
},
],
});
@@ -94,11 +100,18 @@ interface thumbnailState {
image: undefined | string;
}
export const useThumbnail = (hasThumbnail: boolean, thumbnailID?: string) => {
export const useThumbnail = (
hasThumbnail: boolean,
thumbnailID?: string,
isQuickFile?: boolean
) => {
const requestedThumbnail = useRef(false);
const [state, setState] = useState<thumbnailState>({
hasThumbnail: false,
image: undefined,
});
const { isHome } = useUtils();
const listView = useAppSelector((state) => state.filter.listView);
const imageOnError = useCallback(() => {
setState({
@@ -108,7 +121,11 @@ export const useThumbnail = (hasThumbnail: boolean, thumbnailID?: string) => {
}, []);
const getThumbnail = useCallback(async () => {
try {
if (!thumbnailID) return;
if (!thumbnailID || requestedThumbnail.current) return;
if (isQuickFile && !isHome) return;
if (!isQuickFile && listView) return;
console.log("getting thumbnail", thumbnailID);
requestedThumbnail.current = true;
const thumbnailData = await getFileThumbnailAPI(thumbnailID);
setState({
hasThumbnail: true,
@@ -118,7 +135,13 @@ export const useThumbnail = (hasThumbnail: boolean, thumbnailID?: string) => {
console.log("error getting thumbnail data", e);
imageOnError();
}
}, [thumbnailID, getFileThumbnailAPI, imageOnError]);
}, [
thumbnailID,
getFileThumbnailAPI,
imageOnError,
listView,
requestedThumbnail.current,
]);
useEffect(() => {
if (!hasThumbnail || !thumbnailID) return;
+4
View File
@@ -6,6 +6,7 @@ import { useSelector } from "react-redux";
export const useFolders = () => {
const params = useParams();
const sortBy = useSelector((state: any) => state.filter.sortBy);
const trashMode = false;
const foldersReactQuery = useQuery(
[
"folders",
@@ -14,6 +15,7 @@ export const useFolders = () => {
search: params.query || "",
sortBy,
limit: undefined,
trashMode,
},
],
getFoldersListAPI
@@ -26,6 +28,7 @@ export const useFoldersClient = () => {
const params = useParams();
const sortBy = useSelector((state: any) => state.filter.sortBy);
const foldersReactClientQuery = useQueryClient();
const trashMode = false;
const invalidateFoldersCache = () => {
foldersReactClientQuery.invalidateQueries({
@@ -36,6 +39,7 @@ export const useFoldersClient = () => {
search: "",
sortBy,
limit: undefined,
trashMode,
},
],
});
+55 -1
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useRef } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useParams } from "react-router-dom";
export const useUtils = () => {
@@ -39,3 +39,57 @@ export const useClickOutOfBounds = (outOfBoundsCallback: () => any) => {
wrapperRef,
};
};
export const useDragAndDrop = (fileDroppedCallback: (file: any) => any) => {
const [isDraggingFile, setIsDraggingFile] = useState(false);
const isDraggingFileRef = useRef(false);
const onDragDropEvent = useCallback((e: any) => {
e.preventDefault();
e.stopPropagation();
isDraggingFileRef.current = false;
setIsDraggingFile(false);
const fileInput = e.dataTransfer;
fileDroppedCallback(fileInput);
}, []);
const onDragEvent = useCallback((e: any) => {
e.preventDefault();
e.stopPropagation();
}, []);
const onDragEnterEvent = useCallback((e: any) => {
e.preventDefault();
e.stopPropagation();
if (isDraggingFileRef.current) return;
isDraggingFileRef.current = true;
setIsDraggingFile(true);
}, []);
const stopDrag = useCallback((e: any) => {
e?.preventDefault();
e?.stopPropagation();
if (!isDraggingFileRef.current) return;
isDraggingFileRef.current = false;
setIsDraggingFile(false);
}, []);
useEffect(() => {
window.addEventListener("dragover", stopDrag);
window.addEventListener("focus", stopDrag);
return () => {
window.removeEventListener("dragover", stopDrag);
window.removeEventListener("focus", stopDrag);
};
}, []);
return {
isDraggingFile,
onDragDropEvent,
onDragEvent,
onDragEnterEvent,
stopDrag,
};
};
+40 -1
View File
@@ -12,6 +12,11 @@ interface MainSecionType {
export interface SelectedStateType {
mainSection: MainSecionType;
popupModal: FileInterface | null;
multiSelectMode: boolean;
multiSelectMap: {
[key: string]: MainSecionType;
};
multiSelectCount: number;
}
const initialState: SelectedStateType = {
@@ -22,6 +27,9 @@ const initialState: SelectedStateType = {
folder: null,
},
popupModal: null,
multiSelectMode: false,
multiSelectMap: {},
multiSelectCount: 0,
};
const selectedSlice = createSlice({
@@ -32,9 +40,40 @@ const selectedSlice = createSlice({
state.mainSection = action.payload;
},
resetSelected: () => initialState,
setMultiSelectMode: (state, action: PayloadAction<MainSecionType>) => {
state.mainSection = { type: "", id: "", file: null, folder: null };
if (
state.multiSelectMap[action.payload.id] &&
state.multiSelectMap[action.payload.id].type !== action.payload.type
) {
state.multiSelectMode = true;
state.multiSelectMap[action.payload.id] = action.payload;
} else if (state.multiSelectMap[action.payload.id]) {
delete state.multiSelectMap[action.payload.id];
const newCount = state.multiSelectCount - 1;
if (newCount === 0) {
state.multiSelectMode = false;
}
state.multiSelectCount = newCount;
} else {
state.multiSelectMode = true;
state.multiSelectMap[action.payload.id] = action.payload;
state.multiSelectCount++;
}
},
resetMultiSelect: (state) => {
state.multiSelectMode = false;
state.multiSelectMap = {};
state.multiSelectCount = 0;
},
},
});
export const { setMainSelect, resetSelected } = selectedSlice.actions;
export const {
setMainSelect,
resetSelected,
setMultiSelectMode,
resetMultiSelect,
} = selectedSlice.actions;
export default selectedSlice.reducer;