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 ; }