feat(explorer): manage created direct links / option to enable unique redirected direct links

This commit is contained in:
Aaron Liu 2025-06-26 18:45:49 +08:00
parent 820349093b
commit 672b00192c
20 changed files with 389 additions and 27 deletions

View File

@ -192,6 +192,10 @@
"no": "No",
"permanentValid": "Permanent",
"manageShares": "Manage share links",
"manageDirectLinks": "Manage direct links",
"deleteLinkConfirm": "Are you sure to delete this direct link?",
"directLinkNotFound": "The direct link you are looking for does not exist.",
"versionNotFound": "The version you are looking for does not exist.",
"deleteVersionWarning": "Are you sure to delete this version? This operation cannot be undone.",
"setAsCurrent": "Set as current version",
"current": "[Current]",
@ -854,4 +858,4 @@
"used": "Used - {{size}}",
"total": "Total - {{size}}"
}
}
}

View File

@ -645,7 +645,8 @@
"add_passkey": "Add passkey",
"remove_passkey": "Remove passkey",
"redeem_gift_code": "Redeem gift code",
"update_view": "Changed view setting"
"update_view": "Changed view setting",
"delete_direct_link": "Delete direct link"
},
"server": "Server",
"tempPath": "Temporary path",
@ -1104,6 +1105,8 @@
"bathSourceLinkLimitDes": "The maximum number of files allowed for users to obtain direct links in a single batch, fill in 0 means no batch generation of direct links is allowed.",
"redirectedSource": "Use redirected direct link",
"redirectedSourceDes": "Recommended to enable. When enabled, the direct link to the file obtained by the user will be redirected by Cloudreve with a shorter link. When disabled, the direct link to the file obtained by the user becomes the original URL to the file, and is bound to the file version. Some policies produce non-redirected direct links that do not remain persistent; see Cloudreve documents for details.",
"reuseDirectLink": "Reuse existing direct link",
"reuseDirectLinkDes": "When enabled, multiple requests for the same file's direct link will reuse the existing redirect link.",
"downloadSpeedLimit": "Max download speed",
"downloadSpeedLimitDes": "Fill in 0 to indicate no limit. When the restriction is turned on, the maximum download speed will be limited when users download all files under the storage policy that supports the speed limit.",
"anonymousHint": "This user group corresponds to the anonymous visitor who is not signed in.",

View File

@ -184,9 +184,6 @@
"save": "保存",
"noMoreImages": "現在のページには閲覧可能な画像がありません",
"imageViewer": "画像ビューア",
"logUpdateView": "ビュー設定を更新",
"logFileDeleteShare": "共有リンクを削除",
"logFileEditShare": "共有リンクを編集",
"deleteShareWarning": "この共有リンクを削除しますか?",
"edit": "編集",
"editAndReactivate": "編集して再有効化",
@ -194,6 +191,10 @@
"no": "いいえ",
"permanentValid": "有効期限なし",
"manageShares": "共有リンクの管理",
"manageDirectLinks": "直鏈の管理",
"deleteLinkConfirm": "この直鏈を削除しますか?",
"directLinkNotFound": "この直鏈は存在しません。",
"versionNotFound": "このバージョンは存在しません。",
"deleteVersionWarning": "このバージョンを削除してもよろしいですか?この操作は取り消せません。",
"setAsCurrent": "現在のバージョンに設定",
"current": "[現在のバージョン]",

View File

@ -641,7 +641,8 @@
"add_passkey": "パスキー追加",
"remove_passkey": "パスキー削除",
"redeem_gift_code": "ギフトコード交換",
"update_view": "ビュー設定変更"
"update_view": "ビュー設定変更",
"delete_direct_link": "直鏈削除"
},
"server": "サーバー設定",
"tempPath": "一時パス",
@ -1090,6 +1091,8 @@
"bathSourceLinkLimitDes": "ユーザーが一度に取得できる直リンクの最大ファイル数を指定します。0と入力すると、直リンクの取得は許可されません。",
"redirectedSource": "リダイレクトを使用する直リンク",
"redirectedSourceDes": "推奨設定です。有効にすると、ユーザーが取得するファイルの直リンクがCloudreveを経由するようになりリンクが短くなります。無効にするとユーザーが取得するファイルの直リンクはファイルの元のリンクになり、ファイルバージョンに紐づきます。一部のストレージポリシーでは、特定の設定下で取得された非経由直リンクが永久的に有効にならない場合があります。Cloudreveのドキュメントを参照してください。",
"reuseDirectLink": "既存の直リンクを再利用",
"reuseDirectLinkDes": "有効にすると、同じファイルの直リンクを複数回要求した場合、既存の中継直リンクが再利用されます。",
"downloadSpeedLimit": "ダウンロード速度制限",
"downloadSpeedLimitDes": "0と入力すると制限なしになります。制限を有効にすると、ユーザーが速度制限に対応したすべてのストレージポリシー下のファイルをダウンロードする際の最大速度が制限されます。",
"anonymousHint": "このユーザーグループは、ログインしていない匿名の訪問者に対応します。",

