sync: update from private repo (dfadcd5f)
CI / build-and-test (push) Has been cancelled

This commit is contained in:
oss-sync
2026-06-23 06:38:48 +00:00
parent 6a2f2cc736
commit 29ccaf1e92
377 changed files with 31028 additions and 8994 deletions
+106 -7
View File
@@ -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 effecttask 切替で '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,
};
}