From 00a969798b83db9e0845d64c8d14b38177531ee4 Mon Sep 17 00:00:00 2001 From: Nathan Vasse Date: Wed, 29 Jul 2026 12:03:11 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20create=20and=20open=20Doc?= =?UTF-8?q?s=20documents=20from=20the=20explorer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docs documents live in the tree as pointer items, so the explorer needs to treat them specially: a dedicated create menu entry posts an item carrying the docs metadata (same UX as template files, no auto-open), the grid shows the Docs icon, and clicking one opens the Docs frontend instead of the preview. The file preview route hands over to Docs too, so shared links to the item keep working. --- .../apps/drive/src/features/drivers/Driver.ts | 3 ++- .../drivers/implementations/StandardDriver.ts | 6 +++-- .../apps/drive/src/features/drivers/types.ts | 4 ++++ .../components/app-view/AppExplorerGrid.tsx | 14 ++++++++++++ .../explorer/components/icons/ItemIcon.tsx | 6 ++++- .../modals/ExplorerCreateFileModal.tsx | 7 ++++++ .../explorer/hooks/useCreateMenuItems.tsx | 20 ++++++++++++++++- .../drive/src/features/i18n/translations.json | 18 ++++++++++----- .../src/pages/explorer/items/files/[id].tsx | 22 ++++++++++++++++++- 9 files changed, 88 insertions(+), 12 deletions(-) diff --git a/src/frontend/apps/drive/src/features/drivers/Driver.ts b/src/frontend/apps/drive/src/features/drivers/Driver.ts index 17329a10..01f1d7e3 100644 --- a/src/frontend/apps/drive/src/features/drivers/Driver.ts +++ b/src/frontend/apps/drive/src/features/drivers/Driver.ts @@ -175,8 +175,9 @@ export abstract class Driver { }): { promise: Promise; abort: () => Promise }; abstract createFileFromTemplate(data: { parentId: string; - extension: string; + extension?: string; title: string; + metadata?: { external_app?: string }; }): Promise; abstract duplicateItem(id: string): Promise; abstract deleteItems(ids: string[]): Promise; diff --git a/src/frontend/apps/drive/src/features/drivers/implementations/StandardDriver.ts b/src/frontend/apps/drive/src/features/drivers/implementations/StandardDriver.ts index ea3b1932..d5977633 100644 --- a/src/frontend/apps/drive/src/features/drivers/implementations/StandardDriver.ts +++ b/src/frontend/apps/drive/src/features/drivers/implementations/StandardDriver.ts @@ -450,8 +450,9 @@ export class StandardDriver extends Driver { async createFileFromTemplate(data: { parentId?: string; - extension: string; + extension?: string; title: string; + metadata?: { external_app?: string }; }): Promise { const url = data.parentId ? `items/${data.parentId}/children/` : `items/`; @@ -461,7 +462,8 @@ export class StandardDriver extends Driver { method: "POST", body: JSON.stringify({ type: "file", - extension: data.extension, + ...(data.extension ? { extension: data.extension } : {}), + ...(data.metadata ? { metadata: data.metadata } : {}), title: data.title, }), }, diff --git a/src/frontend/apps/drive/src/features/drivers/types.ts b/src/frontend/apps/drive/src/features/drivers/types.ts index 0d1782c1..d326e90b 100644 --- a/src/frontend/apps/drive/src/features/drivers/types.ts +++ b/src/frontend/apps/drive/src/features/drivers/types.ts @@ -84,6 +84,9 @@ export type Item = { url_preview?: string; size?: number; mimetype?: string; + metadata?: { + external_app?: string; + }; user_roles?: Role[]; user_role?: Role; link_reach?: LinkReach; @@ -237,6 +240,7 @@ export type ApiConfig = { FRONTEND_STORAGE_GAUGE_INFORMATION_LINK?: string; FRONTEND_CSS_URL?: string; FRONTEND_JS_URL?: string; + FRONTEND_DOCS_URL?: string; theme_customization?: ThemeCustomization; }; diff --git a/src/frontend/apps/drive/src/features/explorer/components/app-view/AppExplorerGrid.tsx b/src/frontend/apps/drive/src/features/explorer/components/app-view/AppExplorerGrid.tsx index 4b65aed3..aa8a0fa8 100644 --- a/src/frontend/apps/drive/src/features/explorer/components/app-view/AppExplorerGrid.tsx +++ b/src/frontend/apps/drive/src/features/explorer/components/app-view/AppExplorerGrid.tsx @@ -20,6 +20,7 @@ import { openWopiInNewTab } from "@/features/wopi/openWopi"; import { itemToPreviewFile } from "@/features/explorer/utils/utils"; import { useModal } from "@gouvfr-lasuite/cunningham-react"; import { ConvertLegacyFileModal } from "@/features/explorer/components/modals/ConvertLegacyFileModal"; +import { useConfig } from "@/features/config/ConfigProvider"; /** * Wrapper around EmbeddedExplorerGrid to display a list of items in a table. @@ -33,6 +34,7 @@ import { ConvertLegacyFileModal } from "@/features/explorer/components/modals/Co export const AppExplorerGrid = () => { const { t } = useTranslation(); const appExplorer = useAppExplorer(); + const { config } = useConfig(); const router = useRouter(); @@ -53,6 +55,18 @@ export const AppExplorerGrid = () => { const handleFileClick = appExplorer.onFileClick ?? ((item: Item) => { + if (item.metadata?.external_app === "docs") { + if (config.FRONTEND_DOCS_URL) { + window.open( + `${config.FRONTEND_DOCS_URL}/docs/${item.id}/`, + "_blank", + "noopener", + ); + } else { + addToast({t("explorer.grid.no_url")}); + } + return; + } if (item.abilities.convert) { setItemToConvert(item); convertModal.open(); diff --git a/src/frontend/apps/drive/src/features/explorer/components/icons/ItemIcon.tsx b/src/frontend/apps/drive/src/features/explorer/components/icons/ItemIcon.tsx index e13a3a1e..79d22abc 100644 --- a/src/frontend/apps/drive/src/features/explorer/components/icons/ItemIcon.tsx +++ b/src/frontend/apps/drive/src/features/explorer/components/icons/ItemIcon.tsx @@ -26,7 +26,7 @@ export const ItemIcon = ({ if (extendedIcon) { return ; } - return ; + return ; }; /** @@ -45,6 +45,10 @@ export const getItemExtendedIcon = ( return folderIcon.src; } + if (item.metadata?.external_app === "docs") { + return ICONS[type][MimeCategory.DOCS]; + } + const uploadState = item.upload_state; if (uploadState === ItemUploadState.SUSPICIOUS) { return ICONS[type][MimeCategory.SUSPICIOUS]; diff --git a/src/frontend/apps/drive/src/features/explorer/components/modals/ExplorerCreateFileModal.tsx b/src/frontend/apps/drive/src/features/explorer/components/modals/ExplorerCreateFileModal.tsx index 503d61b4..19745177 100644 --- a/src/frontend/apps/drive/src/features/explorer/components/modals/ExplorerCreateFileModal.tsx +++ b/src/frontend/apps/drive/src/features/explorer/components/modals/ExplorerCreateFileModal.tsx @@ -19,6 +19,7 @@ export enum ExplorerCreateFileType { DOC = "doc", POWERPOINT = "powerpoint", CALC = "calc", + DOCS_DOCUMENT = "docs", } const getExtension = (type: ExplorerCreateFileType) => { @@ -29,6 +30,9 @@ const getExtension = (type: ExplorerCreateFileType) => { return "odp"; case ExplorerCreateFileType.CALC: return "ods"; + case ExplorerCreateFileType.DOCS_DOCUMENT: + // Docs documents are external items: no template file, no extension. + return undefined; } }; @@ -53,6 +57,9 @@ export const ExplorerCreateFileModal = ( parentId: props.parentId, extension: extension, title: data.filename, + ...(props.type === ExplorerCreateFileType.DOCS_DOCUMENT + ? { metadata: { external_app: "docs" } } + : {}), }, { onSuccess: (createdItem) => { 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 543ca91c..d3fa42d2 100644 --- a/src/frontend/apps/drive/src/features/explorer/hooks/useCreateMenuItems.tsx +++ b/src/frontend/apps/drive/src/features/explorer/hooks/useCreateMenuItems.tsx @@ -1,4 +1,10 @@ -import { MenuItem, IconSize } from "@gouvfr-lasuite/ui-kit"; +import { + MenuItem, + IconSize, + FileIconContent, + ICONS, + MimeCategory, +} from "@gouvfr-lasuite/ui-kit"; import { useTranslation } from "react-i18next"; import { useGlobalExplorer } from "@/features/explorer/components/GlobalExplorerContext"; import createFolderSvg from "@/assets/icons/create_folder.svg"; @@ -89,6 +95,18 @@ export const useCreateMenuItems = ({ if (includeCreate) { items.push( + { + icon: ( + + ), + label: t("explorer.tree.create.file.docs_document"), + callback: () => + openCreateFileModal(ExplorerCreateFileType.DOCS_DOCUMENT), + }, + { type: "separator" }, { icon: renderFileIcon({ type: ItemType.FILE, diff --git a/src/frontend/apps/drive/src/features/i18n/translations.json b/src/frontend/apps/drive/src/features/i18n/translations.json index 4d4a7de4..9fe8165c 100644 --- a/src/frontend/apps/drive/src/features/i18n/translations.json +++ b/src/frontend/apps/drive/src/features/i18n/translations.json @@ -362,7 +362,8 @@ "file": { "doc": "New text document", "powerpoint": "New slides", - "calc": "New spreadsheet" + "calc": "New spreadsheet", + "docs_document": "New Docs document" } }, "import": { @@ -504,7 +505,8 @@ "label": "File name", "placeholder": "Enter file name", "cancel": "Cancel", - "submit": "Create" + "submit": "Create", + "title_docs": "New Docs document" } }, "upload": { @@ -1084,7 +1086,8 @@ "file": { "doc": "Nouveau document texte", "powerpoint": "Nouvelles diapositives", - "calc": "Nouveau tableau de calcul" + "calc": "Nouveau tableau de calcul", + "docs_document": "Nouveau Docs" }, "workspace": "Nouvel espace" }, @@ -1213,7 +1216,8 @@ "label": "Nom du fichier", "placeholder": "Entrez le nom du fichier", "cancel": "Annuler", - "submit": "Créer" + "submit": "Créer", + "title_docs": "Nouveau Docs" } }, "upload": { @@ -1779,7 +1783,8 @@ "file": { "doc": "Nieuw tekstdocument", "powerpoint": "Nieuwe presentatie", - "calc": "Nieuwe spreadsheet" + "calc": "Nieuwe spreadsheet", + "docs_document": "Nieuw Docs" }, "workspace": "Nieuwe werkruimte" }, @@ -1922,7 +1927,8 @@ "label": "Bestandsnaam", "placeholder": "Voer bestandsnaam in", "cancel": "Annuleren", - "submit": "Maken" + "submit": "Maken", + "title_docs": "Nieuw Docs-document" } }, "upload": { diff --git a/src/frontend/apps/drive/src/pages/explorer/items/files/[id].tsx b/src/frontend/apps/drive/src/pages/explorer/items/files/[id].tsx index 3b7cab05..6b3c6e0c 100644 --- a/src/frontend/apps/drive/src/pages/explorer/items/files/[id].tsx +++ b/src/frontend/apps/drive/src/pages/explorer/items/files/[id].tsx @@ -10,18 +10,38 @@ import { useRouter } from "next/router"; import { useTranslation } from "react-i18next"; import { useItem } from "@/features/explorer/hooks/useQueries"; import { GlobalLayout } from "@/features/layouts/components/global/GlobalLayout"; +import { useConfig } from "@/features/config/ConfigProvider"; +import { useEffect } from "react"; export default function FilePage() { const { t } = useTranslation(); const router = useRouter(); const itemId = router.query.id as string; + const { config } = useConfig(); const { data: item, isLoading, error } = useItem(itemId); + // Items pointing to an external app are not previewable here: hand the + // user over to the owning application. + const externalAppUrl = + item?.metadata?.external_app === "docs" && config?.FRONTEND_DOCS_URL + ? `${config.FRONTEND_DOCS_URL}/docs/${item.id}/` + : null; + + useEffect(() => { + if (externalAppUrl) { + window.location.replace(externalAppUrl); + } + }, [externalAppUrl]); + // On 403, 401, the user is automatically redirected to the 401/403 page. // If the error is a 401 or 403, we want to show the spinner page because an auto redirect is happening. - if (isLoading || (error && [401, 403].includes(error.code))) { + if ( + isLoading || + externalAppUrl || + (error && [401, 403].includes(error.code)) + ) { return ; }