✨(frontend) preview inaccessible folders without loading content

Explain denied access while preserving folder names and the current route.
Clear preview URLs for denied folders and include them in file navigation
without changing how accessible folders are opened.
This commit is contained in:
Nathan Vasse
2026-10-07 14:14:44 +02:00
parent 235ceafa28
commit dc95ed1eca
3 changed files with 125 additions and 7 deletions
@@ -0,0 +1,112 @@
import { Item, ItemType } from "@/features/drivers/types";
import {
getNavigableItem,
isFolderAccessDenied,
} from "@/features/drivers/utils";
import { itemToPreviewFile } from "../utils";
jest.mock("@/features/config/Config", () => ({ getDriver: jest.fn() }));
jest.mock("@/features/i18n/initI18n", () => ({ t: (key: string) => key }));
const folder = {
id: "folder",
title: "Top.secret.pdf",
type: ItemType.FOLDER,
abilities: {},
} as Item;
const restriction: Item = {
...folder,
id: "restriction",
type: ItemType.RESTRICTION,
target: {
id: folder.id,
title: folder.title,
path: "folder",
is_restricted: true,
can_access: false,
deleted: false,
abilities: folder.abilities,
},
};
describe("folder preview access", () => {
it("previews an inaccessible restriction using its entry ID and full name", () => {
expect(isFolderAccessDenied(restriction)).toBe(true);
expect(getNavigableItem(restriction)).toBeUndefined();
expect(itemToPreviewFile(restriction)).toMatchObject({
id: restriction.id,
title: "Top.secret.pdf",
isFolderAccessDenied: true,
mimetype: "",
url: "",
url_preview: "",
size: 0,
});
});
it("does not pass stale content URLs or metadata to a denied folder preview", () => {
expect(
itemToPreviewFile({
...restriction,
mimetype: "application/pdf",
url: "/original.pdf",
url_preview: "/preview.pdf",
size: 100,
}),
).toMatchObject({
isFolderAccessDenied: true,
mimetype: "",
url: "",
url_preview: "",
size: 0,
});
});
it.each([false, true])(
"keeps normal folder navigation when is_restricted is %s",
(is_restricted) => {
const item = { ...folder, is_restricted };
expect(isFolderAccessDenied(item)).toBe(false);
expect(itemToPreviewFile(item).isFolderAccessDenied).toBe(false);
expect(getNavigableItem(item)).toBe(item);
},
);
it("navigates to the target of an accessible restriction", () => {
const item = {
...restriction,
target: { ...restriction.target!, can_access: true },
};
expect(isFolderAccessDenied(item)).toBe(false);
expect(getNavigableItem(item)).toMatchObject({ id: folder.id });
});
it.each([null, { ...restriction.target!, deleted: true }])(
"keeps missing or deleted targets unavailable",
(target) => {
const item = { ...restriction, target };
expect(isFolderAccessDenied(item)).toBe(false);
expect(getNavigableItem(item)).toBeUndefined();
},
);
it("preserves file metadata and keeps the denied-folder flag off", () => {
const item = {
...folder,
type: ItemType.FILE,
mimetype: "application/pdf",
url: "/original.pdf",
url_preview: "/preview.pdf",
size: 100,
};
expect(itemToPreviewFile(item)).toMatchObject({
id: item.id,
mimetype: item.mimetype,
url: item.url,
url_preview: item.url_preview,
size: item.size,
isFolderAccessDenied: false,
});
});
});
@@ -9,6 +9,7 @@ import {
import i18n from "@/features/i18n/initI18n";
import { FilePreviewType } from "@gouvfr-lasuite/ui-components";
import { DefaultRoute } from "@/utils/defaultRoutes";
import { isFolderAccessDenied } from "@/features/drivers/utils";
/**
* When passing through my-files, favorites, shared with me, etc. we set a fromRoute key in the session storage.
@@ -200,15 +201,17 @@ export const getItemTitle = (item: Item) => {
return item.title;
};
export const itemToPreviewFile = (item: Item) => {
export const itemToPreviewFile = (item: Item): FilePreviewType => {
const accessDenied = isFolderAccessDenied(item);
return {
id: item.id,
title: item.title,
mimetype: item.mimetype ?? "",
url_preview: item.url_preview ?? "",
url: item.url ?? "",
mimetype: accessDenied ? "" : (item.mimetype ?? ""),
url_preview: accessDenied ? "" : (item.url_preview ?? ""),
url: accessDenied ? "" : (item.url ?? ""),
isFolderAccessDenied: accessDenied,
isSuspicious: item.upload_state === ItemUploadState.SUSPICIOUS,
is_wopi_supported: item.is_wopi_supported,
size: item.size,
} as FilePreviewType;
size: accessDenied ? 0 : (item.size ?? 0),
};
};
@@ -1,4 +1,5 @@
import { Item, ItemType } from "@/features/drivers/types";
import { isFolderAccessDenied } from "@/features/drivers/utils";
import {
FilePreview,
FilePreviewType,
@@ -41,7 +42,9 @@ export const CustomFilesPreview = ({
const files = useMemo(() => {
return items
.filter((item) => item.type === ItemType.FILE)
.filter(
(item) => item.type === ItemType.FILE || isFolderAccessDenied(item),
)
.map(itemToPreviewFile);
}, [items]);