From b8bdcbf7ed3a8ad764615478e44e954fc2d91364 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Wed, 21 Jan 2026 10:30:24 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=82(frontend)=20use=20max=20size=20and?= =?UTF-8?q?=20extension=20from=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The max size and allowed extensions for document import are now fetched from the application configuration. This ensures consistency across the app and allows for easier updates to these settings in the future. --- src/backend/core/api/viewsets.py | 2 + src/backend/core/tests/test_api_config.py | 2 + .../e2e/__tests__/app-impress/utils-common.ts | 2 + .../impress/src/core/config/api/useConfig.tsx | 2 + .../docs/docs-grid/api/useImportDoc.tsx | 39 ++---- .../docs/docs-grid/components/DocsGrid.tsx | 64 +--------- .../docs/docs-grid/hooks/useImport.tsx | 116 ++++++++++++++++++ 7 files changed, 137 insertions(+), 90 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/docs/docs-grid/hooks/useImport.tsx diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index b0cc3c9b..7432004d 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -2340,6 +2340,8 @@ class ConfigView(drf.views.APIView): "AI_FEATURE_ENABLED", "COLLABORATION_WS_URL", "COLLABORATION_WS_NOT_CONNECTED_READY_ONLY", + "CONVERSION_FILE_EXTENSIONS_ALLOWED", + "CONVERSION_FILE_MAX_SIZE", "CRISP_WEBSITE_ID", "ENVIRONMENT", "FRONTEND_CSS_URL", diff --git a/src/backend/core/tests/test_api_config.py b/src/backend/core/tests/test_api_config.py index e29187b3..ac3a9b30 100644 --- a/src/backend/core/tests/test_api_config.py +++ b/src/backend/core/tests/test_api_config.py @@ -46,6 +46,8 @@ def test_api_config(is_authenticated): "AI_FEATURE_ENABLED": False, "COLLABORATION_WS_URL": "http://testcollab/", "COLLABORATION_WS_NOT_CONNECTED_READY_ONLY": True, + "CONVERSION_FILE_EXTENSIONS_ALLOWED": [".docx", ".md"], + "CONVERSION_FILE_MAX_SIZE": 20971520, "CRISP_WEBSITE_ID": "123", "ENVIRONMENT": "test", "FRONTEND_CSS_URL": "http://testcss/", diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts index 7b6bd2e7..eede9ba2 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts @@ -8,6 +8,8 @@ export const CONFIG = { CRISP_WEBSITE_ID: null, COLLABORATION_WS_URL: 'ws://localhost:4444/collaboration/ws/', COLLABORATION_WS_NOT_CONNECTED_READY_ONLY: true, + CONVERSION_FILE_EXTENSIONS_ALLOWED: ['.docx', '.md'], + CONVERSION_FILE_MAX_SIZE: 20971520, ENVIRONMENT: 'development', FRONTEND_CSS_URL: null, FRONTEND_JS_URL: null, diff --git a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx index df846e67..6181ecab 100644 --- a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx +++ b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx @@ -18,6 +18,8 @@ export interface ConfigResponse { AI_FEATURE_ENABLED?: boolean; COLLABORATION_WS_URL?: string; COLLABORATION_WS_NOT_CONNECTED_READY_ONLY?: boolean; + CONVERSION_FILE_EXTENSIONS_ALLOWED: string[]; + CONVERSION_FILE_MAX_SIZE: number; CRISP_WEBSITE_ID?: string; ENVIRONMENT: string; FRONTEND_CSS_URL?: string; diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/api/useImportDoc.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/api/useImportDoc.tsx index e25122d4..d4b9d1e9 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/api/useImportDoc.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/api/useImportDoc.tsx @@ -17,43 +17,22 @@ import { } from '@/api'; import { Doc, DocsResponse, KEY_LIST_DOC } from '@/docs/doc-management'; -enum ContentTypes { +export enum ContentTypes { Docx = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', Markdown = 'text/markdown', OctetStream = 'application/octet-stream', } -export enum ContentTypesAllowed { - Docx = ContentTypes.Docx, - Markdown = ContentTypes.Markdown, -} - -const getMimeType = (file: File): string => { - if (file.type) { - return file.type; - } - - const extension = file.name.split('.').pop()?.toLowerCase(); - - switch (extension) { - case 'md': - return ContentTypes.Markdown; - case 'markdown': - return ContentTypes.Markdown; - case 'docx': - return ContentTypes.Docx; - default: - return ContentTypes.OctetStream; - } -}; - -export const importDoc = async (file: File): Promise => { +export const importDoc = async ([file, mimeType]: [ + File, + string, +]): Promise => { const form = new FormData(); form.append( 'file', new File([file], file.name, { - type: getMimeType(file), + type: mimeType, lastModified: file.lastModified, }), ); @@ -71,14 +50,14 @@ export const importDoc = async (file: File): Promise => { return response.json() as Promise; }; -type UseImportDocOptions = UseMutationOptions; +type UseImportDocOptions = UseMutationOptions; export function useImportDoc(props?: UseImportDocOptions) { const { toast } = useToastProvider(); const queryClient = useQueryClient(); const { t } = useTranslation(); - return useMutation({ + return useMutation({ mutationFn: importDoc, ...props, onSuccess: (...successProps) => { @@ -135,7 +114,7 @@ export function useImportDoc(props?: UseImportDocOptions) { onError: (...errorProps) => { toast( t(`The document "{{documentName}}" import has failed`, { - documentName: errorProps?.[1].name || '', + documentName: errorProps?.[1][0].name || '', }), VariantType.ERROR, ); diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx index ed074c1e..d9df77b3 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx @@ -1,11 +1,8 @@ import { Button, Tooltip as TooltipBase, - VariantType, - useToastProvider, } from '@gouvfr-lasuite/cunningham-react'; import { useMemo, useState } from 'react'; -import { useDropzone } from 'react-dropzone'; import { useTranslation } from 'react-i18next'; import { InView } from 'react-intersection-observer'; import styled, { css } from 'styled-components'; @@ -16,7 +13,7 @@ import { DocDefaultFilter, useInfiniteDocs } from '@/docs/doc-management'; import { useResponsiveStore } from '@/stores'; import { useInfiniteDocsTrashbin } from '../api'; -import { ContentTypesAllowed, useImportDoc } from '../api/useImportDoc'; +import { useImport } from '../hooks/useImport'; import { useResponsiveDocGrid } from '../hooks/useResponsiveDocGrid'; import { @@ -45,64 +42,11 @@ export const DocsGrid = ({ }: DocsGridProps) => { const { t } = useTranslation(); const [isDragOver, setIsDragOver] = useState(false); - const { toast } = useToastProvider(); - - const MAX_FILE_SIZE = { - bytes: 10 * 1024 * 1024, // 10 MB - text: '10MB', - }; - - const { getRootProps, getInputProps, open } = useDropzone({ - accept: { - [ContentTypesAllowed.Docx]: ['.docx'], - [ContentTypesAllowed.Markdown]: ['.md'], + const { getRootProps, getInputProps, open } = useImport({ + onDragOver: (dragOver: boolean) => { + setIsDragOver(dragOver); }, - maxSize: MAX_FILE_SIZE.bytes, - onDrop(acceptedFiles) { - setIsDragOver(false); - for (const file of acceptedFiles) { - importDoc(file); - } - }, - onDragEnter: () => { - setIsDragOver(true); - }, - onDragLeave: () => { - setIsDragOver(false); - }, - onDropRejected(fileRejections) { - fileRejections.forEach((rejection) => { - const isFileTooLarge = rejection.errors.some( - (error) => error.code === 'file-too-large', - ); - - if (isFileTooLarge) { - toast( - t( - 'The document "{{documentName}}" is too large. Maximum file size is {{maxFileSize}}.', - { - documentName: rejection.file.name, - maxFileSize: MAX_FILE_SIZE.text, - }, - ), - VariantType.ERROR, - ); - } else { - toast( - t( - `The document "{{documentName}}" import has failed (only .docx and .md files are allowed)`, - { - documentName: rejection.file.name, - }, - ), - VariantType.ERROR, - ); - } - }); - }, - noClick: true, }); - const { mutate: importDoc } = useImportDoc(); const withUpload = !target || diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/hooks/useImport.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/hooks/useImport.tsx new file mode 100644 index 00000000..9721f267 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/hooks/useImport.tsx @@ -0,0 +1,116 @@ +import { + VariantType, + useToastProvider, +} from '@gouvfr-lasuite/cunningham-react'; +import { t } from 'i18next'; +import { useMemo } from 'react'; +import { useDropzone } from 'react-dropzone'; + +import { useConfig } from '@/core'; + +import { ContentTypes, useImportDoc } from '../api/useImportDoc'; + +interface UseImportProps { + onDragOver: (isDragOver: boolean) => void; +} + +export const useImport = ({ onDragOver }: UseImportProps) => { + const { toast } = useToastProvider(); + const { data: config } = useConfig(); + + const MAX_FILE_SIZE = useMemo(() => { + const maxSizeInBytes = config?.CONVERSION_FILE_MAX_SIZE ?? 10 * 1024 * 1024; // Default to 10MB + + const units = ['bytes', 'KB', 'MB', 'GB']; + let size = maxSizeInBytes; + let unitIndex = 0; + + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex += 1; + } + + return { + bytes: maxSizeInBytes, + text: `${Math.round(size * 10) / 10}${units[unitIndex]}`, + }; + }, [config?.CONVERSION_FILE_MAX_SIZE]); + + const ACCEPT = useMemo(() => { + const extensions = config?.CONVERSION_FILE_EXTENSIONS_ALLOWED; + const accept: { [key: string]: string[] } = {}; + + if (extensions && extensions.length > 0) { + extensions.forEach((ext) => { + switch (ext.toLowerCase()) { + case '.docx': + accept[ContentTypes.Docx] = ['.docx']; + break; + case '.md': + case '.markdown': + accept[ContentTypes.Markdown] = ['.md']; + break; + default: + break; + } + }); + } else { + // Default to docx and md if no configuration is provided + accept[ContentTypes.Docx] = ['.docx']; + accept[ContentTypes.Markdown] = ['.md']; + } + + return accept; + }, [config?.CONVERSION_FILE_EXTENSIONS_ALLOWED]); + + const { getRootProps, getInputProps, open } = useDropzone({ + accept: ACCEPT, + maxSize: MAX_FILE_SIZE.bytes, + onDrop(acceptedFiles) { + onDragOver(false); + for (const file of acceptedFiles) { + importDoc([file, file.type]); + } + }, + onDragEnter: () => { + onDragOver(true); + }, + onDragLeave: () => { + onDragOver(false); + }, + onDropRejected(fileRejections) { + fileRejections.forEach((rejection) => { + const isFileTooLarge = rejection.errors.some( + (error) => error.code === 'file-too-large', + ); + + if (isFileTooLarge) { + toast( + t( + 'The document "{{documentName}}" is too large. Maximum file size is {{maxFileSize}}.', + { + documentName: rejection.file.name, + maxFileSize: MAX_FILE_SIZE.text, + }, + ), + VariantType.ERROR, + ); + } else { + toast( + t( + `The document "{{documentName}}" import has failed (only .docx and .md files are allowed)`, + { + documentName: rejection.file.name, + }, + ), + VariantType.ERROR, + ); + } + }); + }, + noClick: true, + }); + const { mutate: importDoc } = useImportDoc(); + + return { getRootProps, getInputProps, open }; +};