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)}
+ >
+
+
+
+
+
+
{
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 {