diff --git a/CHANGELOG.md b/CHANGELOG.md
index ae1b29f2..9a09ccb2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project adheres to
- ✨(backend) manage reconciliation requests for user accounts
- ✨(backend) add recursive folder export as ZIP archive
+- ✨(frontend) add folder export action
### Changed
diff --git a/src/frontend/apps/drive/src/features/drivers/types.ts b/src/frontend/apps/drive/src/features/drivers/types.ts
index 5acc9d2f..7a6b20ac 100644
--- a/src/frontend/apps/drive/src/features/drivers/types.ts
+++ b/src/frontend/apps/drive/src/features/drivers/types.ts
@@ -79,6 +79,7 @@ export type Item = {
children_create: boolean;
children_list: boolean;
destroy: boolean;
+ export: boolean;
favorite: boolean;
invite_owner: boolean;
link_configuration: boolean;
diff --git a/src/frontend/apps/drive/src/features/explorer/hooks/useItemActionMenuItems.tsx b/src/frontend/apps/drive/src/features/explorer/hooks/useItemActionMenuItems.tsx
index f9ba0bec..39be937d 100644
--- a/src/frontend/apps/drive/src/features/explorer/hooks/useItemActionMenuItems.tsx
+++ b/src/frontend/apps/drive/src/features/explorer/hooks/useItemActionMenuItems.tsx
@@ -20,6 +20,7 @@ import {
useGlobalExplorer,
} from "../components/GlobalExplorerContext";
import { useDownloadItem } from "@/features/items/hooks/useDownloadItem";
+import { baseApiUrl } from "@/features/api/utils";
import { ExplorerRenameItemModal } from "../components/modals/ExplorerRenameItemModal";
import { ExplorerCreateFolderModal } from "../components/modals/ExplorerCreateFolderModal";
import { ItemShareModal } from "../components/modals/share/ItemShareModal";
@@ -164,6 +165,14 @@ export const useItemActionMenuItems = ({
handleDownloadItem(item);
},
},
+ {
+ icon: ,
+ label: t("explorer.item.actions.export"),
+ isHidden: !item.abilities?.export || minimal,
+ callback: () => {
+ window.location.href = `${baseApiUrl()}items/${effectiveItemId}/export/`;
+ },
+ },
{
icon: ,
label: t("explorer.item.actions.duplicate"),
diff --git a/src/frontend/apps/e2e/__tests__/app-drive/context-menu.spec.ts b/src/frontend/apps/e2e/__tests__/app-drive/context-menu.spec.ts
index 884cf227..ea474d56 100644
--- a/src/frontend/apps/e2e/__tests__/app-drive/context-menu.spec.ts
+++ b/src/frontend/apps/e2e/__tests__/app-drive/context-menu.spec.ts
@@ -79,11 +79,44 @@ test.describe("Context menu", () => {
).toBeVisible();
await expect(page.getByRole("menuitem", { name: "Share" })).toBeVisible();
await expect(page.getByRole("menuitem", { name: "Move" })).toBeVisible();
+ await expect(
+ page.getByRole("menuitem", { name: "Export" }),
+ ).toBeVisible();
await expect(page.getByRole("menuitem", { name: "Rename" })).toBeVisible();
await expect(page.getByRole("menuitem", { name: "Star" })).toBeVisible();
await expect(page.getByRole("menuitem", { name: "Delete" })).toBeVisible();
});
+ test("Right-click on folder > Export calls the export endpoint", async ({
+ page,
+ }) => {
+ await createFolderInCurrentFolder(page, "TestFolder");
+
+ await page.route("**/api/v1.0/items/*/export/", async (route) => {
+ await route.fulfill({
+ status: 200,
+ headers: {
+ "content-disposition": "attachment; filename=test.zip",
+ "content-type": "application/zip",
+ },
+ body: "",
+ });
+ });
+
+ const exportRequestPromise = page.waitForRequest((request) =>
+ /\/api\/v1\.0\/items\/[^/]+\/export\/$/.test(request.url()),
+ );
+
+ const row = await getRowItem(page, "TestFolder");
+ await row.click({ button: "right" });
+ await page.getByRole("menuitem", { name: "Export" }).click();
+
+ const exportRequest = await exportRequestPromise;
+ expect(exportRequest.url()).toMatch(
+ /\/api\/v1\.0\/items\/[^/]+\/export\/$/,
+ );
+ });
+
test("Right-click on item > Rename works", async ({ page }) => {
await createFolderInCurrentFolder(page, "TestFolder");