🛂(frontend) use max size and extension from config

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.
This commit is contained in:
Anthony LC
2026-01-21 10:30:24 +01:00
parent be995fd211
commit b8bdcbf7ed
7 changed files with 137 additions and 90 deletions

View File

@@ -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",

View File

@@ -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/",

View File

@@ -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,

View File

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

View File

@@ -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<Doc> => {
export const importDoc = async ([file, mimeType]: [
File,
string,
]): Promise<Doc> => {
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<Doc> => {
return response.json() as Promise<Doc>;
};
type UseImportDocOptions = UseMutationOptions<Doc, APIError, File>;
type UseImportDocOptions = UseMutationOptions<Doc, APIError, [File, string]>;
export function useImportDoc(props?: UseImportDocOptions) {
const { toast } = useToastProvider();
const queryClient = useQueryClient();
const { t } = useTranslation();
return useMutation<Doc, APIError, File>({
return useMutation<Doc, APIError, [File, string]>({
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,
);

View File

@@ -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 ||

View File

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