mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 03:20:46 +02:00
fileshare (#5414)
Co-authored-by: ConnorYoh <[email protected]> Co-authored-by: Connor Yoh <[email protected]> Co-authored-by: EthanHealy01 <[email protected]> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
ConnorYoh
Connor Yoh
EthanHealy01
Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
parent
47cad0a131
commit
28613caf8a
@@ -34,6 +34,13 @@ const CompactFileDetails: React.FC<CompactFileDetailsProps> = ({
|
||||
const { t } = useTranslation();
|
||||
const hasSelection = selectedFiles.length > 0;
|
||||
const hasMultipleFiles = numberOfFiles > 1;
|
||||
const showOwner = Boolean(
|
||||
currentFile &&
|
||||
(currentFile.remoteOwnedByCurrentUser === false || currentFile.remoteSharedViaLink)
|
||||
);
|
||||
const ownerLabel = currentFile
|
||||
? currentFile.remoteOwnerUsername || t('fileManager.ownerUnknown', 'Unknown')
|
||||
: '';
|
||||
|
||||
return (
|
||||
<Stack gap="xs" style={{ height: '100%' }}>
|
||||
@@ -88,6 +95,11 @@ const CompactFileDetails: React.FC<CompactFileDetailsProps> = ({
|
||||
{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}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Navigation arrows for multiple files */}
|
||||
|
||||
@@ -1,25 +1,68 @@
|
||||
import React from "react";
|
||||
import { Group, Text, ActionIcon, Tooltip } from "@mantine/core";
|
||||
import React, { useEffect } from "react";
|
||||
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";
|
||||
import LinkIcon from "@mui/icons-material/Link";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFileManagerContext } from "@app/contexts/FileManagerContext";
|
||||
import { useFileActionTerminology } from "@app/hooks/useFileActionTerminology";
|
||||
import { useFileActionIcons } from "@app/hooks/useFileActionIcons";
|
||||
import { useAppConfig } from "@app/contexts/AppConfigContext";
|
||||
import BulkUploadToServerModal from "@app/components/shared/BulkUploadToServerModal";
|
||||
import BulkShareModal from "@app/components/shared/BulkShareModal";
|
||||
|
||||
const FileActions: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const terminology = useFileActionTerminology();
|
||||
const icons = useFileActionIcons();
|
||||
const DownloadIcon = icons.download;
|
||||
const { config } = useAppConfig();
|
||||
const [showBulkUploadModal, setShowBulkUploadModal] = React.useState(false);
|
||||
const [showBulkShareModal, setShowBulkShareModal] = React.useState(false);
|
||||
const {
|
||||
recentFiles,
|
||||
selectedFileIds,
|
||||
selectedFiles,
|
||||
filteredFiles,
|
||||
onSelectAll,
|
||||
onDeleteSelected,
|
||||
onDownloadSelected
|
||||
} = useFileManagerContext();
|
||||
onDownloadSelected,
|
||||
refreshRecentFiles,
|
||||
storageFilter,
|
||||
onStorageFilterChange
|
||||
} =
|
||||
useFileManagerContext();
|
||||
const uploadEnabled = config?.storageEnabled === 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: "all", label: t("fileManager.filterAll", "All") },
|
||||
{ value: "local", label: t("fileManager.filterLocal", "Local") }
|
||||
];
|
||||
useEffect(() => {
|
||||
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 hasDownloadAccess = selectedFiles.every((file) => {
|
||||
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 handleSelectAll = () => {
|
||||
onSelectAll();
|
||||
@@ -44,7 +87,6 @@ const FileActions: React.FC = () => {
|
||||
}
|
||||
|
||||
const allFilesSelected = filteredFiles.length > 0 && selectedFileIds.length === filteredFiles.length;
|
||||
const hasSelection = selectedFileIds.length > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -58,8 +100,8 @@ const FileActions: React.FC = () => {
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{/* Left: Select All */}
|
||||
<div>
|
||||
{/* Left: Select All + Filter */}
|
||||
<Group gap="xs">
|
||||
<Tooltip
|
||||
label={allFilesSelected ? t("fileManager.deselectAll", "Deselect All") : t("fileManager.selectAll", "Select All")}
|
||||
>
|
||||
@@ -74,7 +116,17 @@ const FileActions: React.FC = () => {
|
||||
<SelectAllIcon style={{ fontSize: "1rem" }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{showStorageFilter && (
|
||||
<SegmentedControl
|
||||
size="xs"
|
||||
value={storageFilter}
|
||||
onChange={(value) =>
|
||||
onStorageFilterChange(value as "all" | "local" | "sharedWithMe" | "sharedByMe")
|
||||
}
|
||||
data={storageFilterOptions}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
{/* Center: Selected count */}
|
||||
<div
|
||||
@@ -93,6 +145,34 @@ const FileActions: React.FC = () => {
|
||||
|
||||
{/* Right: Delete and Download */}
|
||||
<Group gap="xs">
|
||||
{uploadEnabled && (
|
||||
<Tooltip label={t("fileManager.uploadSelected", "Upload Selected")}>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
color="dimmed"
|
||||
onClick={() => setShowBulkUploadModal(true)}
|
||||
disabled={!canBulkUpload}
|
||||
radius="sm"
|
||||
>
|
||||
<CloudUploadIcon style={{ fontSize: "1rem" }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{shareLinksEnabled && (
|
||||
<Tooltip label={t("fileManager.shareSelected", "Share Selected")}>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
color="dimmed"
|
||||
onClick={() => setShowBulkShareModal(true)}
|
||||
disabled={!canBulkShare}
|
||||
radius="sm"
|
||||
>
|
||||
<LinkIcon style={{ fontSize: "1rem" }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip label={t("fileManager.deleteSelected", "Delete Selected")}>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
@@ -112,13 +192,30 @@ const FileActions: React.FC = () => {
|
||||
size="sm"
|
||||
color="dimmed"
|
||||
onClick={handleDownloadSelected}
|
||||
disabled={!hasSelection}
|
||||
disabled={!hasSelection || !hasDownloadAccess}
|
||||
radius="sm"
|
||||
>
|
||||
<DownloadIcon style={{ fontSize: "1rem" }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
{uploadEnabled && (
|
||||
<BulkUploadToServerModal
|
||||
opened={showBulkUploadModal}
|
||||
onClose={() => setShowBulkUploadModal(false)}
|
||||
files={selectedFiles}
|
||||
onUploaded={refreshRecentFiles}
|
||||
/>
|
||||
)}
|
||||
{shareLinksEnabled && (
|
||||
<BulkShareModal
|
||||
opened={showBulkShareModal}
|
||||
onClose={() => setShowBulkShareModal(false)}
|
||||
files={selectedFiles}
|
||||
onShared={refreshRecentFiles}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Stack, Card, Box, Text, Badge, Group, Divider, ScrollArea } from '@mantine/core';
|
||||
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;
|
||||
@@ -16,6 +19,40 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({
|
||||
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;
|
||||
}, [currentFile]);
|
||||
const isOwnedRemote = useMemo(() => {
|
||||
if (!currentFile) return false;
|
||||
return Boolean(currentFile.remoteStorageId) && currentFile.remoteOwnedByCurrentUser !== false;
|
||||
}, [currentFile]);
|
||||
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 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 ownerLabel = useMemo(() => {
|
||||
if (!currentFile) return '';
|
||||
if (currentFile.remoteOwnerUsername) {
|
||||
return currentFile.remoteOwnerUsername;
|
||||
}
|
||||
return t('fileManager.ownerUnknown', 'Unknown');
|
||||
}, [currentFile, t]);
|
||||
const lastSyncedLabel = useMemo(() => {
|
||||
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' }}>
|
||||
@@ -57,7 +94,7 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({
|
||||
<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() : ''}
|
||||
</Text>
|
||||
@@ -72,6 +109,20 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({
|
||||
</Badge>}
|
||||
|
||||
</Group>
|
||||
{sharingEnabled && isSharedWithYou && (
|
||||
<>
|
||||
<Divider />
|
||||
<Group justify="space-between" py="xs">
|
||||
<Text size="sm" c="dimmed">{t('fileManager.owner', 'Owner')}</Text>
|
||||
<Group gap="xs">
|
||||
<Text size="sm" fw={500}>{ownerLabel}</Text>
|
||||
<Badge size="xs" variant="light" color="grape">
|
||||
{t('fileManager.sharedWithYou', 'Shared with you')}
|
||||
</Badge>
|
||||
</Group>
|
||||
</Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Tool Chain Display */}
|
||||
{currentFile?.toolHistory && currentFile.toolHistory.length > 0 && (
|
||||
@@ -87,8 +138,83 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{currentFile && isSharedWithYou && (
|
||||
<>
|
||||
<Divider />
|
||||
<Button
|
||||
size="sm"
|
||||
variant="light"
|
||||
onClick={() => onMakeCopy(currentFile)}
|
||||
fullWidth
|
||||
>
|
||||
{t('fileManager.makeCopy', 'Make a copy')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{currentFile && isOwnedRemote && (
|
||||
<>
|
||||
<Divider />
|
||||
<Group justify="space-between" py="xs">
|
||||
<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')}
|
||||
</Badge>
|
||||
) : uploadEnabled ? (
|
||||
<Badge size="xs" variant="light" color="teal">
|
||||
{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>
|
||||
</Group>
|
||||
)}
|
||||
{isSharedByYou && sharingEnabled && (
|
||||
<>
|
||||
<Divider />
|
||||
<Group justify="space-between" py="xs">
|
||||
<Text size="sm" c="dimmed">{t('fileManager.sharing', 'Sharing')}</Text>
|
||||
<Badge size="xs" variant="light" color="blue">
|
||||
{t('fileManager.sharedByYou', 'Shared by you')}
|
||||
</Badge>
|
||||
</Group>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="light"
|
||||
onClick={() => setShowShareManageModal(true)}
|
||||
fullWidth
|
||||
>
|
||||
{t('storageShare.manage', 'Manage sharing')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{currentFile && isLocalOnly && (
|
||||
<>
|
||||
<Divider />
|
||||
<Group justify="space-between" py="xs">
|
||||
<Text size="sm" c="dimmed">{t('fileManager.storageState', 'Storage')}</Text>
|
||||
<Badge size="xs" variant="light" color="gray">
|
||||
{t('fileManager.localOnly', 'Local only')}
|
||||
</Badge>
|
||||
</Group>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
{currentFile && isOwnedRemote && isSharedByYou && sharingEnabled && (
|
||||
<ShareManagementModal
|
||||
opened={showShareManageModal}
|
||||
onClose={() => setShowShareManageModal(false)}
|
||||
file={currentFile}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
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';
|
||||
@@ -7,6 +7,9 @@ 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';
|
||||
@@ -16,6 +19,13 @@ 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;
|
||||
@@ -45,8 +55,12 @@ const FileListItem: React.FC<FileListItemProps> = ({
|
||||
}) => {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||
const [showShareModal, setShowShareModal] = useState(false);
|
||||
const [showShareManageModal, setShowShareManageModal] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const { expandedFileIds, onToggleExpansion, onUnzipFile } = useFileManagerContext();
|
||||
const { config } = useAppConfig();
|
||||
const {expandedFileIds, onToggleExpansion, onUnzipFile, refreshRecentFiles } = useFileManagerContext();
|
||||
const { removeFiles } = useFileManagement();
|
||||
|
||||
// Check if this is a ZIP file
|
||||
@@ -65,6 +79,73 @@ const FileListItem: React.FC<FileListItemProps> = ({
|
||||
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 isOwnedOrLocal = file.remoteOwnedByCurrentUser !== false;
|
||||
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 canShare = shareLinksEnabled && isOwnedOrLocal && isLatestVersion;
|
||||
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;
|
||||
return `${normalized}/share/`;
|
||||
}
|
||||
return absoluteWithBasePath('/share/');
|
||||
}, [config?.frontendUrl]);
|
||||
|
||||
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 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.'),
|
||||
expandable: false,
|
||||
durationMs: 2500,
|
||||
});
|
||||
return;
|
||||
}
|
||||
await navigator.clipboard.writeText(`${shareBaseUrl}${token}`);
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('storageShare.copied', 'Link copied to clipboard'),
|
||||
expandable: false,
|
||||
durationMs: 2000,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to copy share link:', error);
|
||||
alert({
|
||||
alertType: 'warning',
|
||||
title: t('storageShare.copyFailed', 'Copy failed'),
|
||||
expandable: false,
|
||||
durationMs: 2500,
|
||||
});
|
||||
}
|
||||
}, [file.remoteStorageId, shareBaseUrl, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -133,6 +214,45 @@ const FileListItem: React.FC<FileListItemProps> = ({
|
||||
<Badge size="xs" variant="light" color={"blue"}>
|
||||
v{currentVersion}
|
||||
</Badge>
|
||||
{sharingEnabled && isSharedWithYou ? (
|
||||
<Badge size="xs" variant="light" color="grape">
|
||||
{t('fileManager.sharedWithYou', 'Shared with you')}
|
||||
</Badge>
|
||||
) : null}
|
||||
{sharingEnabled && isSharedWithYou && accessRole && accessRole !== 'editor' ? (
|
||||
<Badge size="xs" variant="light" color="gray">
|
||||
{accessRole === 'commenter'
|
||||
? t('storageShare.roleCommenter', 'Commenter')
|
||||
: t('storageShare.roleViewer', 'Viewer')}
|
||||
</Badge>
|
||||
) : isLocalOnly ? (
|
||||
<Badge size="xs" variant="light" color="gray">
|
||||
{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>
|
||||
) : uploadEnabled && isUploaded ? (
|
||||
<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>
|
||||
)}
|
||||
|
||||
</Group>
|
||||
<Group gap="xs" align="center">
|
||||
@@ -194,18 +314,68 @@ const FileListItem: React.FC<FileListItemProps> = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{onDownload && (
|
||||
{canDownloadFile && (
|
||||
<Menu.Item
|
||||
leftSection={<DownloadIcon style={{ fontSize: 16 }} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDownload();
|
||||
onDownload?.();
|
||||
}}
|
||||
>
|
||||
{t('fileManager.download', 'Download')}
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
{canUpload && (
|
||||
<Menu.Item
|
||||
leftSection={<CloudUploadIcon style={{ fontSize: 16 }} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowUploadModal(true);
|
||||
}}
|
||||
>
|
||||
{isUploaded
|
||||
? t('fileManager.updateOnServer', 'Update on Server')
|
||||
: t('fileManager.uploadToServer', 'Upload to Server')}
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
{canShare && (
|
||||
<Menu.Item
|
||||
leftSection={<LinkIcon style={{ fontSize: 16 }} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowShareModal(true);
|
||||
}}
|
||||
>
|
||||
{t('fileManager.share', 'Share')}
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
{canCopyShareLink && (
|
||||
<Menu.Item
|
||||
leftSection={<LinkIcon style={{ fontSize: 16 }} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
void handleCopyShareLink();
|
||||
}}
|
||||
>
|
||||
{t('storageShare.copyLink', 'Copy share link')}
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
{canManageShare && (
|
||||
<Menu.Item
|
||||
leftSection={<LinkIcon style={{ fontSize: 16 }} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowShareManageModal(true);
|
||||
}}
|
||||
>
|
||||
{t('storageShare.manage', 'Manage sharing')}
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
{/* Show/Hide History option for latest version files */}
|
||||
{isLatestVersion && hasVersionHistory && (
|
||||
<>
|
||||
@@ -275,6 +445,29 @@ const FileListItem: React.FC<FileListItemProps> = ({
|
||||
</Group>
|
||||
</Box>
|
||||
{ <Divider color="var(--mantine-color-gray-3)" />}
|
||||
{canUpload && (
|
||||
<UploadToServerModal
|
||||
opened={showUploadModal}
|
||||
onClose={() => setShowUploadModal(false)}
|
||||
file={file}
|
||||
onUploaded={refreshRecentFiles}
|
||||
/>
|
||||
)}
|
||||
{canShare && (
|
||||
<ShareFileModal
|
||||
opened={showShareModal}
|
||||
onClose={() => setShowShareModal(false)}
|
||||
file={file}
|
||||
onUploaded={refreshRecentFiles}
|
||||
/>
|
||||
)}
|
||||
{canManageShare && (
|
||||
<ShareManagementModal
|
||||
opened={showShareManageModal}
|
||||
onClose={() => setShowShareManageModal(false)}
|
||||
file={file}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user