From dc95ed1eca4eaf35a8a4d68be201f36a95437219 Mon Sep 17 00:00:00 2001 From: Nathan Vasse Date: Tue, 22 Sep 2026 17:49:03 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20preview=20inaccessible=20?= =?UTF-8?q?folders=20without=20loading=20content?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../utils/__tests__/itemToPreviewFile.test.ts | 112 ++++++++++++++++++ .../src/features/explorer/utils/utils.ts | 15 ++- .../ui/preview/CustomFilesPreview.tsx | 5 +- 3 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 src/frontend/apps/drive/src/features/explorer/utils/__tests__/itemToPreviewFile.test.ts diff --git a/src/frontend/apps/drive/src/features/explorer/utils/__tests__/itemToPreviewFile.test.ts b/src/frontend/apps/drive/src/features/explorer/utils/__tests__/itemToPreviewFile.test.ts new file mode 100644 index 00000000..23e8ad78 --- /dev/null +++ b/src/frontend/apps/drive/src/features/explorer/utils/__tests__/itemToPreviewFile.test.ts @@ -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, + }); + }); +}); diff --git a/src/frontend/apps/drive/src/features/explorer/utils/utils.ts b/src/frontend/apps/drive/src/features/explorer/utils/utils.ts index 54e0982f..2edd9191 100644 --- a/src/frontend/apps/drive/src/features/explorer/utils/utils.ts +++ b/src/frontend/apps/drive/src/features/explorer/utils/utils.ts @@ -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), + }; }; diff --git a/src/frontend/apps/drive/src/features/ui/preview/CustomFilesPreview.tsx b/src/frontend/apps/drive/src/features/ui/preview/CustomFilesPreview.tsx index 1b604e0f..bbcdf842 100644 --- a/src/frontend/apps/drive/src/features/ui/preview/CustomFilesPreview.tsx +++ b/src/frontend/apps/drive/src/features/ui/preview/CustomFilesPreview.tsx @@ -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]);