From 5947ccd3eaceda7894802c896d2fb242570b417d Mon Sep 17 00:00:00 2001 From: Nathan Vasse Date: Wed, 9 Sep 2026 17:46:10 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20disable=20the=20new=20doc?= =?UTF-8?q?ument=20entries=20without=20upload=20rights?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backend now rejects the creation, and the 403 is already toasted, but letting the user name a document only to have it refused is a poor way to learn about a quota. Surface the reason on the entries instead. Entitlements are assumed permissive while the query is in flight, so the menu does not grey out on every page load. "New folder" stays enabled since folders are not quota gated. --- .../explorer/hooks/useCreateMenuItems.tsx | 20 ++++++++ .../create-file-from-template.spec.ts | 47 ++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/frontend/apps/drive/src/features/explorer/hooks/useCreateMenuItems.tsx b/src/frontend/apps/drive/src/features/explorer/hooks/useCreateMenuItems.tsx index 3a7aeb7a..2e857375 100644 --- a/src/frontend/apps/drive/src/features/explorer/hooks/useCreateMenuItems.tsx +++ b/src/frontend/apps/drive/src/features/explorer/hooks/useCreateMenuItems.tsx @@ -14,6 +14,8 @@ import { ExplorerCreateFolderModal } from "../components/modals/ExplorerCreateFo import { useState } from "react"; import { useRouter } from "next/router"; import { isMyFilesRoute } from "@/utils/defaultRoutes"; +import { useEntitlements } from "@/features/entitlement-disclaimers/hooks/useEntitlements"; +import { getCannotUploadReasonDescription } from "@/features/entitlement-disclaimers/disclaimers/CannotUploadDisclaimer"; type UseCreateMenuItemsProps = { includeImport?: boolean; @@ -47,6 +49,18 @@ export const useCreateMenuItems = ({ // it in the current view — no redirect needed. const shouldRedirectToCreated = !canCreateHere && !isOnMyFiles; + // Creating a document writes a real file to storage, so it is gated on the + // same entitlement as an upload. Assume it is allowed while the query is in + // flight: the backend rejects the creation anyway, and greying the entries + // out on every page load would be worse than a rare late disable. + const { data: entitlements } = useEntitlements(); + const canUpload = entitlements?.can_upload.result ?? true; + const cannotUploadReason = canUpload + ? undefined + : (entitlements?.can_upload.message ?? + getCannotUploadReasonDescription(entitlements?.can_upload.reason) ?? + t("entitlements.can_upload.cannot_upload")); + const createFolderModal = useModal(); const [createFileModalType, setCreateFileModalType] = useState(ExplorerCreateFileType.DOC); @@ -97,6 +111,8 @@ export const useCreateMenuItems = ({ }), label: t("explorer.tree.create.file.doc"), callback: () => openCreateFileModal(ExplorerCreateFileType.DOC), + isDisabled: !canUpload, + subText: cannotUploadReason, }, { icon: renderFileIcon({ @@ -107,6 +123,8 @@ export const useCreateMenuItems = ({ }), label: t("explorer.tree.create.file.powerpoint"), callback: () => openCreateFileModal(ExplorerCreateFileType.POWERPOINT), + isDisabled: !canUpload, + subText: cannotUploadReason, }, { icon: renderFileIcon({ @@ -117,6 +135,8 @@ export const useCreateMenuItems = ({ }), label: t("explorer.tree.create.file.calc"), callback: () => openCreateFileModal(ExplorerCreateFileType.CALC), + isDisabled: !canUpload, + subText: cannotUploadReason, }, ); } diff --git a/src/frontend/apps/e2e/__tests__/app-drive/create-file-from-template.spec.ts b/src/frontend/apps/e2e/__tests__/app-drive/create-file-from-template.spec.ts index 13d6c86e..ecf769a1 100644 --- a/src/frontend/apps/e2e/__tests__/app-drive/create-file-from-template.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-drive/create-file-from-template.spec.ts @@ -1,4 +1,4 @@ -import test, { expect } from "@playwright/test"; +import test, { expect, Page } from "@playwright/test"; import { clearDb, login } from "./utils-common"; import { expectRowItem } from "./utils-embedded-grid"; import { createFolderInCurrentFolder } from "./utils-item"; @@ -89,3 +89,48 @@ test.describe("Create file from template in a folder", () => { await expectRowItem(page, "My presentation"); }); }); + +test.describe("Create file from template without the upload entitlement", () => { + const mockEntitlements = async (page: Page) => { + await page.route("**/api/v1.0/entitlements/", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + can_access: { result: true }, + can_upload: { result: false, reason: "user_quota_exceeded" }, + context: {}, + }), + }); + }); + }; + + test.beforeEach(async ({ page }) => { + await clearDb(); + await login(page, "drive@example.com"); + await mockEntitlements(page); + await page.goto("/"); + }); + + test("disables the document entries and explains why", async ({ page }) => { + await page.getByRole("button", { name: "New" }).click(); + + for (const name of ["New text document", "New spreadsheet", "New slides"]) { + const entry = page.getByRole("menuitem", { name }); + await expect(entry).toBeVisible(); + await expect(entry).toBeDisabled(); + } + + await expect( + page.getByText(/your personal quota has been reached/i).first(), + ).toBeVisible(); + }); + + test("keeps New folder available", async ({ page }) => { + await page.getByRole("button", { name: "New" }).click(); + + await expect( + page.getByRole("menuitem", { name: "New folder" }), + ).toBeEnabled(); + }); +});