diff --git a/public/locales/en-US/application.json b/public/locales/en-US/application.json index 6e81c1c..eb7c97b 100644 --- a/public/locales/en-US/application.json +++ b/public/locales/en-US/application.json @@ -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}}" } -} \ No newline at end of file +} diff --git a/public/locales/en-US/dashboard.json b/public/locales/en-US/dashboard.json index e5103be..3142650 100644 --- a/public/locales/en-US/dashboard.json +++ b/public/locales/en-US/dashboard.json @@ -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.", diff --git a/public/locales/ja-JP/application.json b/public/locales/ja-JP/application.json index 4fa4848..7b588ff 100644 --- a/public/locales/ja-JP/application.json +++ b/public/locales/ja-JP/application.json @@ -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": "[現在のバージョン]", diff --git a/public/locales/ja-JP/dashboard.json b/public/locales/ja-JP/dashboard.json index 6664f46..617679c 100644 --- a/public/locales/ja-JP/dashboard.json +++ b/public/locales/ja-JP/dashboard.json @@ -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": "このユーザーグループは、ログインしていない匿名の訪問者に対応します。", diff --git a/public/locales/zh-CN/application.json b/public/locales/zh-CN/application.json index 7fcae11..1b79938 100644 --- a/public/locales/zh-CN/application.json +++ b/public/locales/zh-CN/application.json @@ -193,6 +193,10 @@ "no": "否", "permanentValid": "永久有效", "manageShares": "管理分享链接", + "manageDirectLinks": "管理直链", + "deleteLinkConfirm": "确定要删除此直链吗?", + "directLinkNotFound": "你查找的直链已经不存在。", + "versionNotFound": "你查找的版本已经不存在。", "deleteVersionWarning": "确定要删除此版本吗?此操作无法撤销。", "setAsCurrent": "设为当前版本", "current": "[当前版本]", @@ -858,4 +862,4 @@ "used": "已使用 - {{size}}", "total": "总容量 - {{size}}" } -} \ No newline at end of file +} diff --git a/public/locales/zh-CN/dashboard.json b/public/locales/zh-CN/dashboard.json index 05440e7..37f3255 100644 --- a/public/locales/zh-CN/dashboard.json +++ b/public/locales/zh-CN/dashboard.json @@ -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": "此用户组对应着未登录的匿名访客。", diff --git a/public/locales/zh-TW/application.json b/public/locales/zh-TW/application.json index 9b66265..fbe414b 100644 --- a/public/locales/zh-TW/application.json +++ b/public/locales/zh-TW/application.json @@ -193,6 +193,10 @@ "no": "否", "permanentValid": "永久有效", "manageShares": "管理分享連結", + "manageDirectLinks": "管理直鏈", + "deleteLinkConfirm": "確定要刪除此直鏈嗎?", + "directLinkNotFound": "你查找的直鏈已經不存在。", + "versionNotFound": "你查找的版本已經不存在。", "deleteVersionWarning": "確定要刪除此版本嗎?此操作無法撤銷。", "setAsCurrent": "設為當前版本", "current": "[當前版本]", @@ -854,4 +858,4 @@ "used": "已使用 - {{size}}", "total": "總容量 - {{size}}" } -} \ No newline at end of file +} diff --git a/public/locales/zh-TW/dashboard.json b/public/locales/zh-TW/dashboard.json index c2265b6..3805696 100644 --- a/public/locales/zh-TW/dashboard.json +++ b/public/locales/zh-TW/dashboard.json @@ -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": "此使用者組對應著未登入的匿名訪客。", diff --git a/src/api/api.ts b/src/api/api.ts index 0fd5974..32deb9c 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -1013,6 +1013,12 @@ export function getFileDirectLinks(req: MultipleUriService): ThunkResponse { + return await dispatch(send(`/file/source/${id}`, { method: "DELETE" }, { ...defaultOpts })); + }; +} + export function getUserShares(req: ListShareService, uid: string): ThunkResponse { return async (dispatch, _getState) => { return await dispatch( diff --git a/src/api/explorer.ts b/src/api/explorer.ts index 137a845..71429d0 100644 --- a/src/api/explorer.ts +++ b/src/api/explorer.ts @@ -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 { diff --git a/src/api/user.ts b/src/api/user.ts index eb3f85b..faa1c2b 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -91,6 +91,7 @@ export const GroupPermission = { remote_download: 9, redirected_source: 11, advance_delete: 12, + unique_direct_link: 17, }; export interface UserSettings { diff --git a/src/component/Admin/Group/EditGroup/UploadDownloadSection.tsx b/src/component/Admin/Group/EditGroup/UploadDownloadSection.tsx index 9a58974..d7016e1 100644 --- a/src/component/Admin/Group/EditGroup/UploadDownloadSection.tsx +++ b/src/component/Admin/Group/EditGroup/UploadDownloadSection.tsx @@ -68,6 +68,16 @@ const UploadDownloadSection = () => { [setGroup], ); + const onReuseDirectLinkChange = useCallback( + (e: React.ChangeEvent) => { + setGroup((p: GroupEnt) => ({ + ...p, + permissions: new Boolset(p.permissions).set(GroupPermission.unique_direct_link, !e.target.checked).toString(), + })); + }, + [setGroup], + ); + return ( @@ -131,6 +141,22 @@ const UploadDownloadSection = () => { + + + + + } + label={t("group.reuseDirectLink")} + /> + {t("group.reuseDirectLinkDes")} + + + diff --git a/src/component/Admin/Settings/Event/Events.tsx b/src/component/Admin/Settings/Event/Events.tsx index 965c439..ccdafc8 100644 --- a/src/component/Admin/Settings/Event/Events.tsx +++ b/src/component/Admin/Settings/Event/Events.tsx @@ -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, ], }, diff --git a/src/component/FileManager/ContextMenu/MoreMenuItems.tsx b/src/component/FileManager/ContextMenu/MoreMenuItems.tsx index 8609ca0..448ed8d 100644 --- a/src/component/FileManager/ContextMenu/MoreMenuItems.tsx +++ b/src/component/FileManager/ContextMenu/MoreMenuItems.tsx @@ -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) => { )} > - + {t("application:fileManager.manageShares")} )} + {displayOpt.showDirectLinkManagement && ( + + dispatch( + setDirectLinkManagementDialog({ + open: true, + file: targets[0], + }), + ), + )} + > + + + + {t("application:fileManager.manageDirectLinks")} + + )} {displayOpt.showCreateArchive && ( diff --git a/src/component/FileManager/ContextMenu/useActionDisplayOpt.ts b/src/component/FileManager/ContextMenu/useActionDisplayOpt.ts index 7f332cc..fa39b3d 100644 --- a/src/component/FileManager/ContextMenu/useActionDisplayOpt.ts +++ b/src/component/FileManager/ContextMenu/useActionDisplayOpt.ts @@ -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; }; diff --git a/src/component/FileManager/Dialogs/Dialogs.tsx b/src/component/FileManager/Dialogs/Dialogs.tsx index e7a871a..d7e42c0 100644 --- a/src/component/FileManager/Dialogs/Dialogs.tsx +++ b/src/component/FileManager/Dialogs/Dialogs.tsx @@ -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 && } {directLink != undefined && } {excalidrawViewer != undefined && } + {directLinkManagement != undefined && } ); }; diff --git a/src/component/FileManager/Dialogs/DirectLinksControl.tsx b/src/component/FileManager/Dialogs/DirectLinksControl.tsx new file mode 100644 index 0000000..19c2fa4 --- /dev/null +++ b/src/component/FileManager/Dialogs/DirectLinksControl.tsx @@ -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(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 ( + + + + {hilightButNotFound && ( + + {t("application:fileManager.directLinkNotFound")} + + )} + + + + + {t("fileManager.actions")} + {t("modals.sourceLink")} + {t("setting.viewNumber")} + {t("fileManager.createdAt")} + + + + {!fileExtended && ( + + + + + + + + + + + + + + + )} + {directLinks && + directLinks.map((link) => ( + + highlight == link.id ? `inset 0 0 0 2px ${theme.palette.primary.light}` : "none", + "&:last-child td, &:last-child th": { border: 0 }, + }} + > + + copyURL(link)} size={"small"}> + + + deleteDirectLink(link)} size={"small"}> + + + + event.stopPropagation()} + > + + + {link.url} + + + + {link.downloaded} + + + + + ))} + +
+ {!directLinks && fileExtended && ( + + + {t("application:setting.listEmpty")} + + + )} +
+
+
+
+ ); +}; + +export default DirectLinksControl; diff --git a/src/component/FileManager/Dialogs/VersionControl.tsx b/src/component/FileManager/Dialogs/VersionControl.tsx index 0134a2d..a00823d 100644 --- a/src/component/FileManager/Dialogs/VersionControl.tsx +++ b/src/component/FileManager/Dialogs/VersionControl.tsx @@ -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 = () => { > + {hilightButNotFound && ( + + {t("application:fileManager.versionNotFound")} + + )} diff --git a/src/component/Icons/BranchForkLink.tsx b/src/component/Icons/BranchForkLink.tsx new file mode 100644 index 0000000..1a744ea --- /dev/null +++ b/src/component/Icons/BranchForkLink.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function BranchForkLink(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/redux/globalStateSlice.ts b/src/redux/globalStateSlice.ts index f746182..fc92ea6 100644 --- a/src/redux/globalStateSlice.ts +++ b/src/redux/globalStateSlice.ts @@ -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) => { state.mobileDrawerOpen = action.payload; }, @@ -795,4 +813,6 @@ export const { setSearchPopup, setExcalidrawViewer, closeExcalidrawViewer, + setDirectLinkManagementDialog, + closeDirectLinkManagementDialog, } = globalStateSlice.actions;