View File

@ -193,6 +193,10 @@
"no": "否",
"permanentValid": "永久有效",
"manageShares": "管理分享链接",
"manageDirectLinks": "管理直链",
"deleteLinkConfirm": "确定要删除此直链吗?",
"directLinkNotFound": "你查找的直链已经不存在。",
"versionNotFound": "你查找的版本已经不存在。",
"deleteVersionWarning": "确定要删除此版本吗?此操作无法撤销。",
"setAsCurrent": "设为当前版本",
"current": "[当前版本]",
@ -858,4 +862,4 @@
"used": "已使用 - {{size}}",
"total": "总容量 - {{size}}"
}
}
}

View File

@ -641,7 +641,8 @@
"add_passkey": "添加通行密钥",
"remove_passkey": "移除通行密钥",
"redeem_gift_code": "兑换礼品码",
"update_view": "更改视图设置"
"update_view": "更改视图设置",
"delete_direct_link": "删除直链"
},
"server": "服务器设置",
"tempPath": "临时路径",
@ -1090,6 +1091,8 @@
"bathSourceLinkLimitDes": "允许用户单次批量获取直链的最大文件数量,填写为 0 表示不允许获取直链。",
"redirectedSource": "使用重定向的直链",
"redirectedSourceDes": "推荐开启。开启后,用户获取的文件直链将由 Cloudreve 中转,链接较短。关闭后,用户获取的文件直链会变成文件的原始链接,且与文件版本绑定。部分存储策略在某些设置下获取的非中转直链无法保持永久有效,请参阅 Cloudreve 文档。",
"reuseDirectLink": "重用已有直链",
"reuseDirectLinkDes": "开启后,多次请求同一个文件的直链时,会重用已创建的中转直链。",
"downloadSpeedLimit": "下载限速",
"downloadSpeedLimitDes": "填写为 0 表示不限制。开启限制后,用户下载所有支持限速的存储策略下的文件时,下载最大速度会被限制。",
"anonymousHint": "此用户组对应着未登录的匿名访客。",

View File

@ -193,6 +193,10 @@
"no": "否",
"permanentValid": "永久有效",
"manageShares": "管理分享連結",
"manageDirectLinks": "管理直鏈",
"deleteLinkConfirm": "確定要刪除此直鏈嗎?",
"directLinkNotFound": "你查找的直鏈已經不存在。",
"versionNotFound": "你查找的版本已經不存在。",
"deleteVersionWarning": "確定要刪除此版本嗎?此操作無法撤銷。",
"setAsCurrent": "設為當前版本",
"current": "[當前版本]",
@ -854,4 +858,4 @@
"used": "已使用 - {{size}}",
"total": "總容量 - {{size}}"
}
}
}

View File

