(frontend) disable the new document entries without upload rights

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.
This commit is contained in:
Nathan Vasse
2026-09-15 18:02:24 +02:00
parent 981e86a600
commit 5947ccd3ea
2 changed files with 66 additions and 1 deletions
@@ -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>(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,
},
);
}
@@ -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();
});
});