diff --git a/frontend/public/locales/en-US/dashboard.json b/frontend/public/locales/en-US/dashboard.json index 1e740dd4..07eca696 100644 --- a/frontend/public/locales/en-US/dashboard.json +++ b/frontend/public/locales/en-US/dashboard.json @@ -1496,7 +1496,10 @@ "otherConditions": "Other conditions", "shareLinkExisted": "Has share link", "directLinkExisted": "Has direct link", - "isUploading": "Is uploading" + "isUploading": "Is uploading", + "deletedState": "Deleted state", + "liveOnly": "Live files only", + "trashOnly": "Trash only" }, "entity": { "refenenceCount": "Reference count", diff --git a/frontend/public/locales/zh-CN/dashboard.json b/frontend/public/locales/zh-CN/dashboard.json index c8a1bf2a..45e78430 100644 --- a/frontend/public/locales/zh-CN/dashboard.json +++ b/frontend/public/locales/zh-CN/dashboard.json @@ -1496,7 +1496,10 @@ "otherConditions": "其他条件", "shareLinkExisted": "存在分享链接", "directLinkExisted": "存在中转直链", - "isUploading": "上传中" + "isUploading": "上传中", + "deletedState": "删除状态", + "liveOnly": "仅未删除", + "trashOnly": "仅回收站" }, "entity": { "refenenceCount": "引用次数", diff --git a/frontend/src/component/Admin/File/FileFilterPopover.tsx b/frontend/src/component/Admin/File/FileFilterPopover.tsx index f932bbad..d4948720 100644 --- a/frontend/src/component/Admin/File/FileFilterPopover.tsx +++ b/frontend/src/component/Admin/File/FileFilterPopover.tsx @@ -1,7 +1,7 @@ -import { Box, Button, Checkbox, Popover, PopoverProps, Stack, styled } from "@mui/material"; +import { Box, Button, Checkbox, MenuItem, Popover, PopoverProps, Stack, styled } from "@mui/material"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { DenseFilledTextField, SmallFormControlLabel } from "../../Common/StyledComponents"; +import { DenseFilledTextField, DenseSelect, SmallFormControlLabel } from "../../Common/StyledComponents"; import SettingForm from "../../Pages/Setting/SettingForm"; import SinglePolicySelectionInput from "../Common/SinglePolicySelectionInput"; @@ -18,6 +18,8 @@ export interface FileFilterPopoverProps extends PopoverProps { setHasDirectLink: (hasDirectLink: boolean) => void; isUploading: boolean; setIsUploading: (isUploading: boolean) => void; + deleted: string; + setDeleted: (deleted: string) => void; clearFilters: () => void; } @@ -40,6 +42,8 @@ const FileFilterPopover = ({ setHasDirectLink, isUploading, setIsUploading, + deleted, + setDeleted, clearFilters, onClose, open, @@ -54,6 +58,7 @@ const FileFilterPopover = ({ const [localHasShareLink, setLocalHasShareLink] = useState(hasShareLink); const [localHasDirectLink, setLocalHasDirectLink] = useState(hasDirectLink); const [localIsUploading, setLocalIsUploading] = useState(isUploading); + const [localDeleted, setLocalDeleted] = useState(deleted); // Initialize local state when popup opens useEffect(() => { @@ -64,6 +69,7 @@ const FileFilterPopover = ({ setLocalHasShareLink(hasShareLink); setLocalHasDirectLink(hasDirectLink); setLocalIsUploading(isUploading); + setLocalDeleted(deleted); } }, [open]); @@ -75,6 +81,7 @@ const FileFilterPopover = ({ setHasShareLink(localHasShareLink); setHasDirectLink(localHasDirectLink); setIsUploading(localIsUploading); + setDeleted(localDeleted); onClose?.({}, "backdropClick"); }; @@ -86,6 +93,7 @@ const FileFilterPopover = ({ setLocalHasShareLink(false); setLocalHasDirectLink(false); setLocalIsUploading(false); + setLocalDeleted(""); clearFilters(); onClose?.({}, "backdropClick"); }; @@ -143,6 +151,19 @@ const FileFilterPopover = ({ /> + + setLocalDeleted(e.target.value as string)} + > + {t("user.all")} + {t("file.liveOnly")} + {t("file.trashOnly")} + + + { const { t } = useTranslation("dashboard"); @@ -68,6 +69,7 @@ const FileSetting = () => { const [hasDirectLink, setHasDirectLink] = useQueryState(HasDirectLinkQuery, { defaultValue: "" }); const [shared, setShared] = useQueryState(SharedQuery, { defaultValue: "" }); const [uploading, setUploading] = useQueryState(UploadingQuery, { defaultValue: "" }); + const [deleted, setDeleted] = useQueryState(DeletedQuery, { defaultValue: "" }); const [count, setCount] = useState(0); const [selected, setSelected] = useState([]); const [createNewOpen, setCreateNewOpen] = useState(false); @@ -93,11 +95,12 @@ const FileSetting = () => { setHasDirectLink(""); setShared(""); setUploading(""); - }, [setStoragePolicy, setOwner, setName, setHasDirectLink, setShared, setUploading]); + setDeleted(""); + }, [setStoragePolicy, setOwner, setName, setHasDirectLink, setShared, setUploading, setDeleted]); useEffect(() => { fetchFiles(); - }, [page, pageSize, orderBy, orderDirection, storagePolicy, owner, name, hasDirectLink, shared, uploading]); + }, [page, pageSize, orderBy, orderDirection, storagePolicy, owner, name, hasDirectLink, shared, uploading, deleted]); const fetchFiles = () => { setLoading(true); @@ -115,6 +118,7 @@ const FileSetting = () => { file_direct_link: hasDirectLink === "true" ? "true" : "", file_shared: shared === "true" ? "true" : "", file_metadata: uploading === "true" ? Metadata.upload_session_id : "", + file_deleted: deleted, }, }), ) @@ -183,8 +187,8 @@ const FileSetting = () => { }; const hasActiveFilters = useMemo(() => { - return !!(storagePolicy || owner || name || hasDirectLink || shared || uploading); - }, [storagePolicy, owner, name, hasDirectLink, shared, uploading]); + return !!(storagePolicy || owner || name || hasDirectLink || shared || uploading || deleted); + }, [storagePolicy, owner, name, hasDirectLink, shared, uploading, deleted]); const handleFileDialogOpen = (id: number) => { setFileDialogID(id); @@ -239,6 +243,8 @@ const FileSetting = () => { setHasShareLink={(value: boolean) => setShared(value ? "true" : "")} isUploading={uploading === "true"} setIsUploading={(value: boolean) => setUploading(value ? "true" : "")} + deleted={deleted} + setDeleted={setDeleted} /> }> diff --git a/inventory/file.go b/inventory/file.go index de559b40..02097a1a 100644 --- a/inventory/file.go +++ b/inventory/file.go @@ -62,6 +62,9 @@ type ( HasMetadata string Shared bool HasDirectLink bool + // Deleted filters by soft-delete state: nil lists everything (current + // behavior), true lists only trash, false only live files. + Deleted *bool } MetadataFilter struct { @@ -1200,6 +1203,15 @@ func (f *fileClient) FlattenListFiles(ctx context.Context, args *FlattenListFile query = query.Where(file.HasDirectLinksWith(directlink.DeletedAtIsNil())) } + if args.Deleted != nil { + if *args.Deleted { + // Trash = files detached from their parent by soft-delete. + query = query.Where(file.Not(file.HasParent())) + } else { + query = query.Where(file.HasParent()) + } + } + query.Order(getFileOrderOption(&ListFileParameters{ PaginationArgs: args.PaginationArgs, })...) diff --git a/service/admin/file.go b/service/admin/file.go index ffb26bcf..192c0d87 100644 --- a/service/admin/file.go +++ b/service/admin/file.go @@ -157,6 +157,7 @@ const ( fileMetadataCondition = "file_metadata" fileSharedCondition = "file_shared" fileDirectLinkCondition = "file_direct_link" + fileDeletedCondition = "file_deleted" ) func (service *AdminListService) Files(c *gin.Context) (*ListFileResponse, error) { @@ -177,6 +178,7 @@ func (service *AdminListService) Files(c *gin.Context) (*ListFileResponse, error metadata string shared bool directLink bool + deleted *bool ) if service.Conditions[fileUserCondition] != "" { @@ -205,6 +207,13 @@ func (service *AdminListService) Files(c *gin.Context) (*ListFileResponse, error directLink = true } + switch service.Conditions[fileDeletedCondition] { + case "true": + deleted = lo.ToPtr(true) + case "false": + deleted = lo.ToPtr(false) + } + res, err := fileClient.FlattenListFiles(ctx, &inventory.FlattenListFileParameters{ PaginationArgs: &inventory.PaginationArgs{ Page: service.Page - 1, @@ -218,6 +227,7 @@ func (service *AdminListService) Files(c *gin.Context) (*ListFileResponse, error HasMetadata: metadata, Shared: shared, HasDirectLink: directLink, + Deleted: deleted, }) if err != nil {