added multiselect logic, implemented trash and multi trash
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 = ["/"];
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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[]) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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;
|
||||
@@ -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")}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user