@ -638,7 +638,8 @@
"add_passkey": "新增通行密鑰",
"remove_passkey": "移除通行密鑰",
"redeem_gift_code": "兌換禮品碼",
"update_view": "更改檢視設定"
"update_view": "更改檢視設定",
"delete_direct_link": "刪除直鏈"
},
"server": "伺服器設定",
"tempPath": "臨時路徑",
@ -1086,7 +1087,9 @@
"bathSourceLinkLimit": "批量生成外直鏈量限制",
"bathSourceLinkLimitDes": "允許使用者單次批量獲取直鏈的最大檔案數量,填寫為 0 表示不允許獲取直鏈。",
"redirectedSource": "使用重定向的直鏈",
"redirectedSourceDes": "推荐開啟。開啟後,使用者獲取的檔案直鏈將由 Cloudreve 中轉,連結較短。關閉後,使用者獲取的檔案直鏈會變成檔案的原始連結,且與檔案版本綁定。部分儲存策略在某些設定下獲取的非中轉直鏈無法保持永久有效,請參閱 Cloudreve 檔案。",
"redirectedSourceDes": "推薦開啟。開啟後,使用者獲取的檔案直鏈將由 Cloudreve 中轉,連結較短。關閉後,使用者獲取的檔案直鏈會變成檔案的原始連結,且與檔案版本綁定。部分儲存策略在某些設定下獲取的非中轉直鏈無法保持永久有效,請參閱 Cloudreve 檔案。",
"reuseDirectLink": "重用已有直鏈",
"reuseDirectLinkDes": "開啟後,多次請求同一個檔案的直鏈時,會重用已創建的中轉直鏈。",
"downloadSpeedLimit": "下載限速",
"downloadSpeedLimitDes": "填寫為 0 表示不限制。開啟限制後,使用者下載所有支援限速的儲存策略下的檔案時,下載最大速度會被限制。",
"anonymousHint": "此使用者組對應著未登入的匿名訪客。",

View File

