mirror of
https://github.com/suitenumerique/drive.git
synced 2026-08-17 20:15:40 +02:00
✨(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:
@@ -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;
|
||||
};
|
||||
|
||||
+53
@@ -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";
|
||||
|
||||
+33
-15
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user