This commit is contained in:
@@ -1,18 +1,45 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { fetchLocalFiles, type LocalFileEntry } from '../api';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import {
|
||||
fetchLocalFiles,
|
||||
uploadLocalFiles,
|
||||
deleteLocalFiles,
|
||||
downloadLocalFilesZip,
|
||||
type LocalFileEntry,
|
||||
type WritableTaskSection,
|
||||
} from '../api';
|
||||
import { filesToBase64 } from '../lib/fileBase64';
|
||||
|
||||
type FileSection = 'workspace' | 'input' | 'output' | 'logs';
|
||||
|
||||
/** アップロード・削除が許される区分(サーバの WRITABLE_SECTIONS と一致)。 */
|
||||
function isWritableSection(section: FileSection): section is WritableTaskSection {
|
||||
return section === 'input' || section === 'output';
|
||||
}
|
||||
|
||||
export function useFileBrowser(taskId: number | null) {
|
||||
const [section, setSection] = useState<'workspace' | 'input' | 'output' | 'logs'>('workspace');
|
||||
// 初期値は reset effect(task 切替で 'output' に倒す)と一致させる。'workspace' 初期だと
|
||||
// 初回 fetch が古い section で走り、reset 後の fetch とレースになる。
|
||||
const [section, setSection] = useState<FileSection>('output');
|
||||
const [currentPath, setCurrentPath] = useState('');
|
||||
const [entries, setEntries] = useState<LocalFileEntry[]>([]);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
// fetch のリクエスト連番。古い (taskId/section/path) のレスポンスが後勝ちで entries を
|
||||
// 上書きするのを防ぐため、最新の seq の結果だけ反映する。
|
||||
const reqSeq = useRef(0);
|
||||
// 複数選択(チェック済みファイルの相対パス集合)と書込中フラグ・メッセージ。
|
||||
const [selected, setSelected] = useState<Set<string>>(() => new Set());
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const [message, setMessage] = useState<{ text: string; kind: 'ok' | 'error' } | null>(null);
|
||||
|
||||
// Fetch entries when taskId/section/path changes
|
||||
useEffect(() => {
|
||||
if (!taskId) return;
|
||||
const seq = ++reqSeq.current;
|
||||
fetchLocalFiles(taskId, section, currentPath)
|
||||
.then(r => setEntries(r.entries))
|
||||
.catch(() => setEntries([]));
|
||||
.then(r => { if (seq === reqSeq.current) setEntries(r.entries); })
|
||||
.catch(() => { if (seq === reqSeq.current) setEntries([]); });
|
||||
}, [taskId, section, currentPath]);
|
||||
|
||||
// Reset when task changes
|
||||
@@ -21,19 +48,85 @@ export function useFileBrowser(taskId: number | null) {
|
||||
setCurrentPath('');
|
||||
}, [taskId]);
|
||||
|
||||
// フォルダ移動・区分切替・タスク切替で選択をクリア(別ディレクトリのパスを持ち越さない)。
|
||||
useEffect(() => { setSelected(new Set()); }, [taskId, section, currentPath]);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!taskId) return;
|
||||
const seq = ++reqSeq.current;
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
const r = await fetchLocalFiles(taskId, section, currentPath);
|
||||
setEntries(r.entries);
|
||||
if (seq === reqSeq.current) setEntries(r.entries);
|
||||
} catch {
|
||||
setEntries([]);
|
||||
if (seq === reqSeq.current) setEntries([]);
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
}, [taskId, section, currentPath]);
|
||||
|
||||
const toggleSelect = useCallback((path: string) => {
|
||||
setSelected(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(path)) next.delete(path); else next.add(path);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const toggleSelectAll = useCallback((paths: string[]) => {
|
||||
setSelected(prev => {
|
||||
const allSelected = paths.length > 0 && paths.every(p => prev.has(p));
|
||||
return allSelected ? new Set() : new Set(paths);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const upload = useCallback(async (fileList: File[]) => {
|
||||
if (!taskId || fileList.length === 0 || !isWritableSection(section)) return;
|
||||
setIsUploading(true);
|
||||
setMessage(null);
|
||||
try {
|
||||
const payload = await filesToBase64(fileList);
|
||||
const r = await uploadLocalFiles(taskId, section, currentPath, payload);
|
||||
await refresh();
|
||||
setMessage({ text: `${r.uploaded.length} 件のファイルを追加しました`, kind: 'ok' });
|
||||
} catch (e) {
|
||||
setMessage({ text: `アップロードに失敗しました: ${(e as Error)?.message ?? ''}`, kind: 'error' });
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
}, [taskId, section, currentPath, refresh]);
|
||||
|
||||
const remove = useCallback(async (paths: string[]) => {
|
||||
if (!taskId || paths.length === 0 || !isWritableSection(section)) return;
|
||||
if (!window.confirm(`${paths.length} 件のファイルを削除しますか?この操作は取り消せません。`)) return;
|
||||
setIsDeleting(true);
|
||||
setMessage(null);
|
||||
try {
|
||||
const r = await deleteLocalFiles(taskId, section, paths);
|
||||
setSelected(new Set());
|
||||
await refresh();
|
||||
setMessage({ text: `${r.deleted.length} 件のファイルを削除しました`, kind: 'ok' });
|
||||
} catch (e) {
|
||||
setMessage({ text: `削除に失敗しました: ${(e as Error)?.message ?? ''}`, kind: 'error' });
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
}, [taskId, section, currentPath, refresh]);
|
||||
|
||||
// 選択ファイルを zip でダウンロード(read 操作、全 section 可)。
|
||||
const download = useCallback(async (paths: string[]) => {
|
||||
if (!taskId || paths.length === 0) return;
|
||||
setIsDownloading(true);
|
||||
setMessage(null);
|
||||
try {
|
||||
await downloadLocalFilesZip(taskId, section, paths);
|
||||
} catch (e) {
|
||||
setMessage({ text: `ダウンロードに失敗しました: ${(e as Error)?.message ?? ''}`, kind: 'error' });
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
}, [taskId, section]);
|
||||
|
||||
const pathSegments = currentPath ? currentPath.split('/').filter(Boolean) : [];
|
||||
|
||||
return {
|
||||
@@ -43,5 +136,11 @@ export function useFileBrowser(taskId: number | null) {
|
||||
isRefreshing,
|
||||
refresh,
|
||||
pathSegments,
|
||||
// 書込(アップロード/削除)
|
||||
writableSection: isWritableSection(section),
|
||||
selected, toggleSelect, toggleSelectAll,
|
||||
upload, remove, download,
|
||||
isUploading, isDeleting, isDownloading,
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user