Skip to content

Commit 6f46361

Browse files
committed
feat(FR-2022): add create file action in file browser (#5593)
Resolves #5238 (FR-2022) ## Summary - Add `CreateFileModal` component following the same pattern as `CreateDirectoryModal` - Add "Create File" button in `ExplorerActionControls` toolbar alongside the existing "Create Folder" button - Use `baiClient.vfolder.upload()` with an empty `Blob` to create zero-byte files at the current path - Add i18n translation keys for all 21 supported locale files ## Changes - `packages/backend.ai-ui/src/components/baiClient/FileExplorer/CreateFileModal.tsx` (new) — modal with file name input and validation (no path separators, max 255 chars) - `packages/backend.ai-ui/src/components/baiClient/FileExplorer/ExplorerActionControls.tsx` — add "Create File" button and wire `CreateFileModal` - `packages/backend.ai-ui/src/locale/en.json` and all other locale files — add `CreateANewFile`, `CreateFile`, `CreateFolder`, `FileName`, `FileCreatedSuccessfully`, `PleaseEnterAFileName`, `MaxFileNameLength`, `InvalidFileNameCharacters`, `FileNamePlaceholder` keys - `packages/backend.ai-ui/src/components/BAILink.test.tsx` — fix pre-existing unused import lint warning
1 parent 9798037 commit 6f46361

25 files changed

Lines changed: 65280 additions & 2 deletions
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import BAIModal, { type BAIModalProps } from '../../BAIModal';
2+
import useConnectedBAIClient from '../../provider/BAIClientProvider/hooks/useConnectedBAIClient';
3+
import { FolderInfoContext } from './BAIFileExplorer';
4+
import { useMutation } from '@tanstack/react-query';
5+
import { App, Form, Input, type FormInstance } from 'antd';
6+
import _ from 'lodash';
7+
import React, { use, useRef } from 'react';
8+
import { useTranslation } from 'react-i18next';
9+
10+
interface CreateFileModalProps extends BAIModalProps {
11+
onRequestClose: (success: boolean) => void;
12+
}
13+
14+
const CreateFileModal: React.FC<CreateFileModalProps> = ({
15+
onRequestClose,
16+
...modalProps
17+
}) => {
18+
'use memo';
19+
const { t } = useTranslation();
20+
const { message, modal } = App.useApp();
21+
const { targetVFolderId, currentPath } = use(FolderInfoContext);
22+
const baiClient = useConnectedBAIClient();
23+
const formRef = useRef<FormInstance>(null);
24+
25+
const createFileMutation = useMutation({
26+
mutationFn: async ({
27+
filePath,
28+
vfolderId,
29+
}: {
30+
filePath: string;
31+
vfolderId: string;
32+
}) => {
33+
// Use a newline (1 byte) since tus servers may reject 0-byte uploads.
34+
// A newline is preferred over a space as it makes the file show line numbers (up to 2),
35+
// making it clearer that the file has initial content.
36+
const blob = new Blob(['\n'], { type: 'application/octet-stream' });
37+
const tusUrl: string = await baiClient.vfolder.create_upload_session(
38+
filePath,
39+
blob,
40+
vfolderId,
41+
);
42+
// Complete the tus upload by sending the file content via PATCH
43+
const res = await fetch(tusUrl, {
44+
method: 'PATCH',
45+
headers: {
46+
'Tus-Resumable': '1.0.0',
47+
'Upload-Offset': '0',
48+
'Content-Type': 'application/offset+octet-stream',
49+
},
50+
body: blob,
51+
});
52+
if (!res.ok) {
53+
throw new Error(`Failed to create file: ${res.statusText}`);
54+
}
55+
return res;
56+
},
57+
});
58+
59+
const createFile = () => {
60+
formRef.current
61+
?.validateFields()
62+
.then(async (values) => {
63+
const filePath = [currentPath, values.fileName].join('/');
64+
65+
// Check for duplicate file name before creating
66+
const isDuplicate = await baiClient.vfolder
67+
.list_files(currentPath, targetVFolderId)
68+
.then((res) => _.some(res.items, (f) => f.name === values.fileName))
69+
.catch(() => false);
70+
71+
if (isDuplicate) {
72+
modal.confirm({
73+
title: t('comp:FileExplorer.DuplicatedFiles'),
74+
content: t('comp:FileExplorer.DuplicatedFilesDesc'),
75+
onOk: () => {
76+
createFileMutation
77+
.mutateAsync({ filePath, vfolderId: targetVFolderId })
78+
.then(() => {
79+
onRequestClose(true);
80+
message.success(
81+
t('comp:FileExplorer.FileCreatedSuccessfully'),
82+
);
83+
})
84+
.catch((err) => {
85+
message.error(err?.message || err?.title);
86+
});
87+
},
88+
});
89+
return;
90+
}
91+
92+
createFileMutation
93+
.mutateAsync({ filePath, vfolderId: targetVFolderId })
94+
.then(() => {
95+
onRequestClose(true);
96+
message.success(t('comp:FileExplorer.FileCreatedSuccessfully'));
97+
})
98+
.catch((err) => {
99+
message.error(err?.message || err?.title);
100+
});
101+
})
102+
.catch(() => {});
103+
};
104+
105+
return (
106+
<BAIModal
107+
title={t('comp:FileExplorer.CreateANewFile')}
108+
onCancel={() => onRequestClose(false)}
109+
okText={t('general.button.Create')}
110+
onOk={createFile}
111+
okButtonProps={{ loading: createFileMutation.isPending }}
112+
{...modalProps}
113+
width={400}
114+
>
115+
<Form ref={formRef} layout="vertical">
116+
<Form.Item
117+
name="fileName"
118+
label={t('comp:FileExplorer.FileName')}
119+
rules={[
120+
{
121+
required: true,
122+
message: t('comp:FileExplorer.PleaseEnterAFileName'),
123+
},
124+
{
125+
max: 255,
126+
message: t('comp:FileExplorer.MaxFileNameLength'),
127+
},
128+
{
129+
validator: (_, value) => {
130+
if (value && (value.includes('/') || value.includes('\\'))) {
131+
return Promise.reject(
132+
new Error(t('comp:FileExplorer.InvalidFileNameCharacters')),
133+
);
134+
}
135+
return Promise.resolve();
136+
},
137+
},
138+
]}
139+
>
140+
<Input placeholder={t('comp:FileExplorer.FileNamePlaceholder')} />
141+
</Form.Item>
142+
</Form>
143+
</BAIModal>
144+
);
145+
};
146+
147+
export default CreateFileModal;

packages/backend.ai-ui/src/components/baiClient/FileExplorer/ExplorerActionControls.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { BAITrashBinIcon } from '../../../icons';
22
import BAIFlex from '../../BAIFlex';
33
import { VFolderFile } from '../../provider/BAIClientProvider/types';
44
import CreateDirectoryModal from './CreateDirectoryModal';
5+
import CreateFileModal from './CreateFileModal';
56
import DeleteSelectedItemsModal, {
67
DeleteSelectedItemsModalProps,
78
} from './DeleteSelectedItemsModal';
@@ -62,6 +63,8 @@ const ExplorerActionControls: React.FC<ExplorerActionControlsProps> = ({
6263
const [openUploadDropdown, { toggle: toggleUploadDropdown }] =
6364
useToggle(false);
6465
const [openCreateModal, { toggle: toggleCreateModal }] = useToggle(false);
66+
const [openCreateFileModal, { toggle: toggleCreateFileModal }] =
67+
useToggle(false);
6568
const [openDeleteModal, { toggle: toggleDeleteModal }] = useToggle(false);
6669
const lastFileListRef = useRef<Array<RcFile>>([]);
6770

@@ -84,15 +87,26 @@ const ExplorerActionControls: React.FC<ExplorerActionControlsProps> = ({
8487
</Tooltip>
8588
</>
8689
)}
87-
<Tooltip title={!lg && t('general.button.Create')}>
90+
<Tooltip title={!lg && t('comp:FileExplorer.CreateFolder')}>
8891
<Button
8992
disabled={!enableWrite}
9093
icon={<FolderAddOutlined />}
9194
onClick={() => {
9295
toggleCreateModal();
9396
}}
9497
>
95-
{lg && t('general.button.Create')}
98+
{lg && t('comp:FileExplorer.CreateFolder')}
99+
</Button>
100+
</Tooltip>
101+
<Tooltip title={!lg && t('comp:FileExplorer.CreateFile')}>
102+
<Button
103+
disabled={!enableWrite}
104+
icon={<FileAddOutlined />}
105+
onClick={() => {
106+
toggleCreateFileModal();
107+
}}
108+
>
109+
{lg && t('comp:FileExplorer.CreateFile')}
96110
</Button>
97111
</Tooltip>
98112
<Dropdown
@@ -184,6 +198,16 @@ const ExplorerActionControls: React.FC<ExplorerActionControlsProps> = ({
184198
toggleCreateModal();
185199
}}
186200
/>
201+
<CreateFileModal
202+
destroyOnHidden
203+
open={openCreateFileModal}
204+
onRequestClose={(success: boolean) => {
205+
if (success) {
206+
onRequestClose(true);
207+
}
208+
toggleCreateFileModal();
209+
}}
210+
/>
187211
{extra}
188212
</BAIFlex>
189213
);

packages/backend.ai-ui/src/locale/de.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,10 @@
236236
"ChangeFileExtension": "Dateierweiterung ändern",
237237
"ChangeFileExtensionDesc": "Das Ändern der Dateierweiterung kann dazu führen, dass die Datei unbrauchbar oder falsch geöffnet wird. \nMöchten Sie fortfahren?",
238238
"Controls": "Kontrollen",
239+
"CreateANewFile": "Create a new file",
239240
"CreateANewFolder": "Erstellen Sie einen neuen Ordner",
241+
"CreateFile": "Create File",
242+
"CreateFolder": "Create Folder",
240243
"CreatedAt": "Erstellt at",
241244
"DeleteSelectedItemDesc": "Löschte Dateien und Ordner können nicht wiederhergestellt werden. \nMöchten Sie fortfahren?",
242245
"DeleteSelectedItemsDialog": "Bestätigung löschen",
@@ -245,13 +248,19 @@
245248
"DuplicatedFiles": "Überschreibung der Bestätigung",
246249
"DuplicatedFilesDesc": "Die Datei oder der Ordner mit demselben Namen existieren bereits. \nMöchten Sie überschreiben?",
247250
"EditFile": "Datei bearbeiten",
251+
"FileCreatedSuccessfully": "File created successfully.",
252+
"FileName": "File Name",
253+
"FileNamePlaceholder": "e.g. model-definition.yaml",
248254
"FileTooLargeToEdit": "Die Bearbeitung im Browser ist eingeschränkt, da die Dateigröße {{size}}MB überschreitet.",
249255
"FolderCreatedSuccessfully": "Ordner erfolgreich erstellt.",
250256
"FolderName": "Ordner Name",
257+
"InvalidFileNameCharacters": "File name must not contain path separators (/ or \\\\).",
258+
"MaxFileNameLength": "File name must be 255 characters or less.",
251259
"MaxFolderNameLength": "Der Ordnername muss 255 Zeichen oder weniger betragen.",
252260
"ModifiedAt": "Modifiziert bei",
253261
"MoreOptions": "Weitere Optionen",
254262
"Name": "Name",
263+
"PleaseEnterAFileName": "Please enter the file name.",
255264
"PleaseEnterAFolderName": "Bitte geben Sie den Ordneramen ein.",
256265
"RenameSuccess": "Der Name wurde erfolgreich geändert.",
257266
"SelectedItemsDeletedSuccessfully": "Ausgewählte Dateien und Ordner wurden erfolgreich gelöscht.",

packages/backend.ai-ui/src/locale/el.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,10 @@
236236
"ChangeFileExtension": "Αλλαγή επέκτασης αρχείου",
237237
"ChangeFileExtensionDesc": "Η αλλαγή της επέκτασης του αρχείου μπορεί να προκαλέσει λανθασμένα το αρχείο. \nΘέλετε να προχωρήσετε;",
238238
"Controls": "Χειριστήρια",
239+
"CreateANewFile": "Create a new file",
239240
"CreateANewFolder": "Δημιουργήστε ένα νέο φάκελο",
241+
"CreateFile": "Create File",
242+
"CreateFolder": "Create Folder",
240243
"CreatedAt": "Δημιουργήθηκε στο",
241244
"DeleteSelectedItemDesc": "Τα διαγραμμένα αρχεία και οι φάκελοι δεν μπορούν να αποκατασταθούν. \nΘέλετε να προχωρήσετε;",
242245
"DeleteSelectedItemsDialog": "Διαγραφή επιβεβαίωσης",
@@ -245,13 +248,19 @@
245248
"DuplicatedFiles": "Αντιπροσώπηση επιβεβαίωσης",
246249
"DuplicatedFilesDesc": "Το αρχείο ή ο φάκελος με το ίδιο όνομα υπάρχει ήδη. \nΘέλετε να αντικαταστήσετε;",
247250
"EditFile": "Επεξεργασία αρχείου",
251+
"FileCreatedSuccessfully": "File created successfully.",
252+
"FileName": "File Name",
253+
"FileNamePlaceholder": "e.g. model-definition.yaml",
248254
"FileTooLargeToEdit": "Η επεξεργασία στο πρόγραμμα περιήγησης είναι περιορισμένη επειδή το μέγεθος του αρχείου υπερβαίνει τα {{size}}MB.",
249255
"FolderCreatedSuccessfully": "Ο φάκελος δημιούργησε με επιτυχία.",
250256
"FolderName": "Όνομα φακέλου",
257+
"InvalidFileNameCharacters": "File name must not contain path separators (/ or \\\\).",
258+
"MaxFileNameLength": "File name must be 255 characters or less.",
251259
"MaxFolderNameLength": "Το όνομα του φακέλου πρέπει να είναι 255 χαρακτήρες ή λιγότερο.",
252260
"ModifiedAt": "Τροποποιημένος",
253261
"MoreOptions": "Περισσότερες επιλογές",
254262
"Name": "Ονομα",
263+
"PleaseEnterAFileName": "Please enter the file name.",
255264
"PleaseEnterAFolderName": "Εισαγάγετε το όνομα του φακέλου.",
256265
"RenameSuccess": "Το όνομα έχει αλλάξει με επιτυχία.",
257266
"SelectedItemsDeletedSuccessfully": "Επιλεγμένα αρχεία και φακέλους έχουν διαγραφεί με επιτυχία.",

packages/backend.ai-ui/src/locale/en.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,10 @@
239239
"ChangeFileExtension": "Change File Extension",
240240
"ChangeFileExtensionDesc": "Changing the file extension may cause the file to become unusable or open incorrectly. Do you want to proceed?",
241241
"Controls": "Controls",
242+
"CreateANewFile": "Create a new file",
242243
"CreateANewFolder": "Create a new folder",
244+
"CreateFile": "Create File",
245+
"CreateFolder": "Create Folder",
243246
"CreatedAt": "Created At",
244247
"DeleteSelectedItemDesc": "Deleted files and folders cannot be restored. Do you want to proceed?",
245248
"DeleteSelectedItemsDialog": "Delete Confirmation",
@@ -248,13 +251,19 @@
248251
"DuplicatedFiles": "Overwrite Confirmation",
249252
"DuplicatedFilesDesc": "The file or folder with the same name already exists. Do you want to overwrite?",
250253
"EditFile": "Edit File",
254+
"FileCreatedSuccessfully": "File created successfully.",
255+
"FileName": "File Name",
256+
"FileNamePlaceholder": "e.g. model-definition.yaml",
251257
"FileTooLargeToEdit": "In-browser editing is restricted because the file size exceeds {{size}}MB.",
252258
"FolderCreatedSuccessfully": "Folder created successfully.",
253259
"FolderName": "Folder Name",
260+
"InvalidFileNameCharacters": "File name must not contain path separators (/ or \\).",
261+
"MaxFileNameLength": "File name must be 255 characters or less.",
254262
"MaxFolderNameLength": "Folder name must be 255 characters or less.",
255263
"ModifiedAt": "Modified At",
256264
"MoreOptions": "More options",
257265
"Name": "Name",
266+
"PleaseEnterAFileName": "Please enter the file name.",
258267
"PleaseEnterAFolderName": "Please enter the folder name.",
259268
"RenameSuccess": "The name has been successfully changed.",
260269
"SelectedItemsDeletedSuccessfully": "Selected files and folders have been deleted successfully.",

packages/backend.ai-ui/src/locale/es.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,10 @@
236236
"ChangeFileExtension": "Cambiar la extensión del archivo",
237237
"ChangeFileExtensionDesc": "Cambiar la extensión del archivo puede hacer que el archivo se vuelva inutilizable o se abra incorrectamente. \n¿Quieres continuar?",
238238
"Controls": "Control",
239+
"CreateANewFile": "Create a new file",
239240
"CreateANewFolder": "Crea una nueva carpeta",
241+
"CreateFile": "Create File",
242+
"CreateFolder": "Create Folder",
240243
"CreatedAt": "Creado a",
241244
"DeleteSelectedItemDesc": "Los archivos y carpetas eliminados no se pueden restaurar. \n¿Quieres continuar?",
242245
"DeleteSelectedItemsDialog": "Eliminar confirmación",
@@ -245,13 +248,19 @@
245248
"DuplicatedFiles": "Confirmación de sobrescribencia",
246249
"DuplicatedFilesDesc": "El archivo o carpeta con el mismo nombre ya existe. \n¿Quieres sobrescribir?",
247250
"EditFile": "Editar archivo",
251+
"FileCreatedSuccessfully": "File created successfully.",
252+
"FileName": "File Name",
253+
"FileNamePlaceholder": "e.g. model-definition.yaml",
248254
"FileTooLargeToEdit": "La edición en el navegador está restringida porque el tamaño del archivo excede {{size}}MB.",
249255
"FolderCreatedSuccessfully": "Carpeta creada con éxito.",
250256
"FolderName": "Nombre de carpeta",
257+
"InvalidFileNameCharacters": "File name must not contain path separators (/ or \\\\).",
258+
"MaxFileNameLength": "File name must be 255 characters or less.",
251259
"MaxFolderNameLength": "El nombre de la carpeta debe ser de 255 caracteres o menos.",
252260
"ModifiedAt": "Modificado en",
253261
"MoreOptions": "Más opciones",
254262
"Name": "Nombre",
263+
"PleaseEnterAFileName": "Please enter the file name.",
255264
"PleaseEnterAFolderName": "Ingrese el nombre de la carpeta.",
256265
"RenameSuccess": "El nombre ha sido cambiado con éxito.",
257266
"SelectedItemsDeletedSuccessfully": "Los archivos y carpetas seleccionados se han eliminado correctamente.",

packages/backend.ai-ui/src/locale/fi.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,10 @@
236236
"ChangeFileExtension": "Vaihda tiedoston laajennus",
237237
"ChangeFileExtensionDesc": "Tiedoston laajennuksen muuttaminen voi aiheuttaa tiedoston käyttökelvottoman tai avoimen väärin. \nHaluatko edetä?",
238238
"Controls": "Hallintalaitteet",
239+
"CreateANewFile": "Create a new file",
239240
"CreateANewFolder": "Luo uusi kansio",
241+
"CreateFile": "Create File",
242+
"CreateFolder": "Create Folder",
240243
"CreatedAt": "Luotu jhk",
241244
"DeleteSelectedItemDesc": "Poistettuja tiedostoja ja kansioita ei voida palauttaa. \nHaluatko edetä?",
242245
"DeleteSelectedItemsDialog": "Poista vahvistus",
@@ -245,13 +248,19 @@
245248
"DuplicatedFiles": "Korvata vahvistus",
246249
"DuplicatedFilesDesc": "Tiedosto tai kansio, jolla on sama nimi, on jo olemassa. \nHaluatko korvata?",
247250
"EditFile": "Muokkaa tiedostoa",
251+
"FileCreatedSuccessfully": "File created successfully.",
252+
"FileName": "File Name",
253+
"FileNamePlaceholder": "e.g. model-definition.yaml",
248254
"FileTooLargeToEdit": "Selaimessa muokkaaminen on rajoitettu, koska tiedostokoko ylittää {{size}}MB.",
249255
"FolderCreatedSuccessfully": "Kansio luotu onnistuneesti.",
250256
"FolderName": "Kansionimi",
257+
"InvalidFileNameCharacters": "File name must not contain path separators (/ or \\\\).",
258+
"MaxFileNameLength": "File name must be 255 characters or less.",
251259
"MaxFolderNameLength": "Kansion nimen on oltava enintään 255 merkkiä.",
252260
"ModifiedAt": "Muutettu",
253261
"MoreOptions": "Lisää vaihtoehtoja",
254262
"Name": "Nimi",
263+
"PleaseEnterAFileName": "Please enter the file name.",
255264
"PleaseEnterAFolderName": "Anna kansionimi.",
256265
"RenameSuccess": "Nimi on muutettu onnistuneesti.",
257266
"SelectedItemsDeletedSuccessfully": "Valitut tiedostot ja kansiot on poistettu onnistuneesti.",

packages/backend.ai-ui/src/locale/fr.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,10 @@
236236
"ChangeFileExtension": "Modifier l'extension du fichier",
237237
"ChangeFileExtensionDesc": "La modification de l'extension du fichier peut faire en sorte que le fichier devienne inutilisable ou s'ouvre incorrectement. \nVoulez-vous continuer?",
238238
"Controls": "Commandes",
239+
"CreateANewFile": "Create a new file",
239240
"CreateANewFolder": "Créer un nouveau dossier",
241+
"CreateFile": "Create File",
242+
"CreateFolder": "Create Folder",
240243
"CreatedAt": "Créé à",
241244
"DeleteSelectedItemDesc": "Les fichiers et les dossiers supprimés ne peuvent pas être restaurés. \nVoulez-vous continuer?",
242245
"DeleteSelectedItemsDialog": "Supprimer la confirmation",
@@ -245,13 +248,19 @@
245248
"DuplicatedFiles": "Écran de confirmation",
246249
"DuplicatedFilesDesc": "Le fichier ou le dossier avec le même nom existe déjà. \nVoulez-vous écraser?",
247250
"EditFile": "Modifier le fichier",
251+
"FileCreatedSuccessfully": "File created successfully.",
252+
"FileName": "File Name",
253+
"FileNamePlaceholder": "e.g. model-definition.yaml",
248254
"FileTooLargeToEdit": "La modification dans le navigateur est limitée car la taille du fichier dépasse {{size}}Mo.",
249255
"FolderCreatedSuccessfully": "Dossier créé avec succès.",
250256
"FolderName": "Nom du dossier",
257+
"InvalidFileNameCharacters": "File name must not contain path separators (/ or \\\\).",
258+
"MaxFileNameLength": "File name must be 255 characters or less.",
251259
"MaxFolderNameLength": "Le nom du dossier doit comporter 255 caractères ou moins.",
252260
"ModifiedAt": "Modifié à",
253261
"MoreOptions": "Plus d'options",
254262
"Name": "Nom",
263+
"PleaseEnterAFileName": "Please enter the file name.",
255264
"PleaseEnterAFolderName": "Veuillez saisir le nom du dossier.",
256265
"RenameSuccess": "Le nom a été modifié avec succès.",
257266
"SelectedItemsDeletedSuccessfully": "Les fichiers et dossiers sélectionnés ont été supprimés avec succès.",

0 commit comments

Comments
 (0)