🛂(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:
@@ -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",
|
||||
|
||||
@@ -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/",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
Reference in New Issue
Block a user