feat(files): add a name filter to My Files with a no-results state
Reconnects the existing (orphaned) name-filter pipeline with an input next to the sort controls. When a filter returns nothing, the page shows a dedicated 'no matching files' empty state instead of the generic per-tab message (e.g. 'Nothing modified yet').
This commit is contained in:
@@ -13,6 +13,7 @@ import DriveFileRenameOutlineIcon from "@mui/icons-material/DriveFileRenameOutli
|
||||
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
|
||||
import UploadFileIcon from "@mui/icons-material/UploadFile";
|
||||
import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
|
||||
import { FileId } from "@app/types/file";
|
||||
import { FolderId, FolderRecord, ROOT_FOLDER_ID } from "@app/types/folder";
|
||||
@@ -85,6 +86,8 @@ interface FileGridProps {
|
||||
onChangeSortMode?: (mode: FilesPageSortMode) => void;
|
||||
/** Drives the empty-state copy. */
|
||||
currentTab?: "all" | "local" | "cloud" | "recent" | "shared" | "sharedByMe";
|
||||
/** A filter is applied; an empty result then means "no matches", not "no files". */
|
||||
searchActive?: boolean;
|
||||
/** Cloud reachability; switches the cloud empty-state copy. */
|
||||
serverReachable?: boolean;
|
||||
/** Empty-state CTA handlers; if absent the matching button hides. */
|
||||
@@ -100,6 +103,7 @@ export function FileGrid(props: FileGridProps & { loading?: boolean }) {
|
||||
entries,
|
||||
loading,
|
||||
currentTab,
|
||||
searchActive,
|
||||
serverReachable,
|
||||
onEmptyUpload,
|
||||
onEmptyCreateFolder,
|
||||
@@ -114,6 +118,7 @@ export function FileGrid(props: FileGridProps & { loading?: boolean }) {
|
||||
return (
|
||||
<EmptyState
|
||||
tab={currentTab}
|
||||
searchActive={searchActive}
|
||||
serverReachable={serverReachable}
|
||||
onUpload={onEmptyUpload}
|
||||
onCreateFolder={onEmptyCreateFolder}
|
||||
@@ -183,6 +188,8 @@ function SkeletonGrid({ viewMode }: { viewMode: FilesPageViewMode }) {
|
||||
interface EmptyStateProps {
|
||||
/** Drives copy + iconography. */
|
||||
tab?: "all" | "local" | "cloud" | "recent" | "shared" | "sharedByMe";
|
||||
/** When true the empty list is the result of a filter, not a bare folder. */
|
||||
searchActive?: boolean;
|
||||
/** Switches the cloud empty-state copy. */
|
||||
serverReachable?: boolean;
|
||||
/** CTA handlers; absent => button hidden. */
|
||||
@@ -194,12 +201,35 @@ interface EmptyStateProps {
|
||||
|
||||
function EmptyState({
|
||||
tab = "all",
|
||||
searchActive = false,
|
||||
serverReachable = true,
|
||||
onUpload,
|
||||
onCreateFolder,
|
||||
newFolderDisabledReason,
|
||||
}: EmptyStateProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// A filter with no matches isn't an empty folder - say so, and skip the
|
||||
// upload / new-folder CTAs since clearing the filter is the way out.
|
||||
if (searchActive) {
|
||||
return (
|
||||
<div className="files-page-empty">
|
||||
<span className="files-page-empty-icon">
|
||||
<SearchIcon style={{ fontSize: "2.5rem" }} />
|
||||
</span>
|
||||
<div className="files-page-empty-title">
|
||||
{t("filesPage.empty.noResults.title", "No matching files")}
|
||||
</div>
|
||||
<div className="files-page-empty-hint">
|
||||
{t(
|
||||
"filesPage.empty.noResults.hint",
|
||||
"No files in this folder match your filter. Try a different term or clear the filter.",
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { titleKey, titleFallback, hintKey, hintFallback } = (() => {
|
||||
switch (tab) {
|
||||
case "local":
|
||||
|
||||
@@ -15,10 +15,12 @@ import {
|
||||
MultiSelect,
|
||||
SegmentedControl,
|
||||
Select,
|
||||
TextInput,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import UploadFileIcon from "@mui/icons-material/UploadFile";
|
||||
import QrCode2Icon from "@mui/icons-material/QrCode2";
|
||||
import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder";
|
||||
@@ -136,6 +138,7 @@ export default function FileManagerView() {
|
||||
sortMode,
|
||||
setSortMode,
|
||||
search,
|
||||
setSearch,
|
||||
originFilter,
|
||||
setOriginFilter,
|
||||
typeFilter,
|
||||
@@ -1360,6 +1363,30 @@ export default function FileManagerView() {
|
||||
aria-label={t("filesPage.typeFilter.label", "Filter by type")}
|
||||
/>
|
||||
)}
|
||||
<TextInput
|
||||
size="xs"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
placeholder={t("filesPage.search.placeholder", "Filter files…")}
|
||||
leftSection={<SearchIcon sx={{ fontSize: "1rem" }} />}
|
||||
rightSection={
|
||||
search ? (
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
onClick={() => setSearch("")}
|
||||
aria-label={t("filesPage.search.clear", "Clear filter")}
|
||||
>
|
||||
<CloseIcon sx={{ fontSize: "0.9rem" }} />
|
||||
</ActionIcon>
|
||||
) : null
|
||||
}
|
||||
aria-label={t(
|
||||
"filesPage.search.label",
|
||||
"Filter files by name",
|
||||
)}
|
||||
style={{ width: 180 }}
|
||||
/>
|
||||
<Select
|
||||
size="xs"
|
||||
value={sortMode}
|
||||
@@ -1451,6 +1478,7 @@ export default function FileManagerView() {
|
||||
entries={entries}
|
||||
loading={loading}
|
||||
currentTab={currentTab}
|
||||
searchActive={search.trim().length > 0}
|
||||
serverReachable={folders.serverReachable}
|
||||
selectedFileIds={selectedFileIds}
|
||||
activeWorkspaceFileIds={activeWorkspaceFileIdSet}
|
||||
|
||||
Reference in New Issue
Block a user