Prettier 2: Electric Boogaloo (#6113)

# 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.
This commit is contained in:
James Brunton
2026-04-17 09:50:16 +00:00
committed by GitHub
parent de8c483054
commit 8ab060a4be
1207 changed files with 52646 additions and 15352 deletions
@@ -35,9 +35,14 @@ const CompactFileDetails: React.FC<CompactFileDetailsProps> = ({
const hasSelection = selectedFiles.length > 0;
const hasMultipleFiles = numberOfFiles > 1;
const showOwner = Boolean(
currentFile && (currentFile.remoteOwnedByCurrentUser === false || currentFile.remoteSharedViaLink),
currentFile &&
(currentFile.remoteOwnedByCurrentUser === false ||
currentFile.remoteSharedViaLink),
);
const ownerLabel = currentFile ? currentFile.remoteOwnerUsername || t("fileManager.ownerUnknown", "Unknown") : "";
const ownerLabel = currentFile
? currentFile.remoteOwnerUsername ||
t("fileManager.ownerUnknown", "Unknown")
: "";
return (
<Stack gap="xs" style={{ height: "100%" }}>
@@ -78,7 +83,9 @@ const CompactFileDetails: React.FC<CompactFileDetailsProps> = ({
borderRadius: 4,
}}
>
<PictureAsPdfIcon style={{ fontSize: 20, color: "var(--mantine-color-gray-6)" }} />
<PictureAsPdfIcon
style={{ fontSize: 20, color: "var(--mantine-color-gray-6)" }}
/>
</Center>
) : null}
</Box>
@@ -86,7 +93,9 @@ const CompactFileDetails: React.FC<CompactFileDetailsProps> = ({
{/* File info */}
<Box style={{ flex: 1, minWidth: 0 }}>
<Text size="sm" fw={500} truncate>
<PrivateContent>{currentFile ? currentFile.name : "No file selected"}</PrivateContent>
<PrivateContent>
{currentFile ? currentFile.name : "No file selected"}
</PrivateContent>
</Text>
<Text size="xs" c="dimmed">
{currentFile ? getFileSize(currentFile) : ""}
@@ -101,7 +110,9 @@ const CompactFileDetails: React.FC<CompactFileDetailsProps> = ({
{/* Compact tool chain for mobile */}
{currentFile?.toolHistory && currentFile.toolHistory.length > 0 && (
<Text size="xs" c="dimmed">
{currentFile.toolHistory.map((tool) => t(`home.${tool.toolId}.title`, tool.toolId)).join(" → ")}
{currentFile.toolHistory
.map((tool) => t(`home.${tool.toolId}.title`, tool.toolId))
.join(" → ")}
</Text>
)}
{currentFile && showOwner && (
@@ -114,10 +125,20 @@ const CompactFileDetails: React.FC<CompactFileDetailsProps> = ({
{/* Navigation arrows for multiple files */}
{hasMultipleFiles && (
<Box style={{ display: "flex", gap: "0.25rem" }}>
<ActionIcon variant="subtle" size="sm" onClick={onPrevious} disabled={isAnimating}>
<ActionIcon
variant="subtle"
size="sm"
onClick={onPrevious}
disabled={isAnimating}
>
<ChevronLeftIcon style={{ fontSize: 16 }} />
</ActionIcon>
<ActionIcon variant="subtle" size="sm" onClick={onNext} disabled={isAnimating}>
<ActionIcon
variant="subtle"
size="sm"
onClick={onNext}
disabled={isAnimating}
>
<ChevronRightIcon style={{ fontSize: 16 }} />
</ActionIcon>
</Box>
@@ -131,7 +152,9 @@ const CompactFileDetails: React.FC<CompactFileDetailsProps> = ({
disabled={!hasSelection}
fullWidth
style={{
backgroundColor: hasSelection ? "var(--btn-open-file)" : "var(--mantine-color-gray-4)",
backgroundColor: hasSelection
? "var(--btn-open-file)"
: "var(--mantine-color-gray-4)",
color: "white",
}}
>
@@ -12,7 +12,12 @@ const DesktopLayout: React.FC = () => {
const { activeSource, recentFiles, modalHeight } = useFileManagerContext();
return (
<Grid gutter="xs" h="100%" grow={false} style={{ flexWrap: "nowrap", minWidth: 0 }}>
<Grid
gutter="xs"
h="100%"
grow={false}
style={{ flexWrap: "nowrap", minWidth: 0 }}
>
{/* Column 1: File Sources */}
<Grid.Col
span="content"
@@ -74,9 +79,16 @@ const DesktopLayout: React.FC = () => {
<div style={{ flex: 1, minHeight: 0, overflow: "hidden" }}>
<FileListArea
scrollAreaHeight={activeSource === "recent" && recentFiles.length > 0 ? `calc(${modalHeight} - 7rem)` : "100%"}
scrollAreaHeight={
activeSource === "recent" && recentFiles.length > 0
? `calc(${modalHeight} - 7rem)`
: "100%"
}
scrollAreaStyle={{
height: activeSource === "recent" && recentFiles.length > 0 ? `calc(${modalHeight} - 7rem)` : "100%",
height:
activeSource === "recent" && recentFiles.length > 0
? `calc(${modalHeight} - 7rem)`
: "100%",
backgroundColor: "transparent",
border: "none",
borderRadius: 0,
@@ -32,7 +32,9 @@ const DragOverlay: React.FC<DragOverlayProps> = ({ isVisible }) => {
}}
>
<Stack align="center" gap="md">
<UploadFileIcon style={{ fontSize: "4rem", color: theme.colors.blue[6] }} />
<UploadFileIcon
style={{ fontSize: "4rem", color: theme.colors.blue[6] }}
/>
<Text size="xl" fw={500} c="blue.6">
{t("fileManager.dropFilesHere", "Drop files here to upload")}
</Text>
@@ -1,5 +1,11 @@
import React, { useState } from "react";
import { Button, Group, Text, Stack, useMantineColorScheme } from "@mantine/core";
import {
Button,
Group,
Text,
Stack,
useMantineColorScheme,
} from "@mantine/core";
import HistoryIcon from "@mui/icons-material/History";
import { useTranslation } from "react-i18next";
import { useFileManagerContext } from "@app/contexts/FileManagerContext";
@@ -49,7 +55,9 @@ const EmptyFilesState: React.FC = () => {
>
{/* No Recent Files Message */}
<Stack align="center" gap="sm">
<HistoryIcon style={{ fontSize: "3rem", color: "var(--mantine-color-gray-5)" }} />
<HistoryIcon
style={{ fontSize: "3rem", color: "var(--mantine-color-gray-5)" }}
/>
<Text c="dimmed" ta="center" size="lg">
{t("fileManager.noRecentFiles", "No recent files")}
</Text>
@@ -91,7 +99,8 @@ const EmptyFilesState: React.FC = () => {
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "width .5s ease, padding .5s ease, border-radius .5s ease",
transition:
"width .5s ease, padding .5s ease, border-radius .5s ease",
}}
onClick={handleUploadClick}
onMouseEnter={() => setIsUploadHover(true)}
@@ -102,12 +111,19 @@ const EmptyFilesState: React.FC = () => {
height="1.25rem"
style={{ color: "var(--accent-interactive)" }}
/>
{isUploadHover && <span style={{ marginLeft: ".5rem" }}>{terminology.uploadFromComputer}</span>}
{isUploadHover && (
<span style={{ marginLeft: ".5rem" }}>
{terminology.uploadFromComputer}
</span>
)}
</Button>
</div>
{/* Instruction Text */}
<span className="text-[var(--accent-interactive)]" style={{ fontSize: ".8rem", textAlign: "center" }}>
<span
className="text-[var(--accent-interactive)]"
style={{ fontSize: ".8rem", textAlign: "center" }}
>
{terminology.dropFilesHere}
</span>
</div>
@@ -1,5 +1,11 @@
import React, { useEffect } from "react";
import { Group, Text, ActionIcon, Tooltip, SegmentedControl } from "@mantine/core";
import {
Group,
Text,
ActionIcon,
Tooltip,
SegmentedControl,
} from "@mantine/core";
import SelectAllIcon from "@mui/icons-material/SelectAll";
import DeleteIcon from "@mui/icons-material/Delete";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
@@ -33,33 +39,51 @@ const FileActions: React.FC = () => {
onStorageFilterChange,
} = useFileManagerContext();
const uploadEnabled = config?.storageEnabled === true;
const sharingEnabled = uploadEnabled && config?.storageSharingEnabled === true;
const shareLinksEnabled = sharingEnabled && config?.storageShareLinksEnabled === true;
const sharingEnabled =
uploadEnabled && config?.storageSharingEnabled === true;
const shareLinksEnabled =
sharingEnabled && config?.storageShareLinksEnabled === true;
const showStorageFilter = uploadEnabled;
const storageFilterOptions = sharingEnabled
? [
{ value: "all", label: t("fileManager.filterAll", "All") },
{ value: "local", label: t("fileManager.filterLocal", "Local") },
{ value: "sharedWithMe", label: t("fileManager.filterSharedWithMe", "Shared with me") },
{ value: "sharedByMe", label: t("fileManager.filterSharedByMe", "Shared by me") },
{
value: "sharedWithMe",
label: t("fileManager.filterSharedWithMe", "Shared with me"),
},
{
value: "sharedByMe",
label: t("fileManager.filterSharedByMe", "Shared by me"),
},
]
: [
{ value: "all", label: t("fileManager.filterAll", "All") },
{ value: "local", label: t("fileManager.filterLocal", "Local") },
];
useEffect(() => {
if (!sharingEnabled && (storageFilter === "sharedWithMe" || storageFilter === "sharedByMe")) {
if (
!sharingEnabled &&
(storageFilter === "sharedWithMe" || storageFilter === "sharedByMe")
) {
onStorageFilterChange("all");
}
}, [sharingEnabled, storageFilter, onStorageFilterChange]);
const hasSelection = selectedFileIds.length > 0;
const hasOnlyOwnedSelection = selectedFiles.every((file) => file.remoteOwnedByCurrentUser !== false);
const hasOnlyOwnedSelection = selectedFiles.every(
(file) => file.remoteOwnedByCurrentUser !== false,
);
const hasDownloadAccess = selectedFiles.every((file) => {
const role = (file.remoteOwnedByCurrentUser !== false ? "editor" : (file.remoteAccessRole ?? "viewer")).toLowerCase();
const role = (
file.remoteOwnedByCurrentUser !== false
? "editor"
: (file.remoteAccessRole ?? "viewer")
).toLowerCase();
return role === "editor" || role === "commenter" || role === "viewer";
});
const canBulkUpload = uploadEnabled && hasSelection && hasOnlyOwnedSelection;
const canBulkShare = shareLinksEnabled && hasSelection && hasOnlyOwnedSelection;
const canBulkShare =
shareLinksEnabled && hasSelection && hasOnlyOwnedSelection;
const handleSelectAll = () => {
onSelectAll();
@@ -82,7 +106,8 @@ const FileActions: React.FC = () => {
return null;
}
const allFilesSelected = filteredFiles.length > 0 && selectedFileIds.length === filteredFiles.length;
const allFilesSelected =
filteredFiles.length > 0 && selectedFileIds.length === filteredFiles.length;
return (
<div
@@ -99,7 +124,11 @@ const FileActions: React.FC = () => {
{/* Left: Select All + Filter */}
<Group gap="xs">
<Tooltip
label={allFilesSelected ? t("fileManager.deselectAll", "Deselect All") : t("fileManager.selectAll", "Select All")}
label={
allFilesSelected
? t("fileManager.deselectAll", "Deselect All")
: t("fileManager.selectAll", "Select All")
}
>
<ActionIcon
variant="light"
@@ -116,7 +145,11 @@ const FileActions: React.FC = () => {
<SegmentedControl
size="xs"
value={storageFilter}
onChange={(value) => onStorageFilterChange(value as "all" | "local" | "sharedWithMe" | "sharedByMe")}
onChange={(value) =>
onStorageFilterChange(
value as "all" | "local" | "sharedWithMe" | "sharedByMe",
)
}
data={storageFilterOptions}
/>
)}
@@ -132,7 +165,9 @@ const FileActions: React.FC = () => {
>
{hasSelection && (
<Text size="sm" c="dimmed" fw={500}>
{t("fileManager.selectedCount", "{{count}} selected", { count: selectedFileIds.length })}
{t("fileManager.selectedCount", "{{count}} selected", {
count: selectedFileIds.length,
})}
</Text>
)}
</div>
@@ -18,7 +18,8 @@ const FileDetails: React.FC<FileDetailsProps> = ({ compact = false }) => {
const [isAnimating, setIsAnimating] = useState(false);
// Get the currently displayed file
const currentFile = selectedFiles.length > 0 ? selectedFiles[currentFileIndex] : null;
const currentFile =
selectedFiles.length > 0 ? selectedFiles[currentFileIndex] : null;
const hasSelection = selectedFiles.length > 0;
// Use IndexedDB hook for the current file
@@ -33,7 +34,9 @@ const FileDetails: React.FC<FileDetailsProps> = ({ compact = false }) => {
if (isAnimating) return;
setIsAnimating(true);
setTimeout(() => {
setCurrentFileIndex((prev) => (prev > 0 ? prev - 1 : selectedFiles.length - 1));
setCurrentFileIndex((prev) =>
prev > 0 ? prev - 1 : selectedFiles.length - 1,
);
setIsAnimating(false);
}, 150);
};
@@ -42,7 +45,9 @@ const FileDetails: React.FC<FileDetailsProps> = ({ compact = false }) => {
if (isAnimating) return;
setIsAnimating(true);
setTimeout(() => {
setCurrentFileIndex((prev) => (prev < selectedFiles.length - 1 ? prev + 1 : 0));
setCurrentFileIndex((prev) =>
prev < selectedFiles.length - 1 ? prev + 1 : 0,
);
setIsAnimating(false);
}, 150);
};
@@ -73,7 +78,14 @@ const FileDetails: React.FC<FileDetailsProps> = ({ compact = false }) => {
return (
<Stack gap="lg" h={`calc(${modalHeight} - 2rem)`} justify="flex-start">
{/* Section 1: Thumbnail Preview */}
<Box style={{ width: "100%", height: "min(35vh, 280px)", textAlign: "center", flexShrink: 0 }}>
<Box
style={{
width: "100%",
height: "min(35vh, 280px)",
textAlign: "center",
flexShrink: 0,
}}
>
<FilePreview
file={currentFile}
thumbnail={getCurrentThumbnail()}
@@ -95,7 +107,9 @@ const FileDetails: React.FC<FileDetailsProps> = ({ compact = false }) => {
disabled={!hasSelection}
fullWidth
style={{
backgroundColor: hasSelection ? "var(--btn-open-file)" : "var(--mantine-color-gray-4)",
backgroundColor: hasSelection
? "var(--btn-open-file)"
: "var(--mantine-color-gray-4)",
color: "white",
}}
>
@@ -39,7 +39,8 @@ const FileHistoryGroup: React.FC<FileHistoryGroupProps> = ({
<Box ml="md" mt="xs" mb="sm">
<Group align="center" mb="sm">
<Text size="xs" fw={600} c="dimmed">
{t("fileManager.fileHistory", "File History")} ({sortedHistory.length})
{t("fileManager.fileHistory", "File History")} (
{sortedHistory.length})
</Text>
</Group>
@@ -1,5 +1,15 @@
import React, { useMemo, useState } from "react";
import { Stack, Card, Box, Text, Badge, Group, Divider, ScrollArea, Button } from "@mantine/core";
import {
Stack,
Card,
Box,
Text,
Badge,
Group,
Divider,
ScrollArea,
Button,
} from "@mantine/core";
import { useTranslation } from "react-i18next";
import { detectFileExtension, getFileSize } from "@app/utils/fileUtils";
import { StirlingFileStub } from "@app/types/fileContext";
@@ -14,31 +24,43 @@ interface FileInfoCardProps {
modalHeight: string;
}
const FileInfoCard: React.FC<FileInfoCardProps> = ({ currentFile, modalHeight }) => {
const FileInfoCard: React.FC<FileInfoCardProps> = ({
currentFile,
modalHeight,
}) => {
const { t } = useTranslation();
const { config } = useAppConfig();
const { onMakeCopy } = useFileManagerContext();
const [showShareManageModal, setShowShareManageModal] = useState(false);
const isSharedWithYou = useMemo(() => {
if (!currentFile) return false;
return currentFile.remoteOwnedByCurrentUser === false || currentFile.remoteSharedViaLink;
return (
currentFile.remoteOwnedByCurrentUser === false ||
currentFile.remoteSharedViaLink
);
}, [currentFile]);
const isOwnedRemote = useMemo(() => {
if (!currentFile) return false;
return Boolean(currentFile.remoteStorageId) && currentFile.remoteOwnedByCurrentUser !== false;
return (
Boolean(currentFile.remoteStorageId) &&
currentFile.remoteOwnedByCurrentUser !== false
);
}, [currentFile]);
const localUpdatedAt = currentFile?.createdAt ?? currentFile?.lastModified ?? 0;
const localUpdatedAt =
currentFile?.createdAt ?? currentFile?.lastModified ?? 0;
const remoteUpdatedAt = currentFile?.remoteStorageUpdatedAt ?? 0;
const isUploaded = Boolean(currentFile?.remoteStorageId);
const isUpToDate = isUploaded && remoteUpdatedAt >= localUpdatedAt;
const isOutOfSync = isUploaded && !isUpToDate && isOwnedRemote;
const isLocalOnly = !currentFile?.remoteStorageId && !currentFile?.remoteSharedViaLink;
const isLocalOnly =
!currentFile?.remoteStorageId && !currentFile?.remoteSharedViaLink;
const isSharedByYou = useMemo(() => {
if (!currentFile) return false;
return isOwnedRemote && Boolean(currentFile.remoteHasShareLinks);
}, [currentFile, isOwnedRemote]);
const uploadEnabled = config?.storageEnabled === true;
const sharingEnabled = uploadEnabled && config?.storageSharingEnabled === true;
const sharingEnabled =
uploadEnabled && config?.storageSharingEnabled === true;
const ownerLabel = useMemo(() => {
if (!currentFile) return "";
if (currentFile.remoteOwnerUsername) {
@@ -56,7 +78,12 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({ currentFile, modalHeight })
withBorder
p={0}
mah={`calc(${modalHeight} * 0.45)`}
style={{ overflow: "hidden", flexShrink: 1, display: "flex", flexDirection: "column" }}
style={{
overflow: "hidden",
flexShrink: 1,
display: "flex",
flexDirection: "column",
}}
>
<Box
bg="gray.4"
@@ -75,9 +102,16 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({ currentFile, modalHeight })
<Stack gap="sm">
<Group justify="space-between" py="xs">
<Text size="sm" c="dimmed">
<PrivateContent>{t("fileManager.fileName", "Name")}</PrivateContent>
<PrivateContent>
{t("fileManager.fileName", "Name")}
</PrivateContent>
</Text>
<Text size="sm" fw={500} style={{ maxWidth: "60%", textAlign: "right" }} truncate>
<Text
size="sm"
fw={500}
style={{ maxWidth: "60%", textAlign: "right" }}
truncate
>
{currentFile ? currentFile.name : ""}
</Text>
</Group>
@@ -112,7 +146,9 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({ currentFile, modalHeight })
{t("fileManager.lastModified", "Last modified")}
</Text>
<Text size="sm" fw={500}>
{currentFile ? new Date(currentFile.lastModified).toLocaleDateString() : ""}
{currentFile
? new Date(currentFile.lastModified).toLocaleDateString()
: ""}
</Text>
</Group>
<Divider />
@@ -122,7 +158,11 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({ currentFile, modalHeight })
{t("fileManager.fileVersion", "Version")}
</Text>
{currentFile && (
<Badge size="sm" variant="light" color={currentFile?.versionNumber ? "blue" : "gray"}>
<Badge
size="sm"
variant="light"
color={currentFile?.versionNumber ? "blue" : "gray"}
>
v{currentFile ? currentFile.versionNumber || 1 : ""}
</Badge>
)}
@@ -154,7 +194,11 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({ currentFile, modalHeight })
<Text size="xs" c="dimmed" mb="xs">
{t("fileManager.toolChain", "Tools Applied")}
</Text>
<ToolChain toolChain={currentFile.toolHistory} displayStyle="badges" size="xs" />
<ToolChain
toolChain={currentFile.toolHistory}
displayStyle="badges"
size="xs"
/>
</Box>
</>
)}
@@ -162,7 +206,12 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({ currentFile, modalHeight })
{currentFile && isSharedWithYou && (
<>
<Divider />
<Button size="sm" variant="light" onClick={() => onMakeCopy(currentFile)} fullWidth>
<Button
size="sm"
variant="light"
onClick={() => onMakeCopy(currentFile)}
fullWidth
>
{t("fileManager.makeCopy", "Make a copy")}
</Button>
</>
@@ -177,7 +226,10 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({ currentFile, modalHeight })
</Text>
{uploadEnabled && isOutOfSync ? (
<Badge size="xs" variant="light" color="yellow">
{t("fileManager.changesNotUploaded", "Changes not uploaded")}
{t(
"fileManager.changesNotUploaded",
"Changes not uploaded",
)}
</Badge>
) : uploadEnabled ? (
<Badge size="xs" variant="light" color="teal">
@@ -206,7 +258,12 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({ currentFile, modalHeight })
{t("fileManager.sharedByYou", "Shared by you")}
</Badge>
</Group>
<Button size="sm" variant="light" onClick={() => setShowShareManageModal(true)} fullWidth>
<Button
size="sm"
variant="light"
onClick={() => setShowShareManageModal(true)}
fullWidth
>
{t("storageShare.manage", "Manage sharing")}
</Button>
</>
@@ -12,7 +12,10 @@ interface FileListAreaProps {
scrollAreaStyle?: React.CSSProperties;
}
const FileListArea: React.FC<FileListAreaProps> = ({ scrollAreaHeight, scrollAreaStyle = {} }) => {
const FileListArea: React.FC<FileListAreaProps> = ({
scrollAreaHeight,
scrollAreaStyle = {},
}) => {
const {
activeSource,
recentFiles,
@@ -94,9 +97,14 @@ const FileListArea: React.FC<FileListAreaProps> = ({ scrollAreaHeight, scrollAre
return (
<Center style={{ height: "12.5rem" }}>
<Stack align="center" gap="sm">
<CloudIcon style={{ fontSize: "3rem", color: "var(--mantine-color-gray-5)" }} />
<CloudIcon
style={{ fontSize: "3rem", color: "var(--mantine-color-gray-5)" }}
/>
<Text c="dimmed" ta="center">
{t("fileManager.googleDriveNotAvailable", "Google Drive integration coming soon")}
{t(
"fileManager.googleDriveNotAvailable",
"Google Drive integration coming soon",
)}
</Text>
</Stack>
</Center>
@@ -1,5 +1,14 @@
import React, { useCallback, useMemo, useState } from "react";
import { Group, Box, Text, ActionIcon, Checkbox, Divider, Menu, Badge } from "@mantine/core";
import {
Group,
Box,
Text,
ActionIcon,
Checkbox,
Divider,
Menu,
Badge,
} from "@mantine/core";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import DeleteIcon from "@mui/icons-material/Delete";
import DownloadIcon from "@mui/icons-material/Download";
@@ -60,14 +69,21 @@ const FileListItem: React.FC<FileListItemProps> = ({
const [showShareManageModal, setShowShareManageModal] = useState(false);
const { t } = useTranslation();
const { config } = useAppConfig();
const { expandedFileIds, onToggleExpansion, onUnzipFile, refreshRecentFiles } = useFileManagerContext();
const {
expandedFileIds,
onToggleExpansion,
onUnzipFile,
refreshRecentFiles,
} = useFileManagerContext();
const { removeFiles } = useFileManagement();
// Check if this is a ZIP file
const isZipFile = zipFileService.isZipFileStub(file);
// Check file extension
const extLower = (file.name?.match(/\.([a-z0-9]+)$/i)?.[1] || "").toLowerCase();
const extLower = (
file.name?.match(/\.([a-z0-9]+)$/i)?.[1] || ""
).toLowerCase();
const isCBZ = extLower === "cbz";
const isCBR = extLower === "cbr";
@@ -75,33 +91,55 @@ const FileListItem: React.FC<FileListItemProps> = ({
const shouldShowHovered = isHovered || isMenuOpen;
// Get version information for this file
const leafFileId = (isLatestVersion ? file.id : file.originalFileId || file.id) as FileId;
const leafFileId = (
isLatestVersion ? file.id : file.originalFileId || file.id
) as FileId;
const hasVersionHistory = (file.versionNumber || 1) > 1; // Show history for any processed file (v2+)
const currentVersion = file.versionNumber || 1; // Display original files as v1
const isExpanded = expandedFileIds.has(leafFileId);
const uploadEnabled = config?.storageEnabled === true;
const sharingEnabled = uploadEnabled && config?.storageSharingEnabled === true;
const shareLinksEnabled = sharingEnabled && config?.storageShareLinksEnabled === true;
const sharingEnabled =
uploadEnabled && config?.storageSharingEnabled === true;
const shareLinksEnabled =
sharingEnabled && config?.storageShareLinksEnabled === true;
const isOwnedOrLocal = file.remoteOwnedByCurrentUser !== false;
const isSharedWithYou = sharingEnabled && (file.remoteOwnedByCurrentUser === false || file.remoteSharedViaLink);
const isSharedWithYou =
sharingEnabled &&
(file.remoteOwnedByCurrentUser === false || file.remoteSharedViaLink);
const localUpdatedAt = file.createdAt ?? file.lastModified ?? 0;
const remoteUpdatedAt = file.remoteStorageUpdatedAt ?? 0;
const isUploaded = Boolean(file.remoteStorageId);
const isUpToDate = isUploaded && remoteUpdatedAt >= localUpdatedAt;
const isOutOfSync = isUploaded && !isUpToDate && isOwnedOrLocal;
const isLocalOnly = !file.remoteStorageId && !file.remoteSharedViaLink;
const accessRole = (isOwnedOrLocal ? "editor" : (file.remoteAccessRole ?? "viewer")).toLowerCase();
const hasReadAccess = isOwnedOrLocal || accessRole === "editor" || accessRole === "commenter" || accessRole === "viewer";
const canUpload = uploadEnabled && isOwnedOrLocal && isLatestVersion && (!isUploaded || !isUpToDate);
const accessRole = (
isOwnedOrLocal ? "editor" : (file.remoteAccessRole ?? "viewer")
).toLowerCase();
const hasReadAccess =
isOwnedOrLocal ||
accessRole === "editor" ||
accessRole === "commenter" ||
accessRole === "viewer";
const canUpload =
uploadEnabled &&
isOwnedOrLocal &&
isLatestVersion &&
(!isUploaded || !isUpToDate);
const canShare = shareLinksEnabled && isOwnedOrLocal && isLatestVersion;
const canManageShare = sharingEnabled && isOwnedOrLocal && Boolean(file.remoteStorageId);
const canCopyShareLink = shareLinksEnabled && Boolean(file.remoteHasShareLinks) && Boolean(file.remoteStorageId);
const canManageShare =
sharingEnabled && isOwnedOrLocal && Boolean(file.remoteStorageId);
const canCopyShareLink =
shareLinksEnabled &&
Boolean(file.remoteHasShareLinks) &&
Boolean(file.remoteStorageId);
const canDownloadFile = Boolean(onDownload) && hasReadAccess;
const shareBaseUrl = useMemo(() => {
const frontendUrl = (config?.frontendUrl || "").trim();
if (frontendUrl) {
const normalized = frontendUrl.endsWith("/") ? frontendUrl.slice(0, -1) : frontendUrl;
const normalized = frontendUrl.endsWith("/")
? frontendUrl.slice(0, -1)
: frontendUrl;
return `${normalized}/share/`;
}
return absoluteWithBasePath("/share/");
@@ -110,10 +148,11 @@ const FileListItem: React.FC<FileListItemProps> = ({
const handleCopyShareLink = useCallback(async () => {
if (!file.remoteStorageId) return;
try {
const response = await apiClient.get<{ shareLinks?: Array<{ token?: string }> }>(
`/api/v1/storage/files/${file.remoteStorageId}`,
{ suppressErrorToast: true } as any,
);
const response = await apiClient.get<{
shareLinks?: Array<{ token?: string }>;
}>(`/api/v1/storage/files/${file.remoteStorageId}`, {
suppressErrorToast: true,
} as any);
const links = response.data?.shareLinks ?? [];
const token = links[links.length - 1]?.token;
if (!token) {
@@ -163,9 +202,13 @@ const FileListItem: React.FC<FileListItemProps> = ({
MozUserSelect: "none",
msUserSelect: "none",
paddingLeft: isHistoryFile ? "2rem" : "0.75rem", // Indent history files
borderLeft: isHistoryFile ? "3px solid var(--mantine-color-blue-4)" : "none", // Visual indicator for history
borderLeft: isHistoryFile
? "3px solid var(--mantine-color-blue-4)"
: "none", // Visual indicator for history
}}
onClick={isHistoryFile || isActive ? undefined : (e) => onSelect(e.shiftKey)}
onClick={
isHistoryFile || isActive ? undefined : (e) => onSelect(e.shiftKey)
}
onDoubleClick={onDoubleClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
@@ -217,7 +260,10 @@ const FileListItem: React.FC<FileListItemProps> = ({
{t("fileManager.sharedWithYou", "Shared with you")}
</Badge>
) : null}
{sharingEnabled && isSharedWithYou && accessRole && accessRole !== "editor" ? (
{sharingEnabled &&
isSharedWithYou &&
accessRole &&
accessRole !== "editor" ? (
<Badge size="xs" variant="light" color="gray">
{accessRole === "commenter"
? t("storageShare.roleCommenter", "Commenter")
@@ -228,19 +274,31 @@ const FileListItem: React.FC<FileListItemProps> = ({
{t("fileManager.localOnly", "Local only")}
</Badge>
) : uploadEnabled && isOutOfSync ? (
<Badge size="xs" variant="light" color="yellow" leftSection={<CloudUploadIcon style={{ fontSize: 12 }} />}>
<Badge
size="xs"
variant="light"
color="yellow"
leftSection={<CloudUploadIcon style={{ fontSize: 12 }} />}
>
{t("fileManager.changesNotUploaded", "Changes not uploaded")}
</Badge>
) : uploadEnabled && isUploaded ? (
<Badge size="xs" variant="light" color="teal" leftSection={<CloudDoneIcon style={{ fontSize: 12 }} />}>
<Badge
size="xs"
variant="light"
color="teal"
leftSection={<CloudDoneIcon style={{ fontSize: 12 }} />}
>
{t("fileManager.synced", "Synced")}
</Badge>
) : null}
{sharingEnabled && file.remoteOwnedByCurrentUser !== false && file.remoteHasShareLinks && (
<Badge size="xs" variant="light" color="blue">
{t("fileManager.sharedByYou", "Shared by you")}
</Badge>
)}
{sharingEnabled &&
file.remoteOwnedByCurrentUser !== false &&
file.remoteHasShareLinks && (
<Badge size="xs" variant="light" color="blue">
{t("fileManager.sharedByYou", "Shared by you")}
</Badge>
)}
</Group>
<Group gap="xs" align="center">
<Text size="xs" c="dimmed">
@@ -249,7 +307,12 @@ const FileListItem: React.FC<FileListItemProps> = ({
{/* Tool chain for processed files */}
{file.toolHistory && file.toolHistory.length > 0 && (
<ToolChain toolChain={file.toolHistory} maxWidth={"150px"} displayStyle="text" size="xs" />
<ToolChain
toolChain={file.toolHistory}
maxWidth={"150px"}
displayStyle="text"
size="xs"
/>
)}
</Group>
</Box>
@@ -368,7 +431,9 @@ const FileListItem: React.FC<FileListItemProps> = ({
onToggleExpansion(leafFileId);
}}
>
{isExpanded ? t("fileManager.hideHistory", "Hide History") : t("fileManager.showHistory", "Show History")}
{isExpanded
? t("fileManager.hideHistory", "Hide History")
: t("fileManager.showHistory", "Show History")}
</Menu.Item>
<Menu.Divider />
</>
@@ -436,7 +501,11 @@ const FileListItem: React.FC<FileListItemProps> = ({
/>
)}
{canManageShare && (
<ShareManagementModal opened={showShareManageModal} onClose={() => setShowShareManageModal(false)} file={file} />
<ShareManagementModal
opened={showShareManageModal}
onClose={() => setShowShareManageModal(false)}
file={file}
/>
)}
</>
);
@@ -32,10 +32,19 @@ const GoogleDriveIcon: React.FC<{ disabled?: boolean }> = ({ disabled }) => (
/>
);
const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({ horizontal = false }) => {
const { activeSource, onSourceChange, onLocalFileClick, onGoogleDriveSelect, onNewFilesSelect } = useFileManagerContext();
const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
horizontal = false,
}) => {
const {
activeSource,
onSourceChange,
onLocalFileClick,
onGoogleDriveSelect,
onNewFilesSelect,
} = useFileManagerContext();
const { t } = useTranslation();
const { isEnabled: isGoogleDriveEnabled, openPicker: openGoogleDrivePicker } = useGoogleDrivePicker();
const { isEnabled: isGoogleDriveEnabled, openPicker: openGoogleDrivePicker } =
useGoogleDrivePicker();
const terminology = useFileActionTerminology();
const icons = useFileActionIcons();
const UploadIcon = icons.upload;
@@ -66,21 +75,29 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({ horizontal = fals
};
// Determine visibility of Google Drive button
const shouldHideGoogleDrive = !isGoogleDriveEnabled && config?.hideDisabledToolsGoogleDrive;
const shouldHideGoogleDrive =
!isGoogleDriveEnabled && config?.hideDisabledToolsGoogleDrive;
// Determine visibility of Mobile QR Scanner button
const shouldHideMobileQR = !isMobileUploadEnabled && config?.hideDisabledToolsMobileQRScanner;
const shouldHideMobileQR =
!isMobileUploadEnabled && config?.hideDisabledToolsMobileQRScanner;
const buttonProps = {
variant: (source: string) => (activeSource === source ? "filled" : "subtle"),
getColor: (source: string) => (activeSource === source ? "var(--mantine-color-gray-2)" : undefined),
variant: (source: string) =>
activeSource === source ? "filled" : "subtle",
getColor: (source: string) =>
activeSource === source ? "var(--mantine-color-gray-2)" : undefined,
getStyles: (source: string) => ({
root: {
backgroundColor: activeSource === source ? undefined : "transparent",
color: activeSource === source ? "var(--mantine-color-gray-9)" : "var(--mantine-color-gray-6)",
color:
activeSource === source
? "var(--mantine-color-gray-9)"
: "var(--mantine-color-gray-6)",
border: "none",
"&:hover": {
backgroundColor: activeSource === source ? undefined : "var(--mantine-color-gray-0)",
backgroundColor:
activeSource === source ? undefined : "var(--mantine-color-gray-0)",
},
},
}),
@@ -97,7 +114,9 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({ horizontal = fals
color={buttonProps.getColor("recent")}
styles={buttonProps.getStyles("recent")}
>
{horizontal ? t("fileManager.recent", "Recent") : t("fileManager.recent", "Recent")}
{horizontal
? t("fileManager.recent", "Recent")
: t("fileManager.recent", "Recent")}
</Button>
<Button
@@ -136,17 +155,24 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({ horizontal = fals
backgroundColor: "transparent",
border: "none",
"&:hover": {
backgroundColor: isGoogleDriveEnabled ? "var(--mantine-color-gray-0)" : "transparent",
backgroundColor: isGoogleDriveEnabled
? "var(--mantine-color-gray-0)"
: "transparent",
},
},
}}
title={
!isGoogleDriveEnabled
? t("fileManager.googleDriveNotAvailable", "Google Drive integration not available")
? t(
"fileManager.googleDriveNotAvailable",
"Google Drive integration not available",
)
: undefined
}
>
{horizontal ? t("fileManager.googleDriveShort", "Drive") : t("fileManager.googleDrive", "Google Drive")}
{horizontal
? t("fileManager.googleDriveShort", "Drive")
: t("fileManager.googleDrive", "Google Drive")}
</Button>
)}
@@ -165,13 +191,24 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({ horizontal = fals
backgroundColor: "transparent",
border: "none",
"&:hover": {
backgroundColor: isMobileUploadEnabled ? "var(--mantine-color-gray-0)" : "transparent",
backgroundColor: isMobileUploadEnabled
? "var(--mantine-color-gray-0)"
: "transparent",
},
},
}}
title={!isMobileUploadEnabled ? t("fileManager.mobileUploadNotAvailable", "Mobile upload not available") : undefined}
title={
!isMobileUploadEnabled
? t(
"fileManager.mobileUploadNotAvailable",
"Mobile upload not available",
)
: undefined
}
>
{horizontal ? t("fileManager.mobileShort", "Mobile") : t("fileManager.mobileUpload", "Mobile Upload")}
{horizontal
? t("fileManager.mobileShort", "Mobile")
: t("fileManager.mobileUpload", "Mobile Upload")}
</Button>
)}
</>
@@ -195,7 +232,14 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({ horizontal = fals
return (
<>
<Stack gap="xs" style={{ height: "100%" }}>
<Text size="sm" pt="sm" fw={500} c="dimmed" mb="xs" style={{ paddingLeft: "1rem" }}>
<Text
size="sm"
pt="sm"
fw={500}
c="dimmed"
mb="xs"
style={{ paddingLeft: "1rem" }}
>
{t("fileManager.myFiles", "My Files")}
</Text>
{buttons}
@@ -27,7 +27,11 @@ const MobileLayout: React.FC = () => {
};
return (
<Box h="100%" p="sm" style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
<Box
h="100%"
p="sm"
style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}
>
{/* Section 1: File Sources - Fixed at top */}
<Box style={{ flexShrink: 0 }}>
<FileSourceButtons horizontal={true} />