mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-08-28 19:46:41 +08:00
feat: add create file functionality with modal and context menu integration
This commit is contained in:
@@ -97,6 +97,7 @@
|
|||||||
"Home": "Home",
|
"Home": "Home",
|
||||||
"File Manager": "File Manager",
|
"File Manager": "File Manager",
|
||||||
"New Folder": "New Folder",
|
"New Folder": "New Folder",
|
||||||
|
"New File": "New File",
|
||||||
"Upload": "Upload",
|
"Upload": "Upload",
|
||||||
"Name": "Name",
|
"Name": "Name",
|
||||||
"Size": "Size",
|
"Size": "Size",
|
||||||
|
|||||||
@@ -116,6 +116,7 @@
|
|||||||
"Home": "主页",
|
"Home": "主页",
|
||||||
"File Manager": "文件管理",
|
"File Manager": "文件管理",
|
||||||
"New Folder": "新建目录",
|
"New Folder": "新建目录",
|
||||||
|
"New File": "新建文件",
|
||||||
"Upload": "上传",
|
"Upload": "上传",
|
||||||
"Name": "名称",
|
"Name": "名称",
|
||||||
"Size": "大小",
|
"Size": "大小",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { EmptyState } from './components/EmptyState';
|
|||||||
import { ContextMenu } from './components/ContextMenu';
|
import { ContextMenu } from './components/ContextMenu';
|
||||||
import { DropzoneOverlay } from './components/DropzoneOverlay';
|
import { DropzoneOverlay } from './components/DropzoneOverlay';
|
||||||
import { CreateDirModal } from './components/Modals/CreateDirModal';
|
import { CreateDirModal } from './components/Modals/CreateDirModal';
|
||||||
|
import { CreateFileModal } from './components/Modals/CreateFileModal';
|
||||||
import { RenameModal } from './components/Modals/RenameModal';
|
import { RenameModal } from './components/Modals/RenameModal';
|
||||||
import { ProcessorModal } from './components/Modals/ProcessorModal';
|
import { ProcessorModal } from './components/Modals/ProcessorModal';
|
||||||
import UploadModal from './components/Modals/UploadModal';
|
import UploadModal from './components/Modals/UploadModal';
|
||||||
@@ -49,6 +50,7 @@ const FileExplorerPage = memo(function FileExplorerPage() {
|
|||||||
|
|
||||||
// --- State for Modals ---
|
// --- State for Modals ---
|
||||||
const [creatingDir, setCreatingDir] = useState(false);
|
const [creatingDir, setCreatingDir] = useState(false);
|
||||||
|
const [creatingFile, setCreatingFile] = useState(false);
|
||||||
const [renaming, setRenaming] = useState<VfsEntry | null>(null);
|
const [renaming, setRenaming] = useState<VfsEntry | null>(null);
|
||||||
const [sharingEntries, setSharingEntries] = useState<VfsEntry[]>([]);
|
const [sharingEntries, setSharingEntries] = useState<VfsEntry[]>([]);
|
||||||
const [detailEntry, setDetailEntry] = useState<VfsEntry | null>(null);
|
const [detailEntry, setDetailEntry] = useState<VfsEntry | null>(null);
|
||||||
@@ -138,7 +140,7 @@ const FileExplorerPage = memo(function FileExplorerPage() {
|
|||||||
clearSearchSelection();
|
clearSearchSelection();
|
||||||
}, [clearSearchSelection, clearSelection]);
|
}, [clearSearchSelection, clearSelection]);
|
||||||
|
|
||||||
const { doCreateDir: doCreateDirInCurrentDir } = useFileActions({
|
const { doCreateDir: doCreateDirInCurrentDir, doCreateFile: doCreateFileInCurrentDir } = useFileActions({
|
||||||
path,
|
path,
|
||||||
refresh,
|
refresh,
|
||||||
clearSelection,
|
clearSelection,
|
||||||
@@ -343,6 +345,7 @@ const FileExplorerPage = memo(function FileExplorerPage() {
|
|||||||
|
|
||||||
{/* --- Modals & Context Menus --- */}
|
{/* --- Modals & Context Menus --- */}
|
||||||
<CreateDirModal open={creatingDir} onOk={(name) => { doCreateDirInCurrentDir(name); setCreatingDir(false); }} onCancel={() => setCreatingDir(false)} />
|
<CreateDirModal open={creatingDir} onOk={(name) => { doCreateDirInCurrentDir(name); setCreatingDir(false); }} onCancel={() => setCreatingDir(false)} />
|
||||||
|
<CreateFileModal open={creatingFile} onOk={(name) => { doCreateFileInCurrentDir(name); setCreatingFile(false); }} onCancel={() => setCreatingFile(false)} />
|
||||||
<RenameModal entry={renaming} onOk={(entry, newName) => { doRename(entry, newName); setRenaming(null); }} onCancel={() => setRenaming(null)} />
|
<RenameModal entry={renaming} onOk={(entry, newName) => { doRename(entry, newName); setRenaming(null); }} onCancel={() => setRenaming(null)} />
|
||||||
<FileDetailModal entry={detailEntry} loading={detailLoading} data={detailData} onClose={() => setDetailEntry(null)} />
|
<FileDetailModal entry={detailEntry} loading={detailLoading} data={detailData} onClose={() => setDetailEntry(null)} />
|
||||||
<MoveCopyModal
|
<MoveCopyModal
|
||||||
@@ -422,6 +425,7 @@ const FileExplorerPage = memo(function FileExplorerPage() {
|
|||||||
}}
|
}}
|
||||||
onUploadFile={openFilePicker}
|
onUploadFile={openFilePicker}
|
||||||
onUploadDirectory={openDirectoryPicker}
|
onUploadDirectory={openDirectoryPicker}
|
||||||
|
onCreateFile={() => setCreatingFile(true)}
|
||||||
onCreateDir={() => setCreatingDir(true)}
|
onCreateDir={() => setCreatingDir(true)}
|
||||||
onShare={doShare}
|
onShare={doShare}
|
||||||
onGetDirectLink={doGetDirectLink}
|
onGetDirectLink={doGetDirectLink}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useI18n } from '../../../i18n';
|
|||||||
import {
|
import {
|
||||||
FolderFilled, AppstoreOutlined, AppstoreAddOutlined, DownloadOutlined,
|
FolderFilled, AppstoreOutlined, AppstoreAddOutlined, DownloadOutlined,
|
||||||
EditOutlined, DeleteOutlined, InfoCircleOutlined, UploadOutlined, PlusOutlined,
|
EditOutlined, DeleteOutlined, InfoCircleOutlined, UploadOutlined, PlusOutlined,
|
||||||
ShareAltOutlined, LinkOutlined, CopyOutlined, SwapOutlined
|
ShareAltOutlined, LinkOutlined, CopyOutlined, SwapOutlined, FileAddOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
interface ContextMenuProps {
|
interface ContextMenuProps {
|
||||||
@@ -28,6 +28,7 @@ interface ContextMenuProps {
|
|||||||
onProcess: (entry: VfsEntry, processorType: string) => void;
|
onProcess: (entry: VfsEntry, processorType: string) => void;
|
||||||
onUploadFile: () => void;
|
onUploadFile: () => void;
|
||||||
onUploadDirectory: () => void;
|
onUploadDirectory: () => void;
|
||||||
|
onCreateFile: () => void;
|
||||||
onCreateDir: () => void;
|
onCreateDir: () => void;
|
||||||
onShare: (entries: VfsEntry[]) => void;
|
onShare: (entries: VfsEntry[]) => void;
|
||||||
onGetDirectLink: (entry: VfsEntry) => void;
|
onGetDirectLink: (entry: VfsEntry) => void;
|
||||||
@@ -70,6 +71,7 @@ export const ContextMenu: React.FC<ContextMenuProps> = (props) => {
|
|||||||
{ key: 'upload-folder', label: t('Upload Folder'), onClick: actions.onUploadDirectory },
|
{ key: 'upload-folder', label: t('Upload Folder'), onClick: actions.onUploadDirectory },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{ key: 'new-file', label: t('New File'), icon: <FileAddOutlined />, onClick: actions.onCreateFile },
|
||||||
{ key: 'mkdir', label: t('New Folder'), icon: <PlusOutlined />, onClick: actions.onCreateDir },
|
{ key: 'mkdir', label: t('New Folder'), icon: <PlusOutlined />, onClick: actions.onCreateDir },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Input, Modal } from 'antd';
|
||||||
|
import { useI18n } from '../../../../i18n';
|
||||||
|
|
||||||
|
interface CreateFileModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onOk: (name: string) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateFileModal: React.FC<CreateFileModalProps> = ({ open, onOk, onCancel }) => {
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setName('');
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const handleOk = () => {
|
||||||
|
onOk(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t('New File')}
|
||||||
|
open={open}
|
||||||
|
onOk={handleOk}
|
||||||
|
onCancel={onCancel}
|
||||||
|
okButtonProps={{ disabled: !name.trim() }}
|
||||||
|
destroyOnHidden
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder={t('Filename')}
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
onPressEnter={handleOk}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -37,6 +37,20 @@ export function useFileActions({ path, refresh, clearSelection, onShare, onGetDi
|
|||||||
}
|
}
|
||||||
}, [path, refresh, t]);
|
}, [path, refresh, t]);
|
||||||
|
|
||||||
|
const doCreateFile = useCallback(async (name: string) => {
|
||||||
|
if (!name.trim()) {
|
||||||
|
message.warning(t('Please input name'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const fullPath = (path === '/' ? '' : path) + '/' + name.trim();
|
||||||
|
await vfsApi.uploadFile(fullPath, new Blob([]));
|
||||||
|
refresh();
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e.message);
|
||||||
|
}
|
||||||
|
}, [path, refresh, t]);
|
||||||
|
|
||||||
const doDelete = useCallback(async (entries: VfsEntry[]) => {
|
const doDelete = useCallback(async (entries: VfsEntry[]) => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: t('Confirm delete {name}?', { name: entries.length > 1 ? `${entries.length} ${t('items')}` : entries[0].name }),
|
title: t('Confirm delete {name}?', { name: entries.length > 1 ? `${entries.length} ${t('items')}` : entries[0].name }),
|
||||||
@@ -193,6 +207,7 @@ export function useFileActions({ path, refresh, clearSelection, onShare, onGetDi
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
doCreateDir,
|
doCreateDir,
|
||||||
|
doCreateFile,
|
||||||
doDelete,
|
doDelete,
|
||||||
doRename,
|
doRename,
|
||||||
doDownload,
|
doDownload,
|
||||||
|
|||||||
Reference in New Issue
Block a user