# Description of Changes When I added Prettier formatting in #6052, my aim was to use just the default settings in Prettier. Turns out, Prettier looks _really hard_ for any config files if it's not explicitly given one, which means that if a developer has some sort of Prettier config file lying around on their system, Prettier might find it and use it. Also, Prettier changes its defaults based on stuff in `.editorconfig` without any good way of disabling that behaviour explicitly in its config file. To solve both of these issues, I've introduced a `.prettierrc` file which sets Prettier's defaults explicitly, and then reformatted all our code _again_ in Prettier's actual default settings. This should achieve the aim of #6052 and remove the possibility for it breaking on different dev computers.
231 lines
7.1 KiB
TypeScript
231 lines
7.1 KiB
TypeScript
import React, { useState, useCallback, useEffect, useMemo } from "react";
|
|
import { Modal } from "@mantine/core";
|
|
import { Dropzone } from "@mantine/dropzone";
|
|
import { StirlingFileStub } from "@app/types/fileContext";
|
|
import { useFileManager } from "@app/hooks/useFileManager";
|
|
import { useFilesModalContext } from "@app/contexts/FilesModalContext";
|
|
import { useAppConfig } from "@app/contexts/AppConfigContext";
|
|
import { Tool } from "@app/types/tool";
|
|
import MobileLayout from "@app/components/fileManager/MobileLayout";
|
|
import DesktopLayout from "@app/components/fileManager/DesktopLayout";
|
|
import DragOverlay from "@app/components/fileManager/DragOverlay";
|
|
import { FileManagerProvider } from "@app/contexts/FileManagerContext";
|
|
import { Z_INDEX_FILE_MANAGER_MODAL } from "@app/styles/zIndex";
|
|
import {
|
|
isGoogleDriveConfigured,
|
|
extractGoogleDriveBackendConfig,
|
|
} from "@app/services/googleDrivePickerService";
|
|
import { loadScript } from "@app/utils/scriptLoader";
|
|
import { useAllFiles } from "@app/contexts/FileContext";
|
|
|
|
interface FileManagerProps {
|
|
selectedTool?: Tool | null;
|
|
}
|
|
|
|
const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
|
const {
|
|
isFilesModalOpen,
|
|
closeFilesModal,
|
|
onFileUpload,
|
|
onRecentFileSelect,
|
|
} = useFilesModalContext();
|
|
const { config } = useAppConfig();
|
|
const [recentFiles, setRecentFiles] = useState<StirlingFileStub[]>([]);
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
const [isMobile, setIsMobile] = useState(false);
|
|
|
|
const { loadRecentFiles, handleRemoveFile, loading } = useFileManager();
|
|
|
|
// Get active file IDs from FileContext to show which files are already loaded
|
|
const { fileIds: activeFileIds } = useAllFiles();
|
|
|
|
// File management handlers
|
|
const isFileSupported = useCallback(
|
|
(fileName: string) => {
|
|
if (!selectedTool?.supportedFormats) return true;
|
|
const extension = fileName.split(".").pop()?.toLowerCase();
|
|
return selectedTool.supportedFormats.includes(extension || "");
|
|
},
|
|
[selectedTool?.supportedFormats],
|
|
);
|
|
|
|
const refreshRecentFiles = useCallback(async () => {
|
|
const files = await loadRecentFiles();
|
|
setRecentFiles(files);
|
|
}, [loadRecentFiles]);
|
|
|
|
const handleRecentFilesSelected = useCallback(
|
|
async (files: StirlingFileStub[]) => {
|
|
try {
|
|
// Use StirlingFileStubs directly - preserves all metadata!
|
|
onRecentFileSelect(files);
|
|
} catch (error) {
|
|
console.error("Failed to process selected files:", error);
|
|
}
|
|
},
|
|
[onRecentFileSelect],
|
|
);
|
|
|
|
const handleNewFileUpload = useCallback(
|
|
async (files: File[]) => {
|
|
if (files.length > 0) {
|
|
try {
|
|
// Files will get IDs assigned through onFilesSelect -> FileContext addFiles
|
|
onFileUpload(files);
|
|
await refreshRecentFiles();
|
|
} catch (error) {
|
|
console.error("Failed to process dropped files:", error);
|
|
}
|
|
}
|
|
},
|
|
[onFileUpload, refreshRecentFiles],
|
|
);
|
|
|
|
const handleRemoveFileByIndex = useCallback(
|
|
async (index: number) => {
|
|
await handleRemoveFile(index, recentFiles, setRecentFiles);
|
|
},
|
|
[handleRemoveFile, recentFiles],
|
|
);
|
|
|
|
useEffect(() => {
|
|
const checkMobile = () => setIsMobile(window.innerWidth < 1030);
|
|
checkMobile();
|
|
window.addEventListener("resize", checkMobile);
|
|
return () => window.removeEventListener("resize", checkMobile);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (isFilesModalOpen) {
|
|
refreshRecentFiles();
|
|
} else {
|
|
// Reset state when modal is closed
|
|
setIsDragging(false);
|
|
}
|
|
}, [isFilesModalOpen, refreshRecentFiles]);
|
|
|
|
// Cleanup any blob URLs when component unmounts
|
|
useEffect(() => {
|
|
return () => {
|
|
// StoredFileMetadata doesn't have blob URLs, so no cleanup needed
|
|
// Blob URLs are managed by FileContext and tool operations
|
|
console.log(
|
|
"FileManager unmounting - FileContext handles blob URL cleanup",
|
|
);
|
|
};
|
|
}, []);
|
|
|
|
// Preload Google Drive scripts if configured
|
|
// Use useMemo to only track Google Drive config changes, not all config updates
|
|
const googleDriveBackendConfig = useMemo(
|
|
() => extractGoogleDriveBackendConfig(config),
|
|
[
|
|
config?.googleDriveEnabled,
|
|
config?.googleDriveClientId,
|
|
config?.googleDriveApiKey,
|
|
config?.googleDriveAppId,
|
|
],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (isGoogleDriveConfigured(googleDriveBackendConfig)) {
|
|
// Load scripts in parallel without blocking
|
|
Promise.all([
|
|
loadScript({
|
|
src: "https://apis.google.com/js/api.js",
|
|
id: "gapi-script",
|
|
async: true,
|
|
defer: true,
|
|
}),
|
|
loadScript({
|
|
src: "https://accounts.google.com/gsi/client",
|
|
id: "gis-script",
|
|
async: true,
|
|
defer: true,
|
|
}),
|
|
]).catch((error) => {
|
|
console.warn("Failed to preload Google Drive scripts:", error);
|
|
});
|
|
}
|
|
}, [googleDriveBackendConfig]);
|
|
|
|
// Modal size constants for consistent scaling
|
|
const modalHeight = "80vh";
|
|
const modalWidth = isMobile ? "100%" : "80vw";
|
|
const modalMaxWidth = isMobile ? "100%" : "1200px";
|
|
const modalMaxHeight = "1200px";
|
|
const modalMinWidth = isMobile ? "320px" : "800px";
|
|
|
|
return (
|
|
<Modal
|
|
opened={isFilesModalOpen}
|
|
onClose={closeFilesModal}
|
|
size={isMobile ? "100%" : "auto"}
|
|
centered
|
|
radius="md"
|
|
className="overflow-hidden p-0"
|
|
withCloseButton={false}
|
|
zIndex={Z_INDEX_FILE_MANAGER_MODAL}
|
|
styles={{
|
|
content: {
|
|
position: "relative",
|
|
margin: isMobile ? "1rem" : "2rem",
|
|
},
|
|
body: { padding: 0 },
|
|
header: { display: "none" },
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
position: "relative",
|
|
height: modalHeight,
|
|
width: modalWidth,
|
|
maxWidth: modalMaxWidth,
|
|
maxHeight: modalMaxHeight,
|
|
minWidth: modalMinWidth,
|
|
margin: "0 auto",
|
|
overflow: "hidden",
|
|
}}
|
|
>
|
|
<Dropzone
|
|
onDrop={handleNewFileUpload}
|
|
onDragEnter={() => setIsDragging(true)}
|
|
onDragLeave={() => setIsDragging(false)}
|
|
multiple={true}
|
|
activateOnClick={false}
|
|
style={{
|
|
height: "100%",
|
|
width: "100%",
|
|
border: "none",
|
|
borderRadius: "var(--radius-md)",
|
|
backgroundColor: "var(--bg-file-manager)",
|
|
}}
|
|
styles={{
|
|
inner: { pointerEvents: "all" },
|
|
}}
|
|
>
|
|
<FileManagerProvider
|
|
recentFiles={recentFiles}
|
|
onRecentFilesSelected={handleRecentFilesSelected}
|
|
onNewFilesSelect={handleNewFileUpload}
|
|
onClose={closeFilesModal}
|
|
isFileSupported={isFileSupported}
|
|
isOpen={isFilesModalOpen}
|
|
onFileRemove={handleRemoveFileByIndex}
|
|
modalHeight={modalHeight}
|
|
refreshRecentFiles={refreshRecentFiles}
|
|
isLoading={loading}
|
|
activeFileIds={activeFileIds}
|
|
>
|
|
{isMobile ? <MobileLayout /> : <DesktopLayout />}
|
|
</FileManagerProvider>
|
|
</Dropzone>
|
|
|
|
<DragOverlay isVisible={isDragging} />
|
|
</div>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default FileManager;
|