Add frontend autoformatting and set CI to require formatted code for all languages (#6052)

# Description of Changes
Changes the strategy for autoformatting to reject PRs if they are not
formatted correctly instead of allowing them to merge and then spawning
a new PR to fix the formatting. The old strategy just caused more work
for us because we'd have to manually approve the followup PR and get it
merged, which required 2 reviewers so in practice it rarely got done and
just meant everyone's PRs ended up containing reformatting for unrelated
files, which makes code review unnecessarily difficult. If the PR's code
is not formatted correctly after this PR, a comment will be added
automatically to tell the author how to run the formatter script to fix
their code so it can go in.

This also enables autoformatting for the frontend code, using Prettier.
I've enabled it for pretty much everything in the frontend folder, other
than 3rd party files and files it doesn't make sense for. I also
excluded Markdown because it sounds likely to be more annoying to have
to autoformat the Markdown in the frontend folder but nowhere else. Open
to changing this though if people disagree.

> [!note]
> 
> Advice to reviewers: The first commit contains all of the actual logic
I've introduced (CI changes, Prettier config, etc.)
> The second commit is just the reformatting of the entire frontend
folder.
> The first commit needs proper review, the second one just give it a
spot-check that it's doing what you'd expect.
This commit is contained in:
James Brunton
2026-04-10 17:41:19 +01:00
committed by GitHub
parent 33b2b5827a
commit a3e45bc182
1359 changed files with 57784 additions and 57460 deletions
@@ -1,12 +1,12 @@
import React from 'react';
import { Stack, Box, Text, Button, ActionIcon, Center } from '@mantine/core';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import { useTranslation } from 'react-i18next';
import { getFileSize } from '@app/utils/fileUtils';
import { StirlingFileStub } from '@app/types/fileContext';
import { PrivateContent } from '@app/components/shared/PrivateContent';
import React from "react";
import { Stack, Box, Text, Button, ActionIcon, Center } from "@mantine/core";
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import { useTranslation } from "react-i18next";
import { getFileSize } from "@app/utils/fileUtils";
import { StirlingFileStub } from "@app/types/fileContext";
import { PrivateContent } from "@app/components/shared/PrivateContent";
interface CompactFileDetailsProps {
currentFile: StirlingFileStub | null;
@@ -29,47 +29,56 @@ const CompactFileDetails: React.FC<CompactFileDetailsProps> = ({
isAnimating,
onPrevious,
onNext,
onOpenFiles
onOpenFiles,
}) => {
const { t } = useTranslation();
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%' }}>
<Stack gap="xs" style={{ height: "100%" }}>
{/* Compact mobile layout */}
<Box style={{ display: 'flex', gap: '0.75rem', alignItems: 'center' }}>
<Box style={{ display: "flex", gap: "0.75rem", alignItems: "center" }}>
{/* Small preview */}
<Box style={{ width: '7.5rem', height: '9.375rem', flexShrink: 0, position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Box
style={{
width: "7.5rem",
height: "9.375rem",
flexShrink: 0,
position: "relative",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{currentFile && thumbnail ? (
<PrivateContent>
<img
src={thumbnail}
alt={currentFile.name}
style={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
borderRadius: '0.25rem',
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)'
maxWidth: "100%",
maxHeight: "100%",
objectFit: "contain",
borderRadius: "0.25rem",
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
}}
/>
</PrivateContent>
) : currentFile ? (
<Center style={{
width: '100%',
height: '100%',
backgroundColor: 'var(--mantine-color-gray-1)',
borderRadius: 4
}}>
<PictureAsPdfIcon style={{ fontSize: 20, color: 'var(--mantine-color-gray-6)' }} />
<Center
style={{
width: "100%",
height: "100%",
backgroundColor: "var(--mantine-color-gray-1)",
borderRadius: 4,
}}
>
<PictureAsPdfIcon style={{ fontSize: 20, color: "var(--mantine-color-gray-6)" }} />
</Center>
) : null}
</Box>
@@ -77,10 +86,10 @@ 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) : ''}
{currentFile ? getFileSize(currentFile) : ""}
{selectedFiles.length > 1 && `${selectedFiles.length} files`}
{currentFile && ` • v${currentFile.versionNumber || 1}`}
</Text>
@@ -92,33 +101,23 @@ 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 && (
<Text size="xs" c="dimmed">
{t('fileManager.owner', 'Owner')}: {ownerLabel}
{t("fileManager.owner", "Owner")}: {ownerLabel}
</Text>
)}
</Box>
{/* Navigation arrows for multiple files */}
{hasMultipleFiles && (
<Box style={{ display: 'flex', gap: '0.25rem' }}>
<ActionIcon
variant="subtle"
size="sm"
onClick={onPrevious}
disabled={isAnimating}
>
<Box style={{ display: "flex", gap: "0.25rem" }}>
<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>
@@ -132,14 +131,13 @@ const CompactFileDetails: React.FC<CompactFileDetailsProps> = ({
disabled={!hasSelection}
fullWidth
style={{
backgroundColor: hasSelection ? 'var(--btn-open-file)' : 'var(--mantine-color-gray-4)',
color: 'white'
backgroundColor: hasSelection ? "var(--btn-open-file)" : "var(--mantine-color-gray-4)",
color: "white",
}}
>
{selectedFiles.length > 1
? t('fileManager.openFiles', `Open ${selectedFiles.length} Files`)
: t('fileManager.openFile', 'Open File')
}
? t("fileManager.openFiles", `Open ${selectedFiles.length} Files`)
: t("fileManager.openFile", "Open File")}
</Button>
</Stack>
);
@@ -1,79 +1,85 @@
import React from 'react';
import { Grid } from '@mantine/core';
import FileSourceButtons from '@app/components/fileManager/FileSourceButtons';
import FileDetails from '@app/components/fileManager/FileDetails';
import SearchInput from '@app/components/fileManager/SearchInput';
import FileListArea from '@app/components/fileManager/FileListArea';
import FileActions from '@app/components/fileManager/FileActions';
import HiddenFileInput from '@app/components/fileManager/HiddenFileInput';
import { useFileManagerContext } from '@app/contexts/FileManagerContext';
import React from "react";
import { Grid } from "@mantine/core";
import FileSourceButtons from "@app/components/fileManager/FileSourceButtons";
import FileDetails from "@app/components/fileManager/FileDetails";
import SearchInput from "@app/components/fileManager/SearchInput";
import FileListArea from "@app/components/fileManager/FileListArea";
import FileActions from "@app/components/fileManager/FileActions";
import HiddenFileInput from "@app/components/fileManager/HiddenFileInput";
import { useFileManagerContext } from "@app/contexts/FileManagerContext";
const DesktopLayout: React.FC = () => {
const {
activeSource,
recentFiles,
modalHeight,
} = useFileManagerContext();
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" p="lg" style={{
minWidth: '13.625rem',
width: '13.625rem',
flexShrink: 0,
height: '100%',
}} data-tour="file-sources">
<Grid.Col
span="content"
p="lg"
style={{
minWidth: "13.625rem",
width: "13.625rem",
flexShrink: 0,
height: "100%",
}}
data-tour="file-sources"
>
<FileSourceButtons />
</Grid.Col>
{/* Column 2: File List */}
<Grid.Col span="auto" style={{
display: 'flex',
flexDirection: 'column',
height: '100%',
minHeight: 0,
minWidth: 0,
flex: '1 1 0px'
}}>
<div style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
backgroundColor: 'var(--bg-file-list)',
border: '1px solid var(--mantine-color-gray-2)',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
overflow: 'hidden'
}}>
{activeSource === 'recent' && (
<Grid.Col
span="auto"
style={{
display: "flex",
flexDirection: "column",
height: "100%",
minHeight: 0,
minWidth: 0,
flex: "1 1 0px",
}}
>
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
backgroundColor: "var(--bg-file-list)",
border: "1px solid var(--mantine-color-gray-2)",
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
overflow: "hidden",
}}
>
{activeSource === "recent" && (
<>
<div style={{
flexShrink: 0,
borderBottom: '1px solid var(--mantine-color-gray-3)'
}}>
<div
style={{
flexShrink: 0,
borderBottom: "1px solid var(--mantine-color-gray-3)",
}}
>
<SearchInput />
</div>
<div style={{
flexShrink: 0,
borderBottom: '1px solid var(--mantine-color-gray-3)'
}}>
<div
style={{
flexShrink: 0,
borderBottom: "1px solid var(--mantine-color-gray-3)",
}}
>
<FileActions />
</div>
</>
)}
<div style={{ flex: 1, minHeight: 0, overflow: 'hidden' }}>
<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%',
backgroundColor: 'transparent',
border: 'none',
borderRadius: 0
height: activeSource === "recent" && recentFiles.length > 0 ? `calc(${modalHeight} - 7rem)` : "100%",
backgroundColor: "transparent",
border: "none",
borderRadius: 0,
}}
/>
</div>
@@ -81,14 +87,18 @@ const DesktopLayout: React.FC = () => {
</Grid.Col>
{/* Column 3: File Details */}
<Grid.Col p="xl" span="content" style={{
minWidth: '25rem',
width: '25rem',
flexShrink: 0,
height: '100%',
maxWidth: '18rem'
}}>
<div style={{ height: '100%', overflow: 'hidden' }}>
<Grid.Col
p="xl"
span="content"
style={{
minWidth: "25rem",
width: "25rem",
flexShrink: 0,
height: "100%",
maxWidth: "18rem",
}}
>
<div style={{ height: "100%", overflow: "hidden" }}>
<FileDetails />
</div>
</Grid.Col>
@@ -1,7 +1,7 @@
import React from 'react';
import { Stack, Text, useMantineTheme, alpha } from '@mantine/core';
import UploadFileIcon from '@mui/icons-material/UploadFile';
import { useTranslation } from 'react-i18next';
import React from "react";
import { Stack, Text, useMantineTheme, alpha } from "@mantine/core";
import UploadFileIcon from "@mui/icons-material/UploadFile";
import { useTranslation } from "react-i18next";
interface DragOverlayProps {
isVisible: boolean;
@@ -16,29 +16,29 @@ const DragOverlay: React.FC<DragOverlayProps> = ({ isVisible }) => {
return (
<div
style={{
position: 'absolute',
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: alpha(theme.colors.blue[6], 0.1),
border: `0.125rem dashed ${theme.colors.blue[6]}`,
borderRadius: '1.875rem',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: "1.875rem",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 1000,
pointerEvents: 'none'
pointerEvents: "none",
}}
>
<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')}
{t("fileManager.dropFilesHere", "Drop files here to upload")}
</Text>
</Stack>
</div>
);
};
export default DragOverlay;
export default DragOverlay;
@@ -1,12 +1,12 @@
import React, { useState } from 'react';
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';
import LocalIcon from '@app/components/shared/LocalIcon';
import { useLogoAssets } from '@app/hooks/useLogoAssets';
import { useFileActionTerminology } from '@app/hooks/useFileActionTerminology';
import { useFileActionIcons } from '@app/hooks/useFileActionIcons';
import React, { useState } from "react";
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";
import LocalIcon from "@app/components/shared/LocalIcon";
import { useLogoAssets } from "@app/hooks/useLogoAssets";
import { useFileActionTerminology } from "@app/hooks/useFileActionTerminology";
import { useFileActionIcons } from "@app/hooks/useFileActionIcons";
const EmptyFilesState: React.FC = () => {
const { t } = useTranslation();
@@ -24,92 +24,90 @@ const EmptyFilesState: React.FC = () => {
return (
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '2rem'
height: "100%",
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "2rem",
}}
>
{/* Container */}
<div
style={{
backgroundColor: 'transparent',
padding: '3rem 2rem',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '1.5rem',
minWidth: '20rem',
maxWidth: '28rem',
width: '100%'
backgroundColor: "transparent",
padding: "3rem 2rem",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: "1.5rem",
minWidth: "20rem",
maxWidth: "28rem",
width: "100%",
}}
>
{/* 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')}
{t("fileManager.noRecentFiles", "No recent files")}
</Text>
</Stack>
{/* Stirling PDF Logo */}
<Group gap="xs" align="center">
<img
src={colorScheme === 'dark' ? wordmark.white : wordmark.grey}
src={colorScheme === "dark" ? wordmark.white : wordmark.grey}
alt="Stirling PDF"
style={{ height: '2.2rem', width: 'auto' }}
style={{ height: "2.2rem", width: "auto" }}
/>
</Group>
{/* Upload Button */}
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
marginTop: '0.5rem',
marginBottom: '0.5rem'
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
marginTop: "0.5rem",
marginBottom: "0.5rem",
}}
onMouseLeave={() => setIsUploadHover(false)}
>
<Button
aria-label="Upload"
style={{
backgroundColor: 'var(--bg-file-manager)',
color: 'var(--landing-button-color)',
border: '1px solid var(--landing-button-border)',
borderRadius: isUploadHover ? '2rem' : '1rem',
height: '38px',
width: isUploadHover ? '100%' : '58px',
minWidth: '58px',
paddingLeft: isUploadHover ? '1rem' : 0,
paddingRight: isUploadHover ? '1rem' : 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'width .5s ease, padding .5s ease, border-radius .5s ease'
backgroundColor: "var(--bg-file-manager)",
color: "var(--landing-button-color)",
border: "1px solid var(--landing-button-border)",
borderRadius: isUploadHover ? "2rem" : "1rem",
height: "38px",
width: isUploadHover ? "100%" : "58px",
minWidth: "58px",
paddingLeft: isUploadHover ? "1rem" : 0,
paddingRight: isUploadHover ? "1rem" : 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "width .5s ease, padding .5s ease, border-radius .5s ease",
}}
onClick={handleUploadClick}
onMouseEnter={() => setIsUploadHover(true)}
>
<LocalIcon icon={icons.uploadIconName} width="1.25rem" height="1.25rem" style={{ color: 'var(--accent-interactive)' }} />
{isUploadHover && (
<span style={{ marginLeft: '.5rem' }}>
{terminology.uploadFromComputer}
</span>
)}
<LocalIcon
icon={icons.uploadIconName}
width="1.25rem"
height="1.25rem"
style={{ color: "var(--accent-interactive)" }}
/>
{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>
@@ -30,9 +30,8 @@ const FileActions: React.FC = () => {
onDownloadSelected,
refreshRecentFiles,
storageFilter,
onStorageFilterChange
} =
useFileManagerContext();
onStorageFilterChange,
} = useFileManagerContext();
const uploadEnabled = config?.storageEnabled === true;
const sharingEnabled = uploadEnabled && config?.storageSharingEnabled === true;
const shareLinksEnabled = sharingEnabled && config?.storageShareLinksEnabled === true;
@@ -42,11 +41,11 @@ const FileActions: React.FC = () => {
{ 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: "sharedByMe", label: t("fileManager.filterSharedByMe", "Shared by me") },
]
: [
{ value: "all", label: t("fileManager.filterAll", "All") },
{ value: "local", label: t("fileManager.filterLocal", "Local") }
{ value: "local", label: t("fileManager.filterLocal", "Local") },
];
useEffect(() => {
if (!sharingEnabled && (storageFilter === "sharedWithMe" || storageFilter === "sharedByMe")) {
@@ -56,10 +55,8 @@ const FileActions: React.FC = () => {
const hasSelection = selectedFileIds.length > 0;
const hasOnlyOwnedSelection = selectedFiles.every((file) => file.remoteOwnedByCurrentUser !== false);
const hasDownloadAccess = selectedFiles.every((file) => {
const role = (file.remoteOwnedByCurrentUser !== false
? 'editor'
: (file.remoteAccessRole ?? 'viewer')).toLowerCase();
return role === 'editor' || role === 'commenter' || role === 'viewer';
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;
@@ -80,7 +77,6 @@ const FileActions: React.FC = () => {
}
};
// Only show actions if there are files
if (recentFiles.length === 0) {
return null;
@@ -120,9 +116,7 @@ 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}
/>
)}
@@ -1,19 +1,17 @@
import React, { useEffect, useState } from 'react';
import { Stack, Button, Box } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { useIndexedDBThumbnail } from '@app/hooks/useIndexedDBThumbnail';
import { useFileManagerContext } from '@app/contexts/FileManagerContext';
import FilePreview from '@app/components/shared/FilePreview';
import FileInfoCard from '@app/components/fileManager/FileInfoCard';
import CompactFileDetails from '@app/components/fileManager/CompactFileDetails';
import React, { useEffect, useState } from "react";
import { Stack, Button, Box } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { useIndexedDBThumbnail } from "@app/hooks/useIndexedDBThumbnail";
import { useFileManagerContext } from "@app/contexts/FileManagerContext";
import FilePreview from "@app/components/shared/FilePreview";
import FileInfoCard from "@app/components/fileManager/FileInfoCard";
import CompactFileDetails from "@app/components/fileManager/CompactFileDetails";
interface FileDetailsProps {
compact?: boolean;
}
const FileDetails: React.FC<FileDetailsProps> = ({
compact = false
}) => {
const FileDetails: React.FC<FileDetailsProps> = ({ compact = false }) => {
const { selectedFiles, onOpenFiles, modalHeight } = useFileManagerContext();
const { t } = useTranslation();
const [currentFileIndex, setCurrentFileIndex] = useState(0);
@@ -35,7 +33,7 @@ const FileDetails: React.FC<FileDetailsProps> = ({
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);
};
@@ -44,7 +42,7 @@ const FileDetails: React.FC<FileDetailsProps> = ({
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);
};
@@ -75,7 +73,7 @@ const FileDetails: React.FC<FileDetailsProps> = ({
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()}
@@ -89,10 +87,7 @@ const FileDetails: React.FC<FileDetailsProps> = ({
</Box>
{/* Section 2: File Details */}
<FileInfoCard
currentFile={currentFile}
modalHeight={modalHeight}
/>
<FileInfoCard currentFile={currentFile} modalHeight={modalHeight} />
<Button
size="md"
@@ -100,14 +95,13 @@ const FileDetails: React.FC<FileDetailsProps> = ({
disabled={!hasSelection}
fullWidth
style={{
backgroundColor: hasSelection ? 'var(--btn-open-file)' : 'var(--mantine-color-gray-4)',
color: 'white'
backgroundColor: hasSelection ? "var(--btn-open-file)" : "var(--mantine-color-gray-4)",
color: "white",
}}
>
{selectedFiles.length > 1
? t('fileManager.openFiles', `Open ${selectedFiles.length} Files`)
: t('fileManager.openFile', 'Open File')
}
? t("fileManager.openFiles", `Open ${selectedFiles.length} Files`)
: t("fileManager.openFile", "Open File")}
</Button>
</Stack>
);
@@ -1,8 +1,8 @@
import React from 'react';
import { Box, Text, Collapse, Group } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { StirlingFileStub } from '@app/types/fileContext';
import FileListItem from '@app/components/fileManager/FileListItem';
import React from "react";
import { Box, Text, Collapse, Group } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { StirlingFileStub } from "@app/types/fileContext";
import FileListItem from "@app/components/fileManager/FileListItem";
interface FileHistoryGroupProps {
leafFile: StirlingFileStub;
@@ -27,7 +27,7 @@ const FileHistoryGroup: React.FC<FileHistoryGroupProps> = ({
// Sort history files by version number (oldest first, excluding the current leaf file)
const sortedHistory = historyFiles
.filter(file => file.id !== leafFile.id) // Exclude the leaf file itself
.filter((file) => file.id !== leafFile.id) // Exclude the leaf file itself
.sort((a, b) => (b.versionNumber || 1) - (a.versionNumber || 1));
if (!isExpanded || sortedHistory.length === 0) {
@@ -39,7 +39,7 @@ 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,23 +1,20 @@
import React, { useMemo, useState } from 'react';
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';
import ToolChain from '@app/components/shared/ToolChain';
import { PrivateContent } from '@app/components/shared/PrivateContent';
import { useFileManagerContext } from '@app/contexts/FileManagerContext';
import ShareManagementModal from '@app/components/shared/ShareManagementModal';
import { useAppConfig } from '@app/contexts/AppConfigContext';
import React, { useMemo, useState } from "react";
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";
import ToolChain from "@app/components/shared/ToolChain";
import { PrivateContent } from "@app/components/shared/PrivateContent";
import { useFileManagerContext } from "@app/contexts/FileManagerContext";
import ShareManagementModal from "@app/components/shared/ShareManagementModal";
import { useAppConfig } from "@app/contexts/AppConfigContext";
interface FileInfoCardProps {
currentFile: StirlingFileStub | null;
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();
@@ -43,38 +40,53 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({
const uploadEnabled = config?.storageEnabled === true;
const sharingEnabled = uploadEnabled && config?.storageSharingEnabled === true;
const ownerLabel = useMemo(() => {
if (!currentFile) return '';
if (!currentFile) return "";
if (currentFile.remoteOwnerUsername) {
return currentFile.remoteOwnerUsername;
}
return t('fileManager.ownerUnknown', 'Unknown');
return t("fileManager.ownerUnknown", "Unknown");
}, [currentFile, t]);
const lastSyncedLabel = useMemo(() => {
if (!currentFile?.remoteStorageUpdatedAt) return '';
if (!currentFile?.remoteStorageUpdatedAt) return "";
return new Date(currentFile.remoteStorageUpdatedAt).toLocaleString();
}, [currentFile?.remoteStorageUpdatedAt]);
return (
<Card withBorder p={0} mah={`calc(${modalHeight} * 0.45)`} style={{ overflow: 'hidden', flexShrink: 1, display: 'flex', flexDirection: 'column' }}>
<Box bg="gray.4" p="sm" style={{ borderTopLeftRadius: 'var(--mantine-radius-md)', borderTopRightRadius: 'var(--mantine-radius-md)', flexShrink: 0 }}>
<Card
withBorder
p={0}
mah={`calc(${modalHeight} * 0.45)`}
style={{ overflow: "hidden", flexShrink: 1, display: "flex", flexDirection: "column" }}
>
<Box
bg="gray.4"
p="sm"
style={{
borderTopLeftRadius: "var(--mantine-radius-md)",
borderTopRightRadius: "var(--mantine-radius-md)",
flexShrink: 0,
}}
>
<Text size="sm" fw={500} ta="center" c="white">
{t('fileManager.details', 'File Details')}
{t("fileManager.details", "File Details")}
</Text>
</Box>
<ScrollArea style={{ flex: 1, minHeight: 0 }} p="md">
<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>
{currentFile ? currentFile.name : ''}
<Text size="sm" fw={500} style={{ maxWidth: "60%", textAlign: "right" }} truncate>
{currentFile ? currentFile.name : ""}
</Text>
</Group>
<Divider />
<Group justify="space-between" py="xs">
<Text size="sm" c="dimmed">{t('fileManager.fileFormat', 'Format')}</Text>
<Text size="sm" c="dimmed">
{t("fileManager.fileFormat", "Format")}
</Text>
{currentFile ? (
<Badge size="sm" variant="light">
{detectFileExtension(currentFile.name).toUpperCase()}
@@ -86,38 +98,48 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({
<Divider />
<Group justify="space-between" py="xs">
<Text size="sm" c="dimmed">{t('fileManager.fileSize', 'Size')}</Text>
<Text size="sm" c="dimmed">
{t("fileManager.fileSize", "Size")}
</Text>
<Text size="sm" fw={500}>
{currentFile ? getFileSize(currentFile) : ''}
{currentFile ? getFileSize(currentFile) : ""}
</Text>
</Group>
<Divider />
<Group justify="space-between" py="xs">
<Text size="sm" c="dimmed">{t('fileManager.lastModified', 'Last modified')}</Text>
<Text size="sm" c="dimmed">
{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 />
<Group justify="space-between" py="xs">
<Text size="sm" c="dimmed">{t('fileManager.fileVersion', 'Version')}</Text>
{currentFile &&
<Badge size="sm" variant="light" color={currentFile?.versionNumber ? 'blue' : 'gray'}>
v{currentFile ? (currentFile.versionNumber || 1) : ''}
</Badge>}
<Text size="sm" c="dimmed">
{t("fileManager.fileVersion", "Version")}
</Text>
{currentFile && (
<Badge size="sm" variant="light" color={currentFile?.versionNumber ? "blue" : "gray"}>
v{currentFile ? currentFile.versionNumber || 1 : ""}
</Badge>
)}
</Group>
{sharingEnabled && isSharedWithYou && (
<>
<Divider />
<Group justify="space-between" py="xs">
<Text size="sm" c="dimmed">{t('fileManager.owner', 'Owner')}</Text>
<Text size="sm" c="dimmed">
{t("fileManager.owner", "Owner")}
</Text>
<Group gap="xs">
<Text size="sm" fw={500}>{ownerLabel}</Text>
<Text size="sm" fw={500}>
{ownerLabel}
</Text>
<Badge size="xs" variant="light" color="grape">
{t('fileManager.sharedWithYou', 'Shared with you')}
{t("fileManager.sharedWithYou", "Shared with you")}
</Badge>
</Group>
</Group>
@@ -129,12 +151,10 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({
<>
<Divider />
<Box py="xs">
<Text size="xs" c="dimmed" mb="xs">{t('fileManager.toolChain', 'Tools Applied')}</Text>
<ToolChain
toolChain={currentFile.toolHistory}
displayStyle="badges"
size="xs"
/>
<Text size="xs" c="dimmed" mb="xs">
{t("fileManager.toolChain", "Tools Applied")}
</Text>
<ToolChain toolChain={currentFile.toolHistory} displayStyle="badges" size="xs" />
</Box>
</>
)}
@@ -142,13 +162,8 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({
{currentFile && isSharedWithYou && (
<>
<Divider />
<Button
size="sm"
variant="light"
onClick={() => onMakeCopy(currentFile)}
fullWidth
>
{t('fileManager.makeCopy', 'Make a copy')}
<Button size="sm" variant="light" onClick={() => onMakeCopy(currentFile)} fullWidth>
{t("fileManager.makeCopy", "Make a copy")}
</Button>
</>
)}
@@ -157,39 +172,42 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({
<>
<Divider />
<Group justify="space-between" py="xs">
<Text size="sm" c="dimmed">{t('fileManager.cloudFile', 'Cloud file')}</Text>
<Text size="sm" c="dimmed">
{t("fileManager.cloudFile", "Cloud file")}
</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">
{t('fileManager.synced', 'Synced')}
{t("fileManager.synced", "Synced")}
</Badge>
) : null}
</Group>
{lastSyncedLabel && (
<Group justify="space-between" py="xs">
<Text size="sm" c="dimmed">{t('fileManager.lastSynced', 'Last synced')}</Text>
<Text size="sm" fw={500}>{lastSyncedLabel}</Text>
<Text size="sm" c="dimmed">
{t("fileManager.lastSynced", "Last synced")}
</Text>
<Text size="sm" fw={500}>
{lastSyncedLabel}
</Text>
</Group>
)}
{isSharedByYou && sharingEnabled && (
<>
<Divider />
<Group justify="space-between" py="xs">
<Text size="sm" c="dimmed">{t('fileManager.sharing', 'Sharing')}</Text>
<Text size="sm" c="dimmed">
{t("fileManager.sharing", "Sharing")}
</Text>
<Badge size="xs" variant="light" color="blue">
{t('fileManager.sharedByYou', 'Shared by you')}
{t("fileManager.sharedByYou", "Shared by you")}
</Badge>
</Group>
<Button
size="sm"
variant="light"
onClick={() => setShowShareManageModal(true)}
fullWidth
>
{t('storageShare.manage', 'Manage sharing')}
<Button size="sm" variant="light" onClick={() => setShowShareManageModal(true)} fullWidth>
{t("storageShare.manage", "Manage sharing")}
</Button>
</>
)}
@@ -199,9 +217,11 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({
<>
<Divider />
<Group justify="space-between" py="xs">
<Text size="sm" c="dimmed">{t('fileManager.storageState', 'Storage')}</Text>
<Text size="sm" c="dimmed">
{t("fileManager.storageState", "Storage")}
</Text>
<Badge size="xs" variant="light" color="gray">
{t('fileManager.localOnly', 'Local only')}
{t("fileManager.localOnly", "Local only")}
</Badge>
</Group>
</>
@@ -1,21 +1,18 @@
import React from 'react';
import { Center, ScrollArea, Text, Stack } from '@mantine/core';
import CloudIcon from '@mui/icons-material/Cloud';
import { useTranslation } from 'react-i18next';
import FileListItem from '@app/components/fileManager/FileListItem';
import FileHistoryGroup from '@app/components/fileManager/FileHistoryGroup';
import EmptyFilesState from '@app/components/fileManager/EmptyFilesState';
import { useFileManagerContext } from '@app/contexts/FileManagerContext';
import React from "react";
import { Center, ScrollArea, Text, Stack } from "@mantine/core";
import CloudIcon from "@mui/icons-material/Cloud";
import { useTranslation } from "react-i18next";
import FileListItem from "@app/components/fileManager/FileListItem";
import FileHistoryGroup from "@app/components/fileManager/FileHistoryGroup";
import EmptyFilesState from "@app/components/fileManager/EmptyFilesState";
import { useFileManagerContext } from "@app/contexts/FileManagerContext";
interface FileListAreaProps {
scrollAreaHeight: string;
scrollAreaStyle?: React.CSSProperties;
}
const FileListArea: React.FC<FileListAreaProps> = ({
scrollAreaHeight,
scrollAreaStyle = {},
}) => {
const FileListArea: React.FC<FileListAreaProps> = ({ scrollAreaHeight, scrollAreaStyle = {} }) => {
const {
activeSource,
recentFiles,
@@ -34,12 +31,12 @@ const FileListArea: React.FC<FileListAreaProps> = ({
} = useFileManagerContext();
const { t } = useTranslation();
if (activeSource === 'recent') {
if (activeSource === "recent") {
return (
<ScrollArea
h={scrollAreaHeight}
style={{
...scrollAreaStyle
...scrollAreaStyle,
}}
type="always"
scrollbarSize={8}
@@ -48,8 +45,10 @@ const FileListArea: React.FC<FileListAreaProps> = ({
{recentFiles.length === 0 && !isLoading ? (
<EmptyFilesState />
) : recentFiles.length === 0 && isLoading ? (
<Center style={{ height: '12.5rem' }}>
<Text c="dimmed" ta="center">{t('fileManager.loadingFiles', 'Loading files...')}</Text>
<Center style={{ height: "12.5rem" }}>
<Text c="dimmed" ta="center">
{t("fileManager.loadingFiles", "Loading files...")}
</Text>
</Center>
) : (
filteredFiles.map((file, index) => {
@@ -93,10 +92,12 @@ const FileListArea: React.FC<FileListAreaProps> = ({
// Google Drive placeholder
return (
<Center style={{ height: '12.5rem' }}>
<Center style={{ height: "12.5rem" }}>
<Stack align="center" gap="sm">
<CloudIcon style={{ fontSize: '3rem', color: 'var(--mantine-color-gray-5)' }} />
<Text c="dimmed" ta="center">{t('fileManager.googleDriveNotAvailable', 'Google Drive integration coming soon')}</Text>
<CloudIcon style={{ fontSize: "3rem", color: "var(--mantine-color-gray-5)" }} />
<Text c="dimmed" ta="center">
{t("fileManager.googleDriveNotAvailable", "Google Drive integration coming soon")}
</Text>
</Stack>
</Center>
);
@@ -1,31 +1,31 @@
import React, { useCallback, useMemo, useState } from 'react';
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';
import HistoryIcon from '@mui/icons-material/History';
import RestoreIcon from '@mui/icons-material/Restore';
import UnarchiveIcon from '@mui/icons-material/Unarchive';
import CloseIcon from '@mui/icons-material/Close';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import CloudDoneIcon from '@mui/icons-material/CloudDone';
import LinkIcon from '@mui/icons-material/Link';
import { useTranslation } from 'react-i18next';
import { getFileSize, getFileDate } from '@app/utils/fileUtils';
import { FileId, StirlingFileStub } from '@app/types/fileContext';
import { useFileManagerContext } from '@app/contexts/FileManagerContext';
import { zipFileService } from '@app/services/zipFileService';
import ToolChain from '@app/components/shared/ToolChain';
import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from '@app/styles/zIndex';
import { PrivateContent } from '@app/components/shared/PrivateContent';
import { useFileManagement } from '@app/contexts/FileContext';
import UploadToServerModal from '@app/components/shared/UploadToServerModal';
import ShareFileModal from '@app/components/shared/ShareFileModal';
import { useAppConfig } from '@app/contexts/AppConfigContext';
import ShareManagementModal from '@app/components/shared/ShareManagementModal';
import apiClient from '@app/services/apiClient';
import { absoluteWithBasePath } from '@app/constants/app';
import { alert } from '@app/components/toast';
import React, { useCallback, useMemo, useState } from "react";
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";
import HistoryIcon from "@mui/icons-material/History";
import RestoreIcon from "@mui/icons-material/Restore";
import UnarchiveIcon from "@mui/icons-material/Unarchive";
import CloseIcon from "@mui/icons-material/Close";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
import CloudDoneIcon from "@mui/icons-material/CloudDone";
import LinkIcon from "@mui/icons-material/Link";
import { useTranslation } from "react-i18next";
import { getFileSize, getFileDate } from "@app/utils/fileUtils";
import { FileId, StirlingFileStub } from "@app/types/fileContext";
import { useFileManagerContext } from "@app/contexts/FileManagerContext";
import { zipFileService } from "@app/services/zipFileService";
import ToolChain from "@app/components/shared/ToolChain";
import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from "@app/styles/zIndex";
import { PrivateContent } from "@app/components/shared/PrivateContent";
import { useFileManagement } from "@app/contexts/FileContext";
import UploadToServerModal from "@app/components/shared/UploadToServerModal";
import ShareFileModal from "@app/components/shared/ShareFileModal";
import { useAppConfig } from "@app/contexts/AppConfigContext";
import ShareManagementModal from "@app/components/shared/ShareManagementModal";
import apiClient from "@app/services/apiClient";
import { absoluteWithBasePath } from "@app/constants/app";
import { alert } from "@app/components/toast";
interface FileListItemProps {
file: StirlingFileStub;
@@ -51,7 +51,7 @@ const FileListItem: React.FC<FileListItemProps> = ({
onDoubleClick,
isHistoryFile = false,
isLatestVersion = false,
isActive = false
isActive = false,
}) => {
const [isHovered, setIsHovered] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
@@ -60,22 +60,22 @@ 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 isCBZ = extLower === 'cbz';
const isCBR = extLower === 'cbr';
const extLower = (file.name?.match(/\.([a-z0-9]+)$/i)?.[1] || "").toLowerCase();
const isCBZ = extLower === "cbz";
const isCBR = extLower === "cbr";
// Keep item in hovered state if menu is open
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);
@@ -83,32 +83,28 @@ const FileListItem: React.FC<FileListItemProps> = ({
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 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 canCopyShareLink = shareLinksEnabled && Boolean(file.remoteHasShareLinks) && Boolean(file.remoteStorageId);
const canDownloadFile = Boolean(onDownload) && hasReadAccess;
const shareBaseUrl = useMemo(() => {
const frontendUrl = (config?.frontendUrl || '').trim();
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/');
return absoluteWithBasePath("/share/");
}, [config?.frontendUrl]);
const handleCopyShareLink = useCallback(async () => {
@@ -116,14 +112,14 @@ const FileListItem: React.FC<FileListItemProps> = ({
try {
const response = await apiClient.get<{ shareLinks?: Array<{ token?: string }> }>(
`/api/v1/storage/files/${file.remoteStorageId}`,
{ suppressErrorToast: true } as any
{ suppressErrorToast: true } as any,
);
const links = response.data?.shareLinks ?? [];
const token = links[links.length - 1]?.token;
if (!token) {
alert({
alertType: 'warning',
title: t('storageShare.noLinks', 'No active share links yet.'),
alertType: "warning",
title: t("storageShare.noLinks", "No active share links yet."),
expandable: false,
durationMs: 2500,
});
@@ -131,16 +127,16 @@ const FileListItem: React.FC<FileListItemProps> = ({
}
await navigator.clipboard.writeText(`${shareBaseUrl}${token}`);
alert({
alertType: 'success',
title: t('storageShare.copied', 'Link copied to clipboard'),
alertType: "success",
title: t("storageShare.copied", "Link copied to clipboard"),
expandable: false,
durationMs: 2000,
});
} catch (error) {
console.error('Failed to copy share link:', error);
console.error("Failed to copy share link:", error);
alert({
alertType: 'warning',
title: t('storageShare.copyFailed', 'Copy failed'),
alertType: "warning",
title: t("storageShare.copyFailed", "Copy failed"),
expandable: false,
durationMs: 2500,
});
@@ -152,20 +148,22 @@ const FileListItem: React.FC<FileListItemProps> = ({
<Box
p="sm"
style={{
cursor: isHistoryFile || isActive ? 'default' : 'pointer',
cursor: isHistoryFile || isActive ? "default" : "pointer",
backgroundColor: isActive
? 'var(--file-active-bg)'
: isSelected
? 'var(--mantine-color-gray-1)'
: (shouldShowHovered ? 'var(--mantine-color-gray-1)' : 'var(--bg-file-list)'),
? "var(--file-active-bg)"
: isSelected
? "var(--mantine-color-gray-1)"
: shouldShowHovered
? "var(--mantine-color-gray-1)"
: "var(--bg-file-list)",
opacity: isSupported ? 1 : 0.5,
transition: 'background-color 0.15s ease',
userSelect: 'none',
WebkitUserSelect: 'none',
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
transition: "background-color 0.15s ease",
userSelect: "none",
WebkitUserSelect: "none",
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
}}
onClick={isHistoryFile || isActive ? undefined : (e) => onSelect(e.shiftKey)}
onDoubleClick={onDoubleClick}
@@ -186,8 +184,8 @@ const FileListItem: React.FC<FileListItemProps> = ({
color={isActive ? "green" : undefined}
styles={{
input: {
cursor: isActive ? 'not-allowed' : 'pointer'
}
cursor: isActive ? "not-allowed" : "pointer",
},
}}
/>
</Box>
@@ -203,12 +201,12 @@ const FileListItem: React.FC<FileListItemProps> = ({
size="xs"
variant="light"
style={{
backgroundColor: 'var(--file-active-badge-bg)',
color: 'var(--file-active-badge-fg)',
border: '1px solid var(--file-active-badge-border)'
backgroundColor: "var(--file-active-badge-bg)",
color: "var(--file-active-badge-fg)",
border: "1px solid var(--file-active-badge-border)",
}}
>
{t('fileManager.active', 'Active')}
{t("fileManager.active", "Active")}
</Badge>
)}
<Badge size="xs" variant="light" color={"blue"}>
@@ -216,44 +214,33 @@ const FileListItem: React.FC<FileListItemProps> = ({
</Badge>
{sharingEnabled && isSharedWithYou ? (
<Badge size="xs" variant="light" color="grape">
{t('fileManager.sharedWithYou', 'Shared with you')}
{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')
: t('storageShare.roleViewer', 'Viewer')}
{accessRole === "commenter"
? t("storageShare.roleCommenter", "Commenter")
: t("storageShare.roleViewer", "Viewer")}
</Badge>
) : isLocalOnly ? (
<Badge size="xs" variant="light" color="gray">
{t('fileManager.localOnly', 'Local only')}
{t("fileManager.localOnly", "Local only")}
</Badge>
) : uploadEnabled && isOutOfSync ? (
<Badge
size="xs"
variant="light"
color="yellow"
leftSection={<CloudUploadIcon style={{ fontSize: 12 }} />}
>
{t('fileManager.changesNotUploaded', 'Changes not uploaded')}
<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 }} />}
>
{t('fileManager.synced', 'Synced')}
<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')}
{t("fileManager.sharedByYou", "Shared by you")}
</Badge>
)}
</Group>
<Group gap="xs" align="center">
<Text size="xs" c="dimmed">
@@ -262,12 +249,7 @@ 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>
@@ -288,9 +270,9 @@ const FileListItem: React.FC<FileListItemProps> = ({
onClick={(e) => e.stopPropagation()}
style={{
opacity: shouldShowHovered ? 1 : 0,
transform: shouldShowHovered ? 'scale(1)' : 'scale(0.8)',
transition: 'opacity 0.3s ease, transform 0.3s ease',
pointerEvents: shouldShowHovered ? 'auto' : 'none'
transform: shouldShowHovered ? "scale(1)" : "scale(0.8)",
transition: "opacity 0.3s ease, transform 0.3s ease",
pointerEvents: shouldShowHovered ? "auto" : "none",
}}
>
<MoreVertIcon style={{ fontSize: 20 }} />
@@ -308,7 +290,7 @@ const FileListItem: React.FC<FileListItemProps> = ({
removeFiles([file.id]);
}}
>
{t('fileManager.closeFile', 'Close File')}
{t("fileManager.closeFile", "Close File")}
</Menu.Item>
<Menu.Divider />
</>
@@ -322,7 +304,7 @@ const FileListItem: React.FC<FileListItemProps> = ({
onDownload?.();
}}
>
{t('fileManager.download', 'Download')}
{t("fileManager.download", "Download")}
</Menu.Item>
)}
@@ -335,8 +317,8 @@ const FileListItem: React.FC<FileListItemProps> = ({
}}
>
{isUploaded
? t('fileManager.updateOnServer', 'Update on Server')
: t('fileManager.uploadToServer', 'Upload to Server')}
? t("fileManager.updateOnServer", "Update on Server")
: t("fileManager.uploadToServer", "Upload to Server")}
</Menu.Item>
)}
@@ -348,7 +330,7 @@ const FileListItem: React.FC<FileListItemProps> = ({
setShowShareModal(true);
}}
>
{t('fileManager.share', 'Share')}
{t("fileManager.share", "Share")}
</Menu.Item>
)}
@@ -360,7 +342,7 @@ const FileListItem: React.FC<FileListItemProps> = ({
void handleCopyShareLink();
}}
>
{t('storageShare.copyLink', 'Copy share link')}
{t("storageShare.copyLink", "Copy share link")}
</Menu.Item>
)}
@@ -372,7 +354,7 @@ const FileListItem: React.FC<FileListItemProps> = ({
setShowShareManageModal(true);
}}
>
{t('storageShare.manage', 'Manage sharing')}
{t("storageShare.manage", "Manage sharing")}
</Menu.Item>
)}
@@ -380,20 +362,13 @@ const FileListItem: React.FC<FileListItemProps> = ({
{isLatestVersion && hasVersionHistory && (
<>
<Menu.Item
leftSection={
<HistoryIcon style={{ fontSize: 16 }} />
}
leftSection={<HistoryIcon style={{ fontSize: 16 }} />}
onClick={(e) => {
e.stopPropagation();
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 />
</>
@@ -408,7 +383,7 @@ const FileListItem: React.FC<FileListItemProps> = ({
e.stopPropagation();
}}
>
{t('fileManager.restore', 'Restore')}
{t("fileManager.restore", "Restore")}
</Menu.Item>
<Menu.Divider />
</>
@@ -424,7 +399,7 @@ const FileListItem: React.FC<FileListItemProps> = ({
onUnzipFile(file);
}}
>
{t('fileManager.unzip', 'Unzip')}
{t("fileManager.unzip", "Unzip")}
</Menu.Item>
<Menu.Divider />
</>
@@ -437,14 +412,13 @@ const FileListItem: React.FC<FileListItemProps> = ({
onRemove();
}}
>
{t('fileManager.delete', 'Delete')}
{t("fileManager.delete", "Delete")}
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Group>
</Box>
{ <Divider color="var(--mantine-color-gray-3)" />}
{<Divider color="var(--mantine-color-gray-3)" />}
{canUpload && (
<UploadToServerModal
opened={showUploadModal}
@@ -462,11 +436,7 @@ const FileListItem: React.FC<FileListItemProps> = ({
/>
)}
{canManageShare && (
<ShareManagementModal
opened={showShareManageModal}
onClose={() => setShowShareManageModal(false)}
file={file}
/>
<ShareManagementModal opened={showShareManageModal} onClose={() => setShowShareManageModal(false)} file={file} />
)}
</>
);
@@ -1,15 +1,15 @@
import React, { useState } from 'react';
import { Stack, Text, Button, Group } from '@mantine/core';
import HistoryIcon from '@mui/icons-material/History';
import PhonelinkIcon from '@mui/icons-material/Phonelink';
import { useTranslation } from 'react-i18next';
import { useFileManagerContext } from '@app/contexts/FileManagerContext';
import { useGoogleDrivePicker } from '@app/hooks/useGoogleDrivePicker';
import { useFileActionTerminology } from '@app/hooks/useFileActionTerminology';
import { useFileActionIcons } from '@app/hooks/useFileActionIcons';
import { useAppConfig } from '@app/contexts/AppConfigContext';
import { useIsMobile } from '@app/hooks/useIsMobile';
import MobileUploadModal from '@app/components/shared/MobileUploadModal';
import React, { useState } from "react";
import { Stack, Text, Button, Group } from "@mantine/core";
import HistoryIcon from "@mui/icons-material/History";
import PhonelinkIcon from "@mui/icons-material/Phonelink";
import { useTranslation } from "react-i18next";
import { useFileManagerContext } from "@app/contexts/FileManagerContext";
import { useGoogleDrivePicker } from "@app/hooks/useGoogleDrivePicker";
import { useFileActionTerminology } from "@app/hooks/useFileActionTerminology";
import { useFileActionIcons } from "@app/hooks/useFileActionIcons";
import { useAppConfig } from "@app/contexts/AppConfigContext";
import { useIsMobile } from "@app/hooks/useIsMobile";
import MobileUploadModal from "@app/components/shared/MobileUploadModal";
interface FileSourceButtonsProps {
horizontal?: boolean;
@@ -24,17 +24,15 @@ const GoogleDriveIcon: React.FC<{ disabled?: boolean }> = ({ disabled }) => (
src="/images/google-drive.svg"
alt="Google Drive"
style={{
width: '20px',
height: '20px',
width: "20px",
height: "20px",
opacity: disabled ? 0.5 : 1,
filter: disabled ? 'grayscale(100%)' : 'none',
filter: disabled ? "grayscale(100%)" : "none",
}}
/>
);
const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
horizontal = false
}) => {
const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({ horizontal = false }) => {
const { activeSource, onSourceChange, onLocalFileClick, onGoogleDriveSelect, onNewFilesSelect } = useFileManagerContext();
const { t } = useTranslation();
const { isEnabled: isGoogleDriveEnabled, openPicker: openGoogleDrivePicker } = useGoogleDrivePicker();
@@ -53,7 +51,7 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
onGoogleDriveSelect(files);
}
} catch (error) {
console.error('Failed to pick files from Google Drive:', error);
console.error("Failed to pick files from Google Drive:", error);
}
};
@@ -74,18 +72,18 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
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)',
border: 'none',
'&:hover': {
backgroundColor: activeSource === source ? undefined : 'var(--mantine-color-gray-0)'
}
}
})
backgroundColor: activeSource === source ? undefined : "transparent",
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)",
},
},
}),
};
const buttons = (
@@ -93,18 +91,18 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
<Button
leftSection={<HistoryIcon />}
justify={horizontal ? "center" : "flex-start"}
onClick={() => onSourceChange('recent')}
onClick={() => onSourceChange("recent")}
fullWidth={!horizontal}
size={horizontal ? "xs" : "sm"}
color={buttonProps.getColor('recent')}
styles={buttonProps.getStyles('recent')}
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
variant="subtle"
color='var(--mantine-color-gray-6)'
color="var(--mantine-color-gray-6)"
leftSection={<UploadIcon />}
justify={horizontal ? "center" : "flex-start"}
onClick={onLocalFileClick}
@@ -112,12 +110,12 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
size={horizontal ? "xs" : "sm"}
styles={{
root: {
backgroundColor: 'transparent',
border: 'none',
'&:hover': {
backgroundColor: 'var(--mantine-color-gray-0)'
}
}
backgroundColor: "transparent",
border: "none",
"&:hover": {
backgroundColor: "var(--mantine-color-gray-0)",
},
},
}}
>
{horizontal ? terminology.upload : terminology.uploadFiles}
@@ -126,7 +124,7 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
{!shouldHideGoogleDrive && (
<Button
variant="subtle"
color='var(--mantine-color-gray-6)'
color="var(--mantine-color-gray-6)"
leftSection={<GoogleDriveIcon disabled={!isGoogleDriveEnabled} />}
justify={horizontal ? "center" : "flex-start"}
onClick={handleGoogleDriveClick}
@@ -135,23 +133,27 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
disabled={!isGoogleDriveEnabled}
styles={{
root: {
backgroundColor: 'transparent',
border: 'none',
'&:hover': {
backgroundColor: isGoogleDriveEnabled ? 'var(--mantine-color-gray-0)' : 'transparent'
}
}
backgroundColor: "transparent",
border: "none",
"&:hover": {
backgroundColor: isGoogleDriveEnabled ? "var(--mantine-color-gray-0)" : "transparent",
},
},
}}
title={!isGoogleDriveEnabled ? t('fileManager.googleDriveNotAvailable', 'Google Drive integration not available') : undefined}
title={
!isGoogleDriveEnabled
? 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>
)}
{!shouldHideMobileQR && (
<Button
variant="subtle"
color='var(--mantine-color-gray-6)'
color="var(--mantine-color-gray-6)"
leftSection={<PhonelinkIcon />}
justify={horizontal ? "center" : "flex-start"}
onClick={handleMobileUploadClick}
@@ -160,16 +162,16 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
disabled={!isMobileUploadEnabled}
styles={{
root: {
backgroundColor: 'transparent',
border: 'none',
'&:hover': {
backgroundColor: isMobileUploadEnabled ? 'var(--mantine-color-gray-0)' : 'transparent'
}
}
backgroundColor: "transparent",
border: "none",
"&:hover": {
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>
)}
</>
@@ -178,7 +180,7 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
if (horizontal) {
return (
<>
<Group gap="xs" justify="center" style={{ width: '100%' }}>
<Group gap="xs" justify="center" style={{ width: "100%" }}>
{buttons}
</Group>
<MobileUploadModal
@@ -192,9 +194,9 @@ const FileSourceButtons: React.FC<FileSourceButtonsProps> = ({
return (
<>
<Stack gap="xs" style={{ height: '100%' }}>
<Text size="sm" pt="sm" fw={500} c="dimmed" mb="xs" style={{ paddingLeft: '1rem' }}>
{t('fileManager.myFiles', 'My Files')}
<Stack gap="xs" style={{ height: "100%" }}>
<Text size="sm" pt="sm" fw={500} c="dimmed" mb="xs" style={{ paddingLeft: "1rem" }}>
{t("fileManager.myFiles", "My Files")}
</Text>
{buttons}
</Stack>
@@ -1,5 +1,5 @@
import React from 'react';
import { useFileManagerContext } from '@app/contexts/FileManagerContext';
import React from "react";
import { useFileManagerContext } from "@app/contexts/FileManagerContext";
const HiddenFileInput: React.FC = () => {
const { fileInputRef, onFileInputChange } = useFileManagerContext();
@@ -10,7 +10,7 @@ const HiddenFileInput: React.FC = () => {
type="file"
multiple={true}
onChange={onFileInputChange}
style={{ display: 'none' }}
style={{ display: "none" }}
data-testid="file-input"
/>
);
@@ -1,19 +1,15 @@
import React from 'react';
import { Box } from '@mantine/core';
import FileSourceButtons from '@app/components/fileManager/FileSourceButtons';
import FileDetails from '@app/components/fileManager/FileDetails';
import SearchInput from '@app/components/fileManager/SearchInput';
import FileListArea from '@app/components/fileManager/FileListArea';
import FileActions from '@app/components/fileManager/FileActions';
import HiddenFileInput from '@app/components/fileManager/HiddenFileInput';
import { useFileManagerContext } from '@app/contexts/FileManagerContext';
import React from "react";
import { Box } from "@mantine/core";
import FileSourceButtons from "@app/components/fileManager/FileSourceButtons";
import FileDetails from "@app/components/fileManager/FileDetails";
import SearchInput from "@app/components/fileManager/SearchInput";
import FileListArea from "@app/components/fileManager/FileListArea";
import FileActions from "@app/components/fileManager/FileActions";
import HiddenFileInput from "@app/components/fileManager/HiddenFileInput";
import { useFileManagerContext } from "@app/contexts/FileManagerContext";
const MobileLayout: React.FC = () => {
const {
activeSource,
selectedFiles,
modalHeight,
} = useFileManagerContext();
const { activeSource, selectedFiles, modalHeight } = useFileManagerContext();
// Calculate the height more accurately based on actual content
const calculateFileListHeight = () => {
@@ -21,17 +17,17 @@ const MobileLayout: React.FC = () => {
const baseHeight = `calc(${modalHeight} - 2rem)`; // Account for Stack padding
// Estimate heights of fixed components
const fileSourceHeight = '3rem'; // FileSourceButtons height
const fileDetailsHeight = selectedFiles.length > 0 ? '10rem' : '8rem'; // FileDetails compact height
const fileActionsHeight = activeSource === 'recent' ? '3rem' : '0rem'; // FileActions height (now at bottom)
const searchHeight = activeSource === 'recent' ? '3rem' : '0rem'; // SearchInput height
const gapHeight = activeSource === 'recent' ? '3.75rem' : '2rem'; // Stack gaps
const fileSourceHeight = "3rem"; // FileSourceButtons height
const fileDetailsHeight = selectedFiles.length > 0 ? "10rem" : "8rem"; // FileDetails compact height
const fileActionsHeight = activeSource === "recent" ? "3rem" : "0rem"; // FileActions height (now at bottom)
const searchHeight = activeSource === "recent" ? "3rem" : "0rem"; // SearchInput height
const gapHeight = activeSource === "recent" ? "3.75rem" : "2rem"; // Stack gaps
return `calc(${baseHeight} - ${fileSourceHeight} - ${fileDetailsHeight} - ${fileActionsHeight} - ${searchHeight} - ${gapHeight})`;
};
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} />
@@ -42,28 +38,34 @@ const MobileLayout: React.FC = () => {
</Box>
{/* Section 3 & 4: Search Bar + File List - Unified background extending to modal edge */}
<Box style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
backgroundColor: 'var(--bg-file-list)',
borderRadius: '0.5rem',
border: '1px solid var(--mantine-color-gray-2)',
overflow: 'hidden',
minHeight: 0
}}>
{activeSource === 'recent' && (
<Box
style={{
flex: 1,
display: "flex",
flexDirection: "column",
backgroundColor: "var(--bg-file-list)",
borderRadius: "0.5rem",
border: "1px solid var(--mantine-color-gray-2)",
overflow: "hidden",
minHeight: 0,
}}
>
{activeSource === "recent" && (
<>
<Box style={{
flexShrink: 0,
borderBottom: '1px solid var(--mantine-color-gray-2)'
}}>
<Box
style={{
flexShrink: 0,
borderBottom: "1px solid var(--mantine-color-gray-2)",
}}
>
<SearchInput />
</Box>
<Box style={{
flexShrink: 0,
borderBottom: '1px solid var(--mantine-color-gray-2)'
}}>
<Box
style={{
flexShrink: 0,
borderBottom: "1px solid var(--mantine-color-gray-2)",
}}
>
<FileActions />
</Box>
</>
@@ -74,11 +76,11 @@ const MobileLayout: React.FC = () => {
scrollAreaHeight={calculateFileListHeight()}
scrollAreaStyle={{
height: calculateFileListHeight(),
maxHeight: '60vh',
minHeight: '9.375rem',
backgroundColor: 'transparent',
border: 'none',
borderRadius: 0
maxHeight: "60vh",
minHeight: "9.375rem",
backgroundColor: "transparent",
border: "none",
borderRadius: 0,
}}
/>
</Box>
@@ -1,8 +1,8 @@
import React from 'react';
import { TextInput } from '@mantine/core';
import SearchIcon from '@mui/icons-material/Search';
import { useTranslation } from 'react-i18next';
import { useFileManagerContext } from '@app/contexts/FileManagerContext';
import React from "react";
import { TextInput } from "@mantine/core";
import SearchIcon from "@mui/icons-material/Search";
import { useTranslation } from "react-i18next";
import { useFileManagerContext } from "@app/contexts/FileManagerContext";
interface SearchInputProps {
style?: React.CSSProperties;
@@ -14,20 +14,19 @@ const SearchInput: React.FC<SearchInputProps> = ({ style }) => {
return (
<TextInput
placeholder={t('fileManager.searchFiles', 'Search files...')}
placeholder={t("fileManager.searchFiles", "Search files...")}
leftSection={<SearchIcon />}
value={searchTerm}
onChange={(e) => onSearchChange(e.target.value)}
style={{ padding: '0.5rem', ...style }}
style={{ padding: "0.5rem", ...style }}
styles={{
input: {
border: 'none',
backgroundColor: 'transparent'
}
border: "none",
backgroundColor: "transparent",
},
}}
/>
);
};
export default SearchInput;
export default SearchInput;