Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export interface BAIFileExplorerProps {
enableDownload?: boolean;
enableDelete?: boolean;
enableWrite?: boolean;
enableEdit?: boolean;
onChangeFetchKey?: (fetchKey: string) => void;
ref?: React.Ref<BAIFileExplorerRef>;
onDeleteFilesInBackground?: (
Expand All @@ -64,6 +65,7 @@ export interface BAIFileExplorerProps {
) => void;
// FIXME: need to delete when `delete-file-async` API returns deleting file paths
deletingFilePaths?: Array<string>;
onClickEditFile?: (file: VFolderFile, currentPath: string) => void;
}

const BAIFileExplorer: React.FC<BAIFileExplorerProps> = ({
Expand All @@ -75,8 +77,10 @@ const BAIFileExplorer: React.FC<BAIFileExplorerProps> = ({
enableDownload = false,
enableDelete = false,
enableWrite = false,
enableEdit = false,
onDeleteFilesInBackground,
deletingFilePaths,
onClickEditFile,
style,
ref,
}) => {
Expand Down Expand Up @@ -214,8 +218,10 @@ const BAIFileExplorer: React.FC<BAIFileExplorerProps> = ({
onClickDelete={() => {
setSelectedSingleItem(record);
}}
onClickEdit={() => onClickEditFile?.(record, currentPath)}
enableDownload={enableDownload}
enableDelete={enableDelete}
enableEdit={enableEdit}
deleteButtonProps={{ loading: isPendingDelete }}
/>
</Suspense>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,66 @@ import BAIFlex from '../../BAIFlex';
import useConnectedBAIClient from '../../provider/BAIClientProvider/hooks/useConnectedBAIClient';
import { VFolderFile } from '../../provider/BAIClientProvider/types';
import { FolderInfoContext } from './BAIFileExplorer';
import { MoreOutlined } from '@ant-design/icons';
import { useMutation } from '@tanstack/react-query';
import { App, Button, theme } from 'antd';
import { DownloadIcon } from 'lucide-react';
import { App, Button, theme, Dropdown } from 'antd';
import { DownloadIcon, EditIcon } from 'lucide-react';
import { use } from 'react';
import { useTranslation } from 'react-i18next';

interface FileItemControlsProps {
selectedItem: VFolderFile;
onClickDelete: () => void;
onClickEdit?: () => void;
enableDownload?: boolean;
enableDelete?: boolean;
enableEdit?: boolean;
downloadButtonProps?: BAIButtonProps;
deleteButtonProps?: BAIButtonProps;
}

const TEXT_FILE_EXTENSIONS = [
'.txt',
'.md',
'.json',
'.yaml',
'.yml',
'.xml',
'.csv',
'.js',
'.ts',
'.jsx',
'.tsx',
'.py',
'.sh',
'.bash',
'.html',
'.css',
'.scss',
'.less',
'.sql',
'.log',
'.env',
'.conf',
'.config',
'.ini',
'.toml',
];
Comment on lines +25 to +51
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TEXT_FILE_EXTENSIONS array is defined within the component file but could be shared across the codebase. Consider extracting this to a constants file or utility module, especially since the file type detection logic might be useful elsewhere in the application.

Copilot uses AI. Check for mistakes.

const isTextFile = (fileName: string): boolean => {
const lastDotIndex = fileName.lastIndexOf('.');
if (lastDotIndex === -1) return false;
const ext = fileName.toLowerCase().slice(lastDotIndex);
return TEXT_FILE_EXTENSIONS.includes(ext);
};

const FileItemControls: React.FC<FileItemControlsProps> = ({
selectedItem,
onClickDelete,
onClickEdit,
enableDownload = false,
enableDelete = false,
enableEdit = false,
downloadButtonProps,
deleteButtonProps,
}) => {
Expand Down Expand Up @@ -108,6 +148,37 @@ const FileItemControls: React.FC<FileItemControlsProps> = ({
}}
{...deleteButtonProps}
/>
<Dropdown
menu={{
items: [
{
key: 'fileEdit',
icon: <EditIcon size={14} />,
label: t('comp:FileExplorer.EditFile'),
disabled:
!enableEdit ||
selectedItem.type === 'DIRECTORY' ||
!isTextFile(selectedItem.name),
onClick: (e) => {
e.domEvent.stopPropagation();
onClickEdit?.();
},
},
],
}}
trigger={['click']}
>
<Button
type="text"
size="small"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
icon={<MoreOutlined />}
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The More menu button (MoreOutlined icon) lacks an accessible label. Screen reader users won't know what this button does. Consider adding an aria-label attribute to the Button component to describe its purpose, such as "File actions" or "More options".

Suggested change
icon={<MoreOutlined />}
icon={<MoreOutlined />}
aria-label={t('comp:FileExplorer.MoreOptions')}

Copilot uses AI. Check for mistakes.
style={{ color: token.colorTextSecondary }}
/>
</Dropdown>
Comment on lines +151 to +181
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The More menu dropdown currently only has one item (Edit). Consider whether a dropdown is necessary for a single action, or if this is preparation for future menu items. If it's just for the Edit action, a simpler icon button might be more appropriate. If more actions are planned, this is acceptable.

Copilot uses AI. Check for mistakes.
</BAIFlex>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export { default as BAIClientProvider } from './BAIClientProvider';
export type { BAIClientProviderProps } from './BAIClientProvider';
export { BAIClientContext, BAIAnonymousClientContext } from './context';
export type { BAIClient } from './types';
export type { BAIClient, VFolderFile } from './types';
export { default as useConnectedBAIClient } from './hooks/useConnectedBAIClient';
export { default as useAnonymousBAIClient } from './hooks/useAnonymousBAIClient';
1 change: 1 addition & 0 deletions packages/backend.ai-ui/src/locale/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"DragAndDropDesc": "Ziehen Sie Dateien in diesen Bereich zum Hochladen.",
"DuplicatedFiles": "Überschreibung der Bestätigung",
"DuplicatedFilesDesc": "Die Datei oder der Ordner mit demselben Namen existieren bereits. \nMöchten Sie überschreiben?",
"EditFile": "Datei bearbeiten",
"FolderCreatedSuccessfully": "Ordner erfolgreich erstellt.",
"FolderName": "Ordner Name",
"MaxFolderNameLength": "Der Ordnername muss 255 Zeichen oder weniger betragen.",
Expand Down
1 change: 1 addition & 0 deletions packages/backend.ai-ui/src/locale/el.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"DragAndDropDesc": "Σύρετε και αποθέστε αρχεία σε αυτήν την περιοχή για μεταφόρτωση.",
"DuplicatedFiles": "Αντιπροσώπηση επιβεβαίωσης",
"DuplicatedFilesDesc": "Το αρχείο ή ο φάκελος με το ίδιο όνομα υπάρχει ήδη. \nΘέλετε να αντικαταστήσετε;",
"EditFile": "Επεξεργασία αρχείου",
"FolderCreatedSuccessfully": "Ο φάκελος δημιούργησε με επιτυχία.",
"FolderName": "Όνομα φακέλου",
"MaxFolderNameLength": "Το όνομα του φακέλου πρέπει να είναι 255 χαρακτήρες ή λιγότερο.",
Expand Down
1 change: 1 addition & 0 deletions packages/backend.ai-ui/src/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@
"DragAndDropDesc": "Drag and drop files to this area to upload.",
"DuplicatedFiles": "Overwrite Confirmation",
"DuplicatedFilesDesc": "The file or folder with the same name already exists. Do you want to overwrite?",
"EditFile": "Edit File",
"FolderCreatedSuccessfully": "Folder created successfully.",
"FolderName": "Folder Name",
"MaxFolderNameLength": "Folder name must be 255 characters or less.",
Expand Down
1 change: 1 addition & 0 deletions packages/backend.ai-ui/src/locale/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"DragAndDropDesc": "Arrastre y suelte archivos a esta área para cargar.",
"DuplicatedFiles": "Confirmación de sobrescribencia",
"DuplicatedFilesDesc": "El archivo o carpeta con el mismo nombre ya existe. \n¿Quieres sobrescribir?",
"EditFile": "Editar archivo",
"FolderCreatedSuccessfully": "Carpeta creada con éxito.",
"FolderName": "Nombre de carpeta",
"MaxFolderNameLength": "El nombre de la carpeta debe ser de 255 caracteres o menos.",
Expand Down
1 change: 1 addition & 0 deletions packages/backend.ai-ui/src/locale/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"DragAndDropDesc": "Vedä ja pudota tiedostoja tälle alueelle ladataksesi.",
"DuplicatedFiles": "Korvata vahvistus",
"DuplicatedFilesDesc": "Tiedosto tai kansio, jolla on sama nimi, on jo olemassa. \nHaluatko korvata?",
"EditFile": "Muokkaa tiedostoa",
"FolderCreatedSuccessfully": "Kansio luotu onnistuneesti.",
"FolderName": "Kansionimi",
"MaxFolderNameLength": "Kansion nimen on oltava enintään 255 merkkiä.",
Expand Down
1 change: 1 addition & 0 deletions packages/backend.ai-ui/src/locale/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"DragAndDropDesc": "Faites glisser et déposez les fichiers dans cette zone pour télécharger.",
"DuplicatedFiles": "Écran de confirmation",
"DuplicatedFilesDesc": "Le fichier ou le dossier avec le même nom existe déjà. \nVoulez-vous écraser?",
"EditFile": "Modifier le fichier",
"FolderCreatedSuccessfully": "Dossier créé avec succès.",
"FolderName": "Nom du dossier",
"MaxFolderNameLength": "Le nom du dossier doit comporter 255 caractères ou moins.",
Expand Down
1 change: 1 addition & 0 deletions packages/backend.ai-ui/src/locale/id.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"DragAndDropDesc": "Seret dan jatuhkan file ke area ini untuk diunggah.",
"DuplicatedFiles": "Timpa konfirmasi",
"DuplicatedFilesDesc": "File atau folder dengan nama yang sama sudah ada. \nApakah Anda ingin menimpa?",
"EditFile": "Edit File",
"FolderCreatedSuccessfully": "Folder berhasil dibuat.",
"FolderName": "Nama folder",
"MaxFolderNameLength": "Nama folder harus 255 karakter atau kurang.",
Expand Down
1 change: 1 addition & 0 deletions packages/backend.ai-ui/src/locale/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"DragAndDropDesc": "Trascina i file in quest'area da caricare.",
"DuplicatedFiles": "Sovrascrivere la conferma",
"DuplicatedFilesDesc": "Il file o la cartella con lo stesso nome esiste già. \nVuoi sovrascrivere?",
"EditFile": "Modifica file",
"FolderCreatedSuccessfully": "Cartella creata correttamente.",
"FolderName": "Nome della cartella",
"MaxFolderNameLength": "Il nome della cartella deve essere di 255 caratteri o meno.",
Expand Down
1 change: 1 addition & 0 deletions packages/backend.ai-ui/src/locale/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"DragAndDropDesc": "このエリアにファイルをドラッグアンドドロップしてアップロードします。",
"DuplicatedFiles": "確認の上書き",
"DuplicatedFilesDesc": "同じ名前のファイルまたはフォルダーはすでに存在しています。\n上書きしますか?",
"EditFile": "ファイルを編集",
"FolderCreatedSuccessfully": "フォルダーは正常に作成されました。",
"FolderName": "フォルダー名",
"MaxFolderNameLength": "フォルダー名は255文字以下でなければなりません。",
Expand Down
1 change: 1 addition & 0 deletions packages/backend.ai-ui/src/locale/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"DragAndDropDesc": "파일을 이곳에 드래그하여 업로드하세요.",
"DuplicatedFiles": "덮어쓰기 확인",
"DuplicatedFilesDesc": "업로드 하려는 파일 또는 폴더가 이미 존재합니다. 덮어쓰시겠습니까?",
"EditFile": "파일 편집",
"FolderCreatedSuccessfully": "폴더가 성공적으로 생성되었습니다.",
"FolderName": "폴더 이름",
"MaxFolderNameLength": "폴더 이름은 255자 이내여야 합니다.",
Expand Down
1 change: 1 addition & 0 deletions packages/backend.ai-ui/src/locale/mn.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"DragAndDropDesc": "Байршуулахын тулд энэ газарт файлуудыг чирч, буулгана уу.",
"DuplicatedFiles": "Баталгаажуулалтыг дарж бичих",
"DuplicatedFilesDesc": "Ижил нэртэй файл эсвэл хавтас аль хэдийн байна. \nТа дарж бичихийг хүсч байна уу?",
"EditFile": "Файлыг засах",
"FolderCreatedSuccessfully": "Амжилтанд амжилттай бүтээгдсэн.",
"FolderName": "Хөдөлгөөний нэр",
"MaxFolderNameLength": "Фолдерын нэр 255 тэмдэгт буюу түүнээс бага тэмдэгт байх ёстой.",
Expand Down
1 change: 1 addition & 0 deletions packages/backend.ai-ui/src/locale/ms.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"DragAndDropDesc": "Seret dan lepaskan fail ke kawasan ini untuk dimuat naik.",
"DuplicatedFiles": "Pengesahan Pengesahan",
"DuplicatedFilesDesc": "Fail atau folder dengan nama yang sama sudah ada. \nAdakah anda mahu menimpa?",
"EditFile": "Edit Fail",
"FolderCreatedSuccessfully": "Folder dibuat dengan jayanya.",
"FolderName": "Nama folder",
"MaxFolderNameLength": "Nama folder mestilah 255 aksara atau kurang.",
Expand Down
1 change: 1 addition & 0 deletions packages/backend.ai-ui/src/locale/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"DragAndDropDesc": "Przeciągnij i upuść pliki do tego obszaru, aby przesłać.",
"DuplicatedFiles": "Nadpisz potwierdzenie",
"DuplicatedFilesDesc": "Plik lub folder o tej samej nazwie już istnieje. \nChcesz zastąpić?",
"EditFile": "Edytuj plik",
"FolderCreatedSuccessfully": "Folder utworzony pomyślnie.",
"FolderName": "Nazwa folderu",
"MaxFolderNameLength": "Nazwa folderu musi mieć 255 znaków lub mniej.",
Expand Down
1 change: 1 addition & 0 deletions packages/backend.ai-ui/src/locale/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@
"DragAndDropDesc": "Arraste e solte arquivos para esta área para fazer upload.",
"DuplicatedFiles": "Substituição de substituição",
"DuplicatedFilesDesc": "O arquivo ou pasta com o mesmo nome já existe. \nVocê quer substituir?",
"EditFile": "Editar arquivo",
"FolderCreatedSuccessfully": "Pasta criada com sucesso.",
"FolderName": "Nome da pasta",
"MaxFolderNameLength": "O nome da pasta deve ter 255 caracteres ou menos.",
Expand Down
1 change: 1 addition & 0 deletions packages/backend.ai-ui/src/locale/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"DragAndDropDesc": "Arraste e solte arquivos para esta área para fazer upload.",
"DuplicatedFiles": "Substituição de substituição",
"DuplicatedFilesDesc": "O arquivo ou pasta com o mesmo nome já existe. \nVocê quer substituir?",
"EditFile": "Editar ficheiro",
"FolderCreatedSuccessfully": "Pasta criada com sucesso.",
"FolderName": "Nome da pasta",
"MaxFolderNameLength": "O nome da pasta deve ter 255 caracteres ou menos.",
Expand Down
1 change: 1 addition & 0 deletions packages/backend.ai-ui/src/locale/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"DragAndDropDesc": "Перетащите файлы в эту область, чтобы загрузить.",
"DuplicatedFiles": "Перезаписать подтверждение",
"DuplicatedFilesDesc": "Файл или папка с тем же именем уже существует. \nВы хотите перезаписать?",
"EditFile": "Редактировать файл",
"FolderCreatedSuccessfully": "Папка создана успешно.",
"FolderName": "Имя папки",
"MaxFolderNameLength": "Имя папки должно быть 255 символов или меньше.",
Expand Down
1 change: 1 addition & 0 deletions packages/backend.ai-ui/src/locale/th.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"DragAndDropDesc": "ลากและวางไฟล์ไปยังพื้นที่นี้เพื่ออัปโหลด",
"DuplicatedFiles": "การยืนยันการเขียนทับ",
"DuplicatedFilesDesc": "ไฟล์หรือโฟลเดอร์ที่มีชื่อเดียวกันมีอยู่แล้ว \nคุณต้องการเขียนทับ?",
"EditFile": "แก้ไขไฟล์",
"FolderCreatedSuccessfully": "โฟลเดอร์สร้างสำเร็จ",
"FolderName": "ชื่อโฟลเดอร์",
"MaxFolderNameLength": "ชื่อโฟลเดอร์ต้องเป็น 255 อักขระหรือน้อยกว่า",
Expand Down
1 change: 1 addition & 0 deletions packages/backend.ai-ui/src/locale/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@
"DragAndDropDesc": "Yüklemek için dosyaları bu alana sürükleyin ve bırakın.",
"DuplicatedFiles": "Üzerine Yazın Onay",
"DuplicatedFilesDesc": "Aynı ada sahip dosya veya klasör zaten mevcuttur. \nÜzerine yazmak ister misin?",
"EditFile": "Dosyayı Düzenle",
"FolderCreatedSuccessfully": "Klasör başarıyla oluşturuldu.",
"FolderName": "Klasör adı",
"MaxFolderNameLength": "Klasör adı 255 karakter veya daha az olmalıdır.",
Expand Down
1 change: 1 addition & 0 deletions packages/backend.ai-ui/src/locale/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"DragAndDropDesc": "Kéo và thả các tập tin vào khu vực này để tải lên.",
"DuplicatedFiles": "Ghi đè xác nhận",
"DuplicatedFilesDesc": "Tệp hoặc thư mục có cùng tên đã tồn tại. \nBạn có muốn ghi đè lên?",
"EditFile": "Chỉnh sửa tệp",
"FolderCreatedSuccessfully": "Thư mục đã tạo thành công.",
"FolderName": "Tên thư mục",
"MaxFolderNameLength": "Tên thư mục phải là 255 ký tự hoặc ít hơn.",
Expand Down
1 change: 1 addition & 0 deletions packages/backend.ai-ui/src/locale/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"DragAndDropDesc": "将文件拖到此区域上传。",
"DuplicatedFiles": "覆盖确认",
"DuplicatedFilesDesc": "具有相同名称的文件或文件夹已经存在。\n你想覆盖吗?",
"EditFile": "编辑文件",
"FolderCreatedSuccessfully": "文件夹成功创建了。",
"FolderName": "文件夹名称",
"MaxFolderNameLength": "文件夹名称必须为255个字符或更小。",
Expand Down
1 change: 1 addition & 0 deletions packages/backend.ai-ui/src/locale/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@
"DragAndDropDesc": "將文件拖到此區域上傳。",
"DuplicatedFiles": "覆蓋確認",
"DuplicatedFilesDesc": "具有相同名稱的文件或文件夾已經存在。你想覆蓋嗎?",
"EditFile": "編輯檔案",
"FolderCreatedSuccessfully": "文件夾成功創建了。",
"FolderName": "文件夾名稱",
"MaxFolderNameLength": "文件夾名稱必須為255個字符或更小。",
Expand Down
28 changes: 28 additions & 0 deletions react/src/components/FolderExplorerModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useFileUploadManager } from './FileUploadManager';
import FolderExplorerHeader from './FolderExplorerHeader';
import { useFolderExplorerOpener } from './FolderExplorerOpener';
import TextFileEditorModal from './TextFileEditorModal';
import VFolderNodeDescription from './VFolderNodeDescription';
import { Alert, Divider, Grid, Skeleton, Splitter, theme } from 'antd';
import { createStyles } from 'antd-style';
Expand All @@ -12,8 +13,10 @@ import {
BAILink,
BAIModal,
BAIModalProps,
BAIUnmountAfterClose,
toGlobalId,
useInterval,
VFolderFile,
} from 'backend.ai-ui';
import _ from 'lodash';
import { Suspense, useDeferredValue, useEffect, useRef, useState } from 'react';
Expand Down Expand Up @@ -110,6 +113,10 @@ const FolderExplorerModal: React.FC<FolderExplorerProps> = ({
const { upsertNotification, closeNotification } = useSetBAINotification();
const { generateFolderPath } = useFolderExplorerOpener();
const [deletingFilePaths, setDeletingFilePaths] = useState<Array<string>>([]);
const [editingFile, setEditingFile] = useState<{
file: VFolderFile;
currentPath: string;
} | null>(null);
const { uploadStatus, uploadFiles } = useFileUploadManager(
vfolder_node?.id,
vfolder_node?.name || undefined,
Expand Down Expand Up @@ -199,6 +206,7 @@ const FolderExplorerModal: React.FC<FolderExplorerProps> = ({
enableDownload={hasDownloadContentPermission}
enableDelete={hasDeleteContentPermission}
enableWrite={hasWriteContentPermission}
enableEdit={hasWriteContentPermission}
tableProps={{
scroll: xl
? { x: 'max-content' }
Expand All @@ -208,6 +216,9 @@ const FolderExplorerModal: React.FC<FolderExplorerProps> = ({
paddingBottom: xl ? token.paddingLG : 0,
}}
fileDropContainerRef={bodyRef}
onClickEditFile={(file, currentPath) => {
setEditingFile({ file, currentPath });
}}
/>
) : null;

Expand Down Expand Up @@ -297,6 +308,23 @@ const FolderExplorerModal: React.FC<FolderExplorerProps> = ({
</BAIFlex>
)}
</Suspense>
<Suspense fallback={null}>
<BAIUnmountAfterClose>
<TextFileEditorModal
open={!!editingFile}
targetVFolderId={vfolderID}
currentPath={editingFile?.currentPath || '.'}
fileInfo={editingFile?.file || null}
uploadFiles={uploadFiles}
onRequestClose={(success) => {
if (success) {
fileExplorerRef.current?.refetch();
}
setEditingFile(null);
}}
/>
</BAIUnmountAfterClose>
</Suspense>
</BAIModal>
);
};
Expand Down
Loading
Loading