From 126df446116f675a2ab2a896271b4c276173c42b Mon Sep 17 00:00:00 2001 From: jakeaturner Date: Sat, 15 Jun 2024 21:05:11 -0700 Subject: [PATCH] perf(Assets): project assets perf improvements --- .../src/components/FilesManager/EditFile.tsx | 37 ++- .../components/FilesManager/FilePreview.tsx | 30 +- .../components/FilesManager/FilesManager.tsx | 271 +++++++++--------- 3 files changed, 181 insertions(+), 157 deletions(-) diff --git a/client/src/components/FilesManager/EditFile.tsx b/client/src/components/FilesManager/EditFile.tsx index c6246816..88d1bd2f 100644 --- a/client/src/components/FilesManager/EditFile.tsx +++ b/client/src/components/FilesManager/EditFile.tsx @@ -513,24 +513,31 @@ const EditFile: React.FC = ({ )} - {!isFolder && getValues("isVideo") && getValues('videoStorageID') && ( -
- -
- )} + {!isFolder && + getValues("isVideo") && + getValues("videoStorageID") && ( +
+ +
+ )} @@ -829,7 +836,7 @@ const EditFile: React.FC = ({ onClose={() => setShowCaptionsModal(false)} projectID={projectID} fileID={fileID} - key='captions-modal' + key="captions-modal" /> { projectID: string; fileID: string; - file: ProjectFile; + name: string; + isURL?: boolean; + url?: string; + isVideo?: boolean; + videoStorageID?: string; + storageType: "file" | "folder"; videoStreamURL?: string; } const FilePreview: React.FC = ({ projectID, fileID, - file, + name, + isURL, + url, + isVideo, + videoStorageID, + storageType, videoStreamURL, ...rest }) => { @@ -30,12 +40,12 @@ const FilePreview: React.FC = ({ ); // image is default type if not determined useEffect(() => { - if (!file) return; + if (!fileID) return; const { shouldShow, type } = checkShouldShowPreview(); setShouldShowPreview(shouldShow); setPreviewType(type); if (shouldShow && type === "image") loadFileURL(); - }, [file]); + }, [fileID]); async function loadFileURL() { try { @@ -60,23 +70,23 @@ const FilePreview: React.FC = ({ shouldShow: boolean; type: "image" | "video" | "url"; } { - if (!file) return { shouldShow: false, type: "image" }; + if (!fileID) return { shouldShow: false, type: "image" }; // Don't show preview for folders - if (file.storageType !== "file") { + if (storageType !== "file") { return { shouldShow: false, type: "image" }; } - if (file.isURL && file.url) { + if (isURL && url) { return { shouldShow: true, type: "url" }; } - if (file.isVideo && file.videoStorageID) { + if (isVideo && videoStorageID) { return { shouldShow: true, type: "video" }; } // Check if file is an image - const ext = file.name.split(".").pop()?.toLowerCase(); + const ext = name.split(".").pop()?.toLowerCase(); const validImgExt = ["png", "jpg", "jpeg", "gif", "bmp", "svg"].includes( ext ?? "" ); @@ -114,7 +124,7 @@ const FilePreview: React.FC = ({ <>

External URL

- +
)} diff --git a/client/src/components/FilesManager/FilesManager.tsx b/client/src/components/FilesManager/FilesManager.tsx index 11054148..8b5a0750 100644 --- a/client/src/components/FilesManager/FilesManager.tsx +++ b/client/src/components/FilesManager/FilesManager.tsx @@ -1,5 +1,4 @@ -import React, { useState, useEffect, useCallback } from "react"; -import axios from "axios"; +import React, { useState, useMemo } from "react"; import { Button, Loader, @@ -13,7 +12,6 @@ import { SemanticWIDTHS, Popup, Dropdown, - ButtonOr, } from "semantic-ui-react"; const AddFolder = React.lazy(() => import("./AddFolder")); const ChangeAccess = React.lazy(() => import("./ChangeAccess")); @@ -43,8 +41,8 @@ import api from "../../api"; import { saveAs } from "file-saver"; import { useTypedSelector } from "../../state/hooks"; import { base64ToBlob } from "../../utils/misc"; -import { Link } from "react-router-dom"; import { useMediaQuery } from "react-responsive"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; interface FilesManagerProps extends SegmentProps { projectID: string; @@ -81,14 +79,11 @@ const FilesManager: React.FC = ({ { key: "actions", text: "", width: 1, collapsing: true }, ]; - // Global Error Handling + const queryClient = useQueryClient(); const { handleGlobalError } = useGlobalError(); - const user = useTypedSelector((state) => state.user); const isTailwindLg = useMediaQuery({ minWidth: 1024 }, undefined); - const [files, setFiles] = useState([]); - const [showUploader, setShowUploader] = useState(false); const [showAddFolder, setShowAddFolder] = useState(false); const [showChangeAccess, setShowChangeAccess] = useState(false); @@ -96,10 +91,7 @@ const FilesManager: React.FC = ({ const [showEdit, setShowEdit] = useState(false); const [showMove, setShowMove] = useState(false); const [showLargeDownload, setShowLargeDownload] = useState(false); - const [filesLoading, setFilesLoading] = useState(false); const [downloadLoading, setDownloadLoading] = useState(false); - const [itemsChecked, setItemsChecked] = useState(0); - const [allItemsChecked, setAllItemsChecked] = useState(false); const [currDirectory, setCurrDirectory] = useState(""); const [currDirPath, setCurrDirPath] = useState([ @@ -114,113 +106,121 @@ const FilesManager: React.FC = ({ const [accessFiles, setAccessFiles] = useState([]); const [deleteFiles, setDeleteFiles] = useState([]); - /** - * Load the Files list from the server, prepare it for the UI, then save it to state. - */ - const getFiles = async () => { - setFilesLoading(true); + const { data: files, isFetching: filesLoading } = useQuery({ + queryKey: ["project-files", projectID, currDirectory], + queryFn: () => getFiles(), + staleTime: 1000 * 60 * 5, // 5 minutes + refetchOnWindowFocus: false, + }); + + async function getFiles() { try { const fileRes = await api.getProjectFiles(projectID, currDirectory); - if (!fileRes.data.err) { - if (Array.isArray(fileRes.data.files)) { - const withChecked = fileRes.data.files.map((item: ProjectFile) => ({ - ...item, - checked: false, - })); - setFiles(withChecked); - setAllItemsChecked(false); - } - if (Array.isArray(fileRes.data.path)) { - setCurrDirPath(fileRes.data.path); - } - } else { + if (fileRes.data.err) { throw new Error(fileRes.data.errMsg); } - } catch (e) { - handleGlobalError(e); + + if ( + !Array.isArray(fileRes.data.files) || + !Array.isArray(fileRes.data.path) + ) { + throw new Error("Unable to fetch files. Please try again later."); + } + + const withChecked = fileRes.data.files.map((item: ProjectFile) => ({ + ...item, + checked: false, + })); + setCurrDirPath(fileRes.data.path); + + return withChecked; + } catch (err) { + handleGlobalError(err); + return []; } - setFilesLoading(false); - }; + } - /** - * Load the Files list on open. - */ - useEffect(() => { - getFiles(); - }, [currDirectory]); + const itemsChecked = useMemo(() => { + if (!files) return 0; + return files.filter((item) => item.checked).length; + }, [files]); + + const allItemsChecked = useMemo(() => { + if (!files) return false; + return files.length > 0 && files.every((item) => item.checked); + }, [files]); + + const handleEntryCheckedMutation = useMutation({ + mutationFn: ({ id }: { id: string }) => _handleEntryChecked(id), + onSuccess(data, variables, context) { + if (!data) return; + queryClient.setQueryData( + ["project-files", projectID, currDirectory], + data + ); + }, + }); + + const handleToggleAllCheckedMutation = useMutation({ + mutationFn: _handleToggleAllChecked, + onSuccess(data, variables, context) { + if (!data) return; + queryClient.setQueryData( + ["project-files", projectID, currDirectory], + data + ); + }, + }); /** - * Track updates to the number and type of currently checked files. + * Update state when a File entry is checked/unchecked. */ - useEffect(() => { - let numChecked = 0; - files.forEach((item) => { - if (item.checked) { - numChecked += 1; + async function _handleEntryChecked(id: string): Promise { + if (!id || typeof id !== "string") { + if (files) return files; // no change + return []; + } + + if (!files) return []; + const newMapped = files.map((item) => { + if (item.fileID === id) { + return { + ...item, + checked: !item.checked, + }; } + return item; }); - setItemsChecked(numChecked); - }, [files, setItemsChecked]); - - /** - * Update state when a File entry is checked/unchecked. - */ - function handleEntryChecked(e: React.ChangeEvent) { - setFiles((prevFiles) => - prevFiles.map((item) => { - if (item.fileID === e.target.value) { - return { - ...item, - checked: !item.checked, - }; - } - return item; - }) - ); + return newMapped; } /** * Toggles the checked status of all entries in the list. */ - function handleToggleAllChecked() { + async function _handleToggleAllChecked(): Promise { + if (!files) return []; + const foundChecked = files.find((item) => item.checked); if (foundChecked) { // one checked, uncheck all - setFiles((prevFiles) => - prevFiles.map((item) => { - return { - ...item, - checked: false, - }; - }) - ); - setAllItemsChecked(false); - } else { - // none checked, check all - setFiles((prevFiles) => - prevFiles.map((item) => { - return { - ...item, - checked: true, - }; - }) - ); - setAllItemsChecked(true); + const newMapped = files.map((item) => { + return { + ...item, + checked: false, + }; + }); + + return newMapped; } - } - /** - * Sets the Files Uploader tool to open. - */ - function handleShowUploader() { - setShowUploader(true); - } - - /** - * Sets the Files Uploader tool to closed. - */ - function handleUploaderClose() { - setShowUploader(false); + // none checked, check all + const newMapped = files.map((item) => { + return { + ...item, + checked: true, + }; + }); + return newMapped; } /** @@ -228,21 +228,7 @@ const FilesManager: React.FC = ({ */ function handleUploadFinished() { setShowUploader(false); - getFiles(); - } - - /** - * Sets the Add Folder tool to open. - */ - function handleShowAddFolder() { - setShowAddFolder(true); - } - - /** - * Set the Add Folder tool to close. - */ - function handleAddFolderClose() { - setShowAddFolder(false); + queryClient.invalidateQueries(["project-files", projectID, currDirectory]); } /** @@ -250,7 +236,7 @@ const FilesManager: React.FC = ({ */ function handleAddFolderFinished() { setShowAddFolder(false); - getFiles(); + queryClient.invalidateQueries(["project-files", projectID, currDirectory]); } /** @@ -278,7 +264,7 @@ const FilesManager: React.FC = ({ */ function handleEditFinished() { handleEditClose(); - getFiles(); + queryClient.invalidateQueries(["project-files", projectID, currDirectory]); } /** @@ -305,7 +291,7 @@ const FilesManager: React.FC = ({ */ function handleMoveFinished() { handleMoveClose(); - getFiles(); + queryClient.invalidateQueries(["project-files", projectID]); // invalidate entire project as we don't know where it moved } /** @@ -334,7 +320,7 @@ const FilesManager: React.FC = ({ */ function handleAccessFinished() { handleAccessClose(); - getFiles(); + queryClient.invalidateQueries(["project-files", projectID, currDirectory]); } /** @@ -361,10 +347,11 @@ const FilesManager: React.FC = ({ */ function handleDeleteFinished() { handleDeleteClose(); - getFiles(); + queryClient.invalidateQueries(["project-files", projectID, currDirectory]); } function handleDownloadRequest() { + if (!files) return; const requested = files.filter( (obj) => obj.checked && obj.storageType === "file" && !obj.isVideo // filter out videos for now ); @@ -416,7 +403,7 @@ const FilesManager: React.FC = ({ handleGlobalError(err); } finally { setDownloadLoading(false); - handleToggleAllChecked(); + handleToggleAllCheckedMutation.mutate(); } } @@ -498,7 +485,9 @@ const FilesManager: React.FC = ({ handleDownloadFile(projectID, item.fileID, item.isVideo)} + onClick={() => + handleDownloadFile(projectID, item.fileID, item.isVideo) + } /> )} @@ -527,7 +516,9 @@ const FilesManager: React.FC = ({ ) : ( handleDownloadFile(projectID, item.fileID, item.isVideo)} + onClick={() => + handleDownloadFile(projectID, item.fileID, item.isVideo) + } className="text-lg cursor-pointer break-all" > {item.name} @@ -567,14 +558,14 @@ const FilesManager: React.FC = ({ widths="6" className={itemsChecked <= 1 ? "max-w-[34rem]" : ""} > -