@ -1013,6 +1013,12 @@ export function getFileDirectLinks(req: MultipleUriService): ThunkResponse<Direc
};
}
export function sendDeleteDirectLink(id: string): ThunkResponse {
return async (dispatch, _getState) => {
return await dispatch(send(`/file/source/${id}`, { method: "DELETE" }, { ...defaultOpts }));
};
}
export function getUserShares(req: ListShareService, uid: string): ThunkResponse<ListShareResponse> {
return async (dispatch, _getState) => {
return await dispatch(

View File

@ -51,6 +51,14 @@ export interface ExtendedInfo {
shares?: Share[];
entities?: Entity[];
view?: ExplorerView;
direct_links?: DirectLink[];
}
export interface DirectLink {
id: string;
created_at: string;
url: string;
downloaded: number;
}
export interface Entity {
@ -386,6 +394,7 @@ export const AuditLogType = {
redeem_gift_code: 54,
file_imported: 55,
update_view: 56,
delete_direct_link: 57,
};
export interface MultipleUriService {

View File

@ -91,6 +91,7 @@ export const GroupPermission = {
remote_download: 9,
redirected_source: 11,
advance_delete: 12,
unique_direct_link: 17,
};
export interface UserSettings {

View File

@ -68,6 +68,16 @@ const UploadDownloadSection = () => {
[setGroup],
);
const onReuseDirectLinkChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setGroup((p: GroupEnt) => ({
...p,
permissions: new Boolset(p.permissions).set(GroupPermission.unique_direct_link, !e.target.checked).toString(),
}));
},
[setGroup],
);
return (
<SettingSection>
<Typography variant="h6" gutterBottom>
@ -131,6 +141,22 @@ const UploadDownloadSection = () => {
</FormControl>
</SettingForm>
</Stack>
<Collapse in={values?.settings?.redirected_source} unmountOnExit>
<SettingForm lgWidth={5}>
<FormControl fullWidth sx={{ mt: 3 }}>
<FormControlLabel
control={
<Switch
checked={!permission.enabled(GroupPermission.unique_direct_link)}
onChange={onReuseDirectLinkChange}
/>
}
label={t("group.reuseDirectLink")}
/>
<NoMarginHelperText>{t("group.reuseDirectLinkDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
</Collapse>
</Collapse>
<SettingForm lgWidth={5} title={t("group.downloadSpeedLimit")}>
<FormControl fullWidth>

View File

@ -63,6 +63,7 @@ export const eventCategories = {
AuditLogType.move_to_trash,
AuditLogType.update_metadata,
AuditLogType.get_direct_link,
AuditLogType.delete_direct_link,
AuditLogType.update_view,
],
},

View File

@ -1,18 +1,20 @@
import { ListItemIcon, ListItemText } from "@mui/material";
import { useCallback, useContext } from "react";
import { CascadingContext, CascadingMenuItem } from "./CascadingMenu.tsx";
import { useTranslation } from "react-i18next";
import { useAppDispatch } from "../../../redux/hooks.ts";
import { closeContextMenu } from "../../../redux/fileManagerSlice.ts";
import {
setCreateArchiveDialog,
setDirectLinkManagementDialog,
setManageShareDialog,
setVersionControlDialog,
} from "../../../redux/globalStateSlice.ts";
import { ListItemIcon, ListItemText } from "@mui/material";
import { useAppDispatch } from "../../../redux/hooks.ts";
import Archive from "../../Icons/Archive.tsx";
import BranchForkLink from "../../Icons/BranchForkLink.tsx";
import HistoryOutlined from "../../Icons/HistoryOutlined.tsx";
import LinkSetting from "../../Icons/LinkSetting.tsx";
import { CascadingContext, CascadingMenuItem } from "./CascadingMenu.tsx";
import { SubMenuItemsProps } from "./OrganizeMenuItems.tsx";
import Archive from "../../Icons/Archive.tsx";
const MoreMenuItems = ({ displayOpt, targets }: SubMenuItemsProps) => {
const { rootPopupState } = useContext(CascadingContext);
@ -64,11 +66,28 @@ const MoreMenuItems = ({ displayOpt, targets }: SubMenuItemsProps) => {
)}
>
<ListItemIcon>
<LinkSetting fontSize="small" />
<BranchForkLink fontSize="small" />
</ListItemIcon>
<ListItemText>{t("application:fileManager.manageShares")}</ListItemText>
</CascadingMenuItem>
)}
{displayOpt.showDirectLinkManagement && (
<CascadingMenuItem
onClick={onClick(() =>
dispatch(
setDirectLinkManagementDialog({
open: true,
file: targets[0],
}),
),
)}
>
<ListItemIcon>
<LinkSetting fontSize="small" />
</ListItemIcon>
<ListItemText>{t("application:fileManager.manageDirectLinks")}</ListItemText>
</CascadingMenuItem>
)}
{displayOpt.showCreateArchive && (
<CascadingMenuItem
onClick={onClick(() =>

View File

@ -73,6 +73,7 @@ export interface DisplayOption {
showMore?: boolean;
showVersionControl?: boolean;
showDirectLinkManagement?: boolean;
showManageShares?: boolean;
showCreateArchive?: boolean;
@ -229,6 +230,7 @@ export const getActionOpt = (
display.orCapability &&
(currentUserAnonymous?.group?.direct_link_batch_size ?? 0) >= targets.length &&
display.orCapability.enabled(NavigatorCapability.download_file);
display.showDirectLinkManagement = display.showDirectLink && targets.length == 1 && display.hasFile;
display.showOpen =
targets.length == 1 &&
display.hasFile &&
@ -287,7 +289,11 @@ export const getActionOpt = (
display.orCapability &&
display.orCapability.enabled(NavigatorCapability.download_file);
display.showMore = display.showVersionControl || display.showManageShares || display.showCreateArchive;
display.showMore =
display.showVersionControl ||
display.showManageShares ||
display.showCreateArchive ||
display.showDirectLinkManagement;
return display;
};

View File

@ -30,6 +30,7 @@ import AdvanceSearch from "../Search/AdvanceSearch/AdvanceSearch.tsx";
import React from "react";
import ColumnSetting from "../Explorer/ListView/ColumnSetting.tsx";
import DirectLinks from "./DirectLinks.tsx";
import DirectLinksControl from "./DirectLinksControl.tsx";
const Dialogs = () => {
const showCreateArchive = useAppSelector((state) => state.globalState.createArchiveDialogOpen);
@ -39,6 +40,7 @@ const Dialogs = () => {
const showListViewColumnSetting = useAppSelector((state) => state.globalState.listViewColumnSettingDialogOpen);
const directLink = useAppSelector((state) => state.globalState.directLinkDialogOpen);
const excalidrawViewer = useAppSelector((state) => state.globalState.excalidrawViewer);
const directLinkManagement = useAppSelector((state) => state.globalState.directLinkManagementDialogOpen);
return (
<>
@ -72,6 +74,7 @@ const Dialogs = () => {
{showListViewColumnSetting != undefined && <ColumnSetting />}
{directLink != undefined && <DirectLinks />}
{excalidrawViewer != undefined && <ExcalidrawViewer />}
{directLinkManagement != undefined && <DirectLinksControl />}
</>
);
};

View File

@ -0,0 +1,227 @@
import {
Alert,
Box,
DialogContent,
IconButton,
Link,
Skeleton,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
} from "@mui/material";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { getFileInfo, sendDeleteDirectLink } from "../../../api/api.ts";
import { DirectLink, FileResponse } from "../../../api/explorer.ts";
import { closeDirectLinkManagementDialog } from "../../../redux/globalStateSlice.ts";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
import { confirmOperation } from "../../../redux/thunks/dialog.ts";
import { copyToClipboard } from "../../../util/index.ts";
import AutoHeight from "../../Common/AutoHeight.tsx";
import { NoWrapTableCell, StyledTableContainerPaper } from "../../Common/StyledComponents.tsx";
import TimeBadge from "../../Common/TimeBadge.tsx";
import DraggableDialog from "../../Dialogs/DraggableDialog.tsx";
import CopyOutlined from "../../Icons/CopyOutlined.tsx";
import DeleteOutlined from "../../Icons/DeleteOutlined.tsx";
const DirectLinksControl = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const [fileExtended, setFileExtended] = useState<FileResponse | undefined>(undefined);
const [loading, setLoading] = useState(false);
const open = useAppSelector((state) => state.globalState.directLinkManagementDialogOpen);
const target = useAppSelector((state) => state.globalState.directLinkManagementDialogFile);
const highlight = useAppSelector((state) => state.globalState.directLinkHighlight);
const hilightButNotFound = useMemo(() => {
return (
highlight &&
fileExtended?.extended_info &&
!fileExtended?.extended_info?.direct_links?.some((link) => link.id == highlight)
);
}, [highlight, fileExtended?.extended_info?.direct_links]);
const onClose = useCallback(() => {
if (!loading) {
dispatch(closeDirectLinkManagementDialog());
}
}, [dispatch, loading]);
useEffect(() => {
if (target && open) {
if (target.extended_info) {
setFileExtended(target);
} else {
setFileExtended(undefined);
dispatch(
getFileInfo({
uri: target.path,
extended: true,
}),
).then((res) => setFileExtended(res));
}
}
}, [target, open]);
const directLinks = useMemo(() => {
return fileExtended?.extended_info?.direct_links;
}, [fileExtended?.extended_info?.direct_links]);
const handleRowClick = useCallback((directLink: DirectLink) => {
window.open(directLink.url, "_blank");
}, []);
const copyURL = useCallback((actionTarget: DirectLink) => {
if (!actionTarget) {
return;
}
copyToClipboard(actionTarget.url);
}, []);
const deleteDirectLink = useCallback(
(actionTarget: DirectLink) => {
if (!target || !actionTarget) {
return;
}
dispatch(confirmOperation(t("fileManager.deleteLinkConfirm"))).then(() => {
setLoading(true);
dispatch(sendDeleteDirectLink(actionTarget.id))
.then(() => {
setFileExtended((prev) =>
prev
? {
...prev,
extended_info: prev.extended_info
? {
...prev.extended_info,
direct_links: prev.extended_info.direct_links?.filter((link) => link.id !== actionTarget.id),
}
: undefined,
}
: undefined,
);
})
.finally(() => {
setLoading(false);
});
});
},
[t, target, dispatch],
);
return (
<DraggableDialog
title={t("application:fileManager.manageDirectLinks")}
loading={loading}
dialogProps={{
open: open ?? false,
onClose: onClose,
fullWidth: true,
maxWidth: "md",
}}
>
<DialogContent>
<AutoHeight>
{hilightButNotFound && (
<Alert severity="warning" sx={{ mb: 2 }}>
{t("application:fileManager.directLinkNotFound")}
</Alert>
)}
<TableContainer component={StyledTableContainerPaper}>
<Table sx={{ width: "100%" }} size="small">
<TableHead>
<TableRow>
<NoWrapTableCell>{t("fileManager.actions")}</NoWrapTableCell>
<TableCell>{t("modals.sourceLink")}</TableCell>
<NoWrapTableCell>{t("setting.viewNumber")}</NoWrapTableCell>
<NoWrapTableCell>{t("fileManager.createdAt")}</NoWrapTableCell>
</TableRow>
</TableHead>
<TableBody>
{!fileExtended && (
<TableRow
hover
sx={{
"&:last-child td, &:last-child th": { border: 0 },
}}
>
<NoWrapTableCell component="th" scope="row">
<Skeleton variant={"text"} width={100} />
</NoWrapTableCell>
<TableCell>
<Skeleton variant={"text"} width={200} />
</TableCell>
<NoWrapTableCell>
<Skeleton variant={"text"} width={60} />
</NoWrapTableCell>
<NoWrapTableCell>
<Skeleton variant={"text"} width={100} />
</NoWrapTableCell>
</TableRow>
)}
{directLinks &&
directLinks.map((link) => (
<TableRow
key={link.id}
hover
selected={highlight == link.id}
sx={{
boxShadow: (theme) =>
highlight == link.id ? `inset 0 0 0 2px ${theme.palette.primary.light}` : "none",
"&:last-child td, &:last-child th": { border: 0 },
}}
>
<NoWrapTableCell component="th" scope="row">
<IconButton onClick={() => copyURL(link)} size={"small"}>
<CopyOutlined fontSize={"small"} />
</IconButton>
<IconButton disabled={loading} onClick={() => deleteDirectLink(link)} size={"small"}>
<DeleteOutlined fontSize={"small"} />
</IconButton>
</NoWrapTableCell>
<TableCell
sx={{
maxWidth: 300,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
onClick={(event) => event.stopPropagation()}
>
<Typography variant="body2" sx={{ cursor: "text" }}>
<Link href={link.url} target="_blank" underline="hover">
{link.url}
</Link>
</Typography>
</TableCell>
<NoWrapTableCell>{link.downloaded}</NoWrapTableCell>
<NoWrapTableCell>
<TimeBadge variant={"body2"} datetime={link.created_at} />
</NoWrapTableCell>
</TableRow>
))}
</TableBody>
</Table>
{!directLinks && fileExtended && (
<Box sx={{ p: 1, width: "100%", textAlign: "center" }}>
<Typography variant={"caption"} color={"text.secondary"}>
{t("application:setting.listEmpty")}
</Typography>
</Box>
)}
</TableContainer>
</AutoHeight>
</DialogContent>
</DraggableDialog>
);
};
export default DirectLinksControl;

View File

@ -1,5 +1,5 @@
import { useTranslation } from "react-i18next";
import {
Alert,
Box,
DialogContent,
IconButton,
@ -14,25 +14,26 @@ import {
TableRow,
Typography,
} from "@mui/material";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import DraggableDialog from "../../Dialogs/DraggableDialog.tsx";
import { useTranslation } from "react-i18next";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
import { confirmOperation } from "../../../redux/thunks/dialog.ts";
import { downloadSingleFile } from "../../../redux/thunks/download.ts";
import { setFileVersion } from "../../../redux/thunks/file.ts";
import { openViewers } from "../../../redux/thunks/viewer.ts";
import { sizeToString } from "../../../util";
import AutoHeight from "../../Common/AutoHeight.tsx";
import { closeVersionControlDialog } from "../../../redux/globalStateSlice.ts";
import { Entity, EntityType, FileResponse } from "../../../api/explorer.ts";
import { deleteVersion, getFileInfo } from "../../../api/api.ts";
import { NoWrapTableCell, StyledTableContainerPaper } from "../../Common/StyledComponents.tsx";
import TimeBadge from "../../Common/TimeBadge.tsx";
import { sizeToString } from "../../../util";
import { AnonymousUser } from "../../Common/User/UserAvatar.tsx";
import UserBadge from "../../Common/User/UserBadge.tsx";
import DraggableDialog from "../../Dialogs/DraggableDialog.tsx";
import MoreVertical from "../../Icons/MoreVertical.tsx";
import { SquareMenuItem } from "../ContextMenu/ContextMenu.tsx";
import { downloadSingleFile } from "../../../redux/thunks/download.ts";
import AutoHeight from "../../Common/AutoHeight.tsx";
import { confirmOperation } from "../../../redux/thunks/dialog.ts";
import { openViewers } from "../../../redux/thunks/viewer.ts";
import { FileManagerIndex } from "../FileManager.tsx";
import { setFileVersion } from "../../../redux/thunks/file.ts";
import { AnonymousUser } from "../../Common/User/UserAvatar.tsx";
const VersionControl = () => {
const { t } = useTranslation();
@ -73,6 +74,10 @@ const VersionControl = () => {
return fileExtended?.extended_info?.entities?.filter((e) => e.type == EntityType.version);
}, [fileExtended?.extended_info?.entities]);
const hilightButNotFound = useMemo(() => {
return highlight && fileExtended?.extended_info && !versionEntities?.some((e) => e.id == highlight);
}, [highlight, fileExtended?.extended_info?.entities]);
const handleActionClose = () => {
setAnchorEl(null);
};
@ -201,6 +206,11 @@ const VersionControl = () => {
>
<DialogContent>
<AutoHeight>
{hilightButNotFound && (
<Alert severity="warning" sx={{ mb: 2 }}>
{t("application:fileManager.versionNotFound")}
</Alert>
)}
<TableContainer component={StyledTableContainerPaper}>
<Table sx={{ width: "100%" }} size="small">
<TableHead>

View File

@ -0,0 +1,9 @@
import { SvgIcon, SvgIconProps } from "@mui/material";
export default function BranchForkLink(props: SvgIconProps) {
return (
<SvgIcon {...props}>
<path d="M6 8.965a3.5 3.5 0 1 0-1.5-.11v6.29A3.502 3.502 0 0 0 5.5 22a3.5 3.5 0 0 0 .5-6.965V13h6.25A3.75 3.75 0 0 0 16 9.25v-.285a3.5 3.5 0 1 0-1.5-.11v.395a2.25 2.25 0 0 1-2.25 2.25H6zM7.5 5.5a2 2 0 1 1-4 0a2 2 0 0 1 4 0m10 0a2 2 0 1 1-4 0a2 2 0 0 1 4 0m-12 11a2 2 0 1 1 0 4a2 2 0 0 1 0-4M23 18.75A3.75 3.75 0 0 0 19.25 15l-.102.007a.75.75 0 0 0 .102 1.493l.154.005A2.25 2.25 0 0 1 19.25 21l-.003.005l-.102.007a.75.75 0 0 0 .108 1.493V22.5l.2-.005A3.75 3.75 0 0 0 23 18.75m-6.5-3a.75.75 0 0 0-.75-.75l-.2.005a3.75 3.75 0 0 0 .2 7.495l.102-.007A.75.75 0 0 0 15.75 21l-.154-.005a2.25 2.25 0 0 1 .154-4.495l.102-.007a.75.75 0 0 0 .648-.743m3.5 3a.75.75 0 0 0-.75-.75h-3.5l-.102.007a.75.75 0 0 0 .102 1.493h3.5l.102-.007A.75.75 0 0 0 20 18.75" />
</SvgIcon>
);
}

View File

@ -190,6 +190,11 @@ export interface GlobalStateSlice {
directLinkDialogOpen?: boolean;
directLinkRes?: DirectLink[];
// Direct Link management dialog
directLinkManagementDialogOpen?: boolean;
directLinkManagementDialogFile?: FileResponse;
directLinkHighlight?: string;
// DnD
dndState: DndState;
@ -262,6 +267,19 @@ export const globalStateSlice = createSlice({
name: "globalState",
initialState,
reducers: {
setDirectLinkManagementDialog: (
state,
action: PayloadAction<{ open: boolean; file?: FileResponse; highlight?: string }>,
) => {
state.directLinkManagementDialogOpen = action.payload.open;
state.directLinkManagementDialogFile = action.payload.file;
state.directLinkHighlight = action.payload.highlight;
},
closeDirectLinkManagementDialog: (state) => {
state.directLinkManagementDialogOpen = false;
state.directLinkManagementDialogFile = undefined;
state.directLinkHighlight = undefined;
},
setMobileDrawerOpen: (state, action: PayloadAction<boolean>) => {
state.mobileDrawerOpen = action.payload;
},
@ -795,4 +813,6 @@ export const {
setSearchPopup,
setExcalidrawViewer,
closeExcalidrawViewer,
setDirectLinkManagementDialog,
closeDirectLinkManagementDialog,
} = globalStateSlice.actions;