Skip to content

Commit 45fc608

Browse files
committed
🛂(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.
1 parent 15e22e1 commit 45fc608

File tree

7 files changed

+140
-90
lines changed

7 files changed

+140
-90
lines changed

src/backend/core/api/viewsets.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2381,6 +2381,8 @@ def get(self, request):
23812381
"AI_FEATURE_ENABLED",
23822382
"COLLABORATION_WS_URL",
23832383
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY",
2384+
"CONVERSION_FILE_EXTENSIONS_ALLOWED",
2385+
"CONVERSION_FILE_MAX_SIZE",
23842386
"CRISP_WEBSITE_ID",
23852387
"ENVIRONMENT",
23862388
"FRONTEND_CSS_URL",

src/backend/core/tests/test_api_config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ def test_api_config(is_authenticated):
4646
"AI_FEATURE_ENABLED": False,
4747
"COLLABORATION_WS_URL": "http://testcollab/",
4848
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY": True,
49+
"CONVERSION_FILE_EXTENSIONS_ALLOWED": [".docx", ".md"],
50+
"CONVERSION_FILE_MAX_SIZE": 20971520,
4951
"CRISP_WEBSITE_ID": "123",
5052
"ENVIRONMENT": "test",
5153
"FRONTEND_CSS_URL": "http://testcss/",

src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export const CONFIG = {
88
CRISP_WEBSITE_ID: null,
99
COLLABORATION_WS_URL: 'ws://localhost:4444/collaboration/ws/',
1010
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY: true,
11+
CONVERSION_FILE_EXTENSIONS_ALLOWED: ['.docx', '.md'],
12+
CONVERSION_FILE_MAX_SIZE: 20971520,
1113
ENVIRONMENT: 'development',
1214
FRONTEND_CSS_URL: null,
1315
FRONTEND_JS_URL: null,

src/frontend/apps/impress/src/core/config/api/useConfig.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export interface ConfigResponse {
1818
AI_FEATURE_ENABLED?: boolean;
1919
COLLABORATION_WS_URL?: string;
2020
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY?: boolean;
21+
CONVERSION_FILE_EXTENSIONS_ALLOWED: string[];
22+
CONVERSION_FILE_MAX_SIZE: number;
2123
CRISP_WEBSITE_ID?: string;
2224
ENVIRONMENT: string;
2325
FRONTEND_CSS_URL?: string;

src/frontend/apps/impress/src/features/docs/docs-grid/api/useImportDoc.tsx

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,43 +17,22 @@ import {
1717
} from '@/api';
1818
import { Doc, DocsResponse, KEY_LIST_DOC } from '@/docs/doc-management';
1919

20-
enum ContentTypes {
20+
export enum ContentTypes {
2121
Docx = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
2222
Markdown = 'text/markdown',
2323
OctetStream = 'application/octet-stream',
2424
}
2525

26-
export enum ContentTypesAllowed {
27-
Docx = ContentTypes.Docx,
28-
Markdown = ContentTypes.Markdown,
29-
}
30-
31-
const getMimeType = (file: File): string => {
32-
if (file.type) {
33-
return file.type;
34-
}
35-
36-
const extension = file.name.split('.').pop()?.toLowerCase();
37-
38-
switch (extension) {
39-
case 'md':
40-
return ContentTypes.Markdown;
41-
case 'markdown':
42-
return ContentTypes.Markdown;
43-
case 'docx':
44-
return ContentTypes.Docx;
45-
default:
46-
return ContentTypes.OctetStream;
47-
}
48-
};
49-
50-
export const importDoc = async (file: File): Promise<Doc> => {
26+
export const importDoc = async ([file, mimeType]: [
27+
File,
28+
string,
29+
]): Promise<Doc> => {
5130
const form = new FormData();
5231

5332
form.append(
5433
'file',
5534
new File([file], file.name, {
56-
type: getMimeType(file),
35+
type: mimeType,
5736
lastModified: file.lastModified,
5837
}),
5938
);
@@ -71,14 +50,14 @@ export const importDoc = async (file: File): Promise<Doc> => {
7150
return response.json() as Promise<Doc>;
7251
};
7352

74-
type UseImportDocOptions = UseMutationOptions<Doc, APIError, File>;
53+
type UseImportDocOptions = UseMutationOptions<Doc, APIError, [File, string]>;
7554

7655
export function useImportDoc(props?: UseImportDocOptions) {
7756
const { toast } = useToastProvider();
7857
const queryClient = useQueryClient();
7958
const { t } = useTranslation();
8059

81-
return useMutation<Doc, APIError, File>({
60+
return useMutation<Doc, APIError, [File, string]>({
8261
mutationFn: importDoc,
8362
...props,
8463
onSuccess: (...successProps) => {
@@ -135,7 +114,7 @@ export function useImportDoc(props?: UseImportDocOptions) {
135114
onError: (...errorProps) => {
136115
toast(
137116
t(`The document "{{documentName}}" import has failed`, {
138-
documentName: errorProps?.[1].name || '',
117+
documentName: errorProps?.[1][0].name || '',
139118
}),
140119
VariantType.ERROR,
141120
);

src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGrid.tsx

Lines changed: 4 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import {
22
Button,
33
Tooltip as TooltipBase,
4-
VariantType,
5-
useToastProvider,
64
} from '@gouvfr-lasuite/cunningham-react';
75
import { useMemo, useState } from 'react';
8-
import { useDropzone } from 'react-dropzone';
96
import { useTranslation } from 'react-i18next';
107
import { InView } from 'react-intersection-observer';
118
import styled, { css } from 'styled-components';
@@ -16,7 +13,7 @@ import { DocDefaultFilter, useInfiniteDocs } from '@/docs/doc-management';
1613
import { useResponsiveStore } from '@/stores';
1714

1815
import { useInfiniteDocsTrashbin } from '../api';
19-
import { ContentTypesAllowed, useImportDoc } from '../api/useImportDoc';
16+
import { useImport } from '../hooks/useImport';
2017
import { useResponsiveDocGrid } from '../hooks/useResponsiveDocGrid';
2118

2219
import {
@@ -45,64 +42,11 @@ export const DocsGrid = ({
4542
}: DocsGridProps) => {
4643
const { t } = useTranslation();
4744
const [isDragOver, setIsDragOver] = useState(false);
48-
const { toast } = useToastProvider();
49-
50-
const MAX_FILE_SIZE = {
51-
bytes: 10 * 1024 * 1024, // 10 MB
52-
text: '10MB',
53-
};
54-
55-
const { getRootProps, getInputProps, open } = useDropzone({
56-
accept: {
57-
[ContentTypesAllowed.Docx]: ['.docx'],
58-
[ContentTypesAllowed.Markdown]: ['.md'],
59-
},
60-
maxSize: MAX_FILE_SIZE.bytes,
61-
onDrop(acceptedFiles) {
62-
setIsDragOver(false);
63-
for (const file of acceptedFiles) {
64-
importDoc(file);
65-
}
66-
},
67-
onDragEnter: () => {
68-
setIsDragOver(true);
69-
},
70-
onDragLeave: () => {
71-
setIsDragOver(false);
72-
},
73-
onDropRejected(fileRejections) {
74-
fileRejections.forEach((rejection) => {
75-
const isFileTooLarge = rejection.errors.some(
76-
(error) => error.code === 'file-too-large',
77-
);
78-
79-
if (isFileTooLarge) {
80-
toast(
81-
t(
82-
'The document "{{documentName}}" is too large. Maximum file size is {{maxFileSize}}.',
83-
{
84-
documentName: rejection.file.name,
85-
maxFileSize: MAX_FILE_SIZE.text,
86-
},
87-
),
88-
VariantType.ERROR,
89-
);
90-
} else {
91-
toast(
92-
t(
93-
`The document "{{documentName}}" import has failed (only .docx and .md files are allowed)`,
94-
{
95-
documentName: rejection.file.name,
96-
},
97-
),
98-
VariantType.ERROR,
99-
);
100-
}
101-
});
45+
const { getRootProps, getInputProps, open } = useImport({
46+
onDragOver: (dragOver: boolean) => {
47+
setIsDragOver(dragOver);
10248
},
103-
noClick: true,
10449
});
105-
const { mutate: importDoc } = useImportDoc();
10650

10751
const withUpload =
10852
!target ||
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import {
2+
VariantType,
3+
useToastProvider,
4+
} from '@gouvfr-lasuite/cunningham-react';
5+
import { t } from 'i18next';
6+
import { useMemo } from 'react';
7+
import { useDropzone } from 'react-dropzone';
8+
9+
import { useConfig } from '@/core';
10+
11+
import { ContentTypes, useImportDoc } from '../api/useImportDoc';
12+
13+
interface UseImportProps {
14+
onDragOver: (isDragOver: boolean) => void;
15+
}
16+
17+
export const useImport = ({ onDragOver }: UseImportProps) => {
18+
const { toast } = useToastProvider();
19+
const { data: config } = useConfig();
20+
21+
const MAX_FILE_SIZE = useMemo(() => {
22+
const maxSizeInBytes = config?.CONVERSION_FILE_MAX_SIZE ?? 10 * 1024 * 1024; // Default to 10MB
23+
24+
const units = ['bytes', 'KB', 'MB', 'GB'];
25+
let size = maxSizeInBytes;
26+
let unitIndex = 0;
27+
28+
while (size >= 1024 && unitIndex < units.length - 1) {
29+
size /= 1024;
30+
unitIndex += 1;
31+
}
32+
33+
return {
34+
bytes: maxSizeInBytes,
35+
text: `${Math.round(size * 10) / 10}${units[unitIndex]}`,
36+
};
37+
}, [config?.CONVERSION_FILE_MAX_SIZE]);
38+
39+
const ACCEPT = useMemo(() => {
40+
const extensions = config?.CONVERSION_FILE_EXTENSIONS_ALLOWED;
41+
const accept: { [key: string]: string[] } = {};
42+
43+
if (extensions && extensions.length > 0) {
44+
extensions.forEach((ext) => {
45+
switch (ext.toLowerCase()) {
46+
case '.docx':
47+
accept[ContentTypes.Docx] = ['.docx'];
48+
break;
49+
case '.md':
50+
case '.markdown':
51+
accept[ContentTypes.Markdown] = ['.md'];
52+
break;
53+
default:
54+
break;
55+
}
56+
});
57+
} else {
58+
// Default to docx and md if no configuration is provided
59+
accept[ContentTypes.Docx] = ['.docx'];
60+
accept[ContentTypes.Markdown] = ['.md'];
61+
}
62+
63+
return accept;
64+
}, [config?.CONVERSION_FILE_EXTENSIONS_ALLOWED]);
65+
66+
const { getRootProps, getInputProps, open } = useDropzone({
67+
accept: ACCEPT,
68+
maxSize: MAX_FILE_SIZE.bytes,
69+
onDrop(acceptedFiles) {
70+
onDragOver(false);
71+
//setIsDragOver(false);
72+
for (const file of acceptedFiles) {
73+
importDoc([file, file.type]);
74+
}
75+
},
76+
onDragEnter: () => {
77+
//setIsDragOver(true);
78+
onDragOver(true);
79+
},
80+
onDragLeave: () => {
81+
onDragOver(false);
82+
//setIsDragOver(false);
83+
},
84+
onDropRejected(fileRejections) {
85+
fileRejections.forEach((rejection) => {
86+
const isFileTooLarge = rejection.errors.some(
87+
(error) => error.code === 'file-too-large',
88+
);
89+
90+
if (isFileTooLarge) {
91+
toast(
92+
t(
93+
'The document "{{documentName}}" is too large. Maximum file size is {{maxFileSize}}.',
94+
{
95+
documentName: rejection.file.name,
96+
maxFileSize: MAX_FILE_SIZE.text,
97+
},
98+
),
99+
VariantType.ERROR,
100+
);
101+
} else {
102+
toast(
103+
t(
104+
`The document "{{documentName}}" import has failed (only .docx and .md files are allowed)`,
105+
{
106+
documentName: rejection.file.name,
107+
},
108+
),
109+
VariantType.ERROR,
110+
);
111+
}
112+
});
113+
},
114+
noClick: true,
115+
});
116+
const { mutate: importDoc } = useImportDoc();
117+
118+
return { getRootProps, getInputProps, open };
119+
};

0 commit comments

Comments
 (0)