(frontend) filter the search modal by location, type, contact and date

Replace the search modal type/workspace/scope filters with the location,
file type, shared-with and modification date filters, matching the new
search design.
This commit is contained in:
Nicolas Clerc
2026-06-29 16:49:52 +02:00
parent 77b3156805
commit ebf39cf9bb
7 changed files with 112 additions and 20 deletions
+1
View File
@@ -12,6 +12,7 @@ and this project adheres to
- ✨(backend) allow converting a file while it is being analyzed
- ✨(frontend) add file type, contact and modification date topbar filters
- ✨(frontend) add location, file type, contact and date search filters
### Fixed
@@ -42,6 +42,7 @@ export type ItemFilters = {
is_favorite?: boolean;
category?: string;
contact?: string;
location?: string;
updated_at_after?: string;
updated_at_before?: string;
};
@@ -0,0 +1,53 @@
import { Filter, FilterOption, IconProps } from "@gouvfr-lasuite/ui-kit";
import { JSX, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Key } from "react-aria-components";
import { MyFilesIcon } from "@/features/ui/components/icon/MyFilesIcon";
import { SharedWithMeIcon } from "@/features/ui/components/icon/SharedWithMeIcon";
import { StarredIcon } from "@/features/ui/components/icon/StarredIcon";
import { TrashIcon } from "@/features/ui/components/icon/TrashIcon";
import { getResetOption } from "./filterUtils";
const LOCATION_OPTIONS: {
value: string;
icon: (props: Partial<IconProps>) => JSX.Element;
}[] = [
{ value: "my_files", icon: MyFilesIcon },
{ value: "shared_with_me", icon: SharedWithMeIcon },
{ value: "starred", icon: StarredIcon },
{ value: "trashbin", icon: TrashIcon },
];
export const ExplorerFilterLocation = (props: {
value: string | null;
onChange: (value: Key | null) => void;
}) => {
const { t } = useTranslation();
const options: FilterOption[] = useMemo(
() => [
// Reset sits at the top of the list, above the locations, as in the design.
{ ...getResetOption(t), showSeparator: true },
...LOCATION_OPTIONS.map(({ value, icon: Icon }) => ({
label: t(`explorer.filters.location.options.${value}`),
value,
render: () => (
<div className="explorer__filters__item">
<Icon />
{t(`explorer.filters.location.options.${value}`)}
</div>
),
})),
],
[t],
);
return (
<Filter
label={t("explorer.filters.location.label")}
options={options}
selectedKey={props.value ?? null} // undefined would trigger "uncontrolled components become controlled" warning.
onSelectionChange={props.onChange}
/>
);
};
@@ -1,6 +1,7 @@
export { ExplorerFilters } from "./ExplorerFilters";
export { handleFilterChange } from "./filterUtils";
export { ALL, handleFilterChange } from "./filterUtils";
export { ExplorerFilterContact } from "./ExplorerFilterContact";
export { ExplorerFilterLocation } from "./ExplorerFilterLocation";
export { ExplorerFilterCategory } from "./ExplorerFilterCategory";
export { ExplorerFilterModified } from "./ExplorerFilterModified";
export { ExplorerFilterType } from "./ExplorerFilterType";
@@ -21,16 +21,23 @@ import {
useGlobalExplorer,
} from "../../GlobalExplorerContext";
import {
ExplorerFilterType,
ExplorerFilterWorkspace,
ExplorerFilterScope,
ALL,
ExplorerFilterCategory,
ExplorerFilterContact,
ExplorerFilterLocation,
ExplorerFilterModified,
handleFilterChange,
} from "@/features/explorer/components/filters";
import { ItemFilters } from "@/features/drivers/Driver";
import { Key } from "react-aria-components";
import { clearFromRoute, getItemTitle } from "@/features/explorer/utils/utils";
import { messageModalTrashNavigate } from "../../trash/utils";
import { useIsMinimalLayout } from "@/utils/useLayout";
import { useAuth } from "@/features/auth/Auth";
import {
applyDateRange,
DateRange,
dateRangeFromFilters,
} from "@/features/explorer/utils/dateFilters";
import { openWopiInNewTab } from "@/features/wopi/openWopi";
import { itemToPreviewFile } from "@/features/explorer/utils/utils";
@@ -40,8 +47,8 @@ type ExplorerSearchModalProps = Pick<ModalProps, "isOpen" | "onClose"> & {
export const ExplorerSearchModal = (props: ExplorerSearchModalProps) => {
const { t } = useTranslation();
const { user } = useAuth();
const [inputValue, setInputValue] = useState<string>("");
const isMinimalLayout = useIsMinimalLayout();
const [filters, setFilters] = useState<ItemFilters>(
props.defaultFilters || {},
);
@@ -86,6 +93,11 @@ export const ExplorerSearchModal = (props: ExplorerSearchModalProps) => {
setFilters(handleFilterChange(filters, name, value));
};
// The modification date filter drives two query params at once.
const onModifiedChange = (range: DateRange | null) => {
setFilters(applyDateRange(filters, range));
};
const modals = useModals();
const onItemClick = (item: Item) => {
@@ -149,18 +161,24 @@ export const ExplorerSearchModal = (props: ExplorerSearchModalProps) => {
>
<div className="explorer__search__modal__filters">
<div className="explorer__search__modal__filters__inputs">
<ExplorerFilterType
value={filters?.type ?? null}
onChange={(value) => onFilterChange("type", value)}
<ExplorerFilterLocation
value={filters?.location ?? null}
onChange={(value) => onFilterChange("location", value)}
/>
<ExplorerFilterWorkspace
value={filters?.workspace ?? null}
isDisabled={isMinimalLayout}
onChange={(value) => onFilterChange("workspace", value)}
<ExplorerFilterCategory
value={filters?.category ?? null}
onChange={(value) => onFilterChange("category", value)}
/>
<ExplorerFilterScope
value={filters?.scope ?? null}
onChange={(value) => onFilterChange("scope", value)}
{/* Contacts and user search both require authentication. */}
{user && (
<ExplorerFilterContact
value={filters?.contact ?? null}
onChange={(value) => onFilterChange("contact", value ?? ALL)}
/>
)}
<ExplorerFilterModified
value={dateRangeFromFilters(filters)}
onChange={onModifiedChange}
/>
</div>
@@ -246,6 +246,15 @@
}
},
"filters": {
"location": {
"label": "Location",
"options": {
"my_files": "My files",
"shared_with_me": "Shared with me",
"starred": "Starred",
"trashbin": "Trash"
}
},
"scopes": {
"label": "Location",
"options": {
@@ -884,6 +893,15 @@
}
},
"filters": {
"location": {
"label": "Emplacement",
"options": {
"my_files": "Mes fichiers",
"shared_with_me": "Partagés avec moi",
"starred": "Favoris",
"trashbin": "Corbeille"
}
},
"scopes": {
"label": "Emplacement",
"options": {
@@ -105,9 +105,9 @@ test("Search folder from trash and cannot navigate to it", async ({ page }) => {
await expect(input).toBeVisible();
await input.fill("I am");
// Set the scope to trash.
// Set the location to the trash.
await page.getByRole("button", { name: "Location" }).click();
await page.getByRole("option", { name: "Recycle bin" }).click();
await page.getByRole("option", { name: "Trash" }).click();
searchItems = page.getByTestId("search-item");
await expect(searchItems).toHaveCount(1);
@@ -142,9 +142,9 @@ test("Search a deleted file and click on it", async ({ page }) => {
await expect(input).toBeVisible();
await input.fill("resum");
// Set the scope to trash.
// Set the location to the trash.
await page.getByRole("button", { name: "Location" }).click();
await page.getByRole("option", { name: "Recycle bin" }).click();
await page.getByRole("option", { name: "Trash" }).click();
let searchItems = page.getByTestId("search-item");
await expect(searchItems).toHaveCount(1);