feat(web): add batch delete for selected design files (#771)

Adds batch deletion for selected design files.
This commit is contained in:
yinjialu 2026-05-07 20:03:13 +08:00 committed by GitHub
parent bc9a49ff48
commit 168cb8ab4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 120 additions and 0 deletions

View file

@ -16,6 +16,7 @@ interface Props {
onOpenFile: (name: string) => void;
onOpenLiveArtifact: (tabId: LiveArtifactWorkspaceEntry['tabId']) => void;
onDeleteFile: (name: string) => void;
onDeleteFiles: (names: string[]) => Promise<void> | void;
onUpload: () => void;
onUploadFiles: (files: File[]) => void;
onPaste: () => void;
@ -50,6 +51,7 @@ export function DesignFilesPanel({
onOpenFile,
onOpenLiveArtifact,
onDeleteFile,
onDeleteFiles,
onUpload,
onUploadFiles,
onPaste,
@ -67,6 +69,7 @@ export function DesignFilesPanel({
const [sectionLimits, setSectionLimits] = useState<Partial<Record<Section, number>>>({});
const [isSectionExpansionPending, startSectionExpansion] = useTransition();
const [selected, setSelected] = useState<Set<string>>(new Set());
const [deleting, setDeleting] = useState(false);
const grouped = useMemo(() => {
const groups: Record<Section, ProjectFile[]> = {
@ -160,6 +163,22 @@ export function DesignFilesPanel({
});
}
async function handleBatchDelete() {
if (deleting) return;
const fileList = [...selected];
if (fileList.length === 0) return;
setDeleting(true);
try {
await onDeleteFiles(fileList);
// Don't clear `selected` here: confirm-cancel and all-fail paths
// should leave the user's selection intact for retry. The
// `useEffect` above prunes successfully-deleted names automatically
// once `files` refreshes.
} finally {
setDeleting(false);
}
}
async function handleBatchDownload() {
const fileList = [...selected];
if (fileList.length === 0) return;
@ -239,6 +258,16 @@ export function DesignFilesPanel({
<Icon name="download" size={13} />
<span>{t('designFiles.downloadSelected', { n: selected.size })}</span>
</button>
<button
type="button"
className="danger"
data-testid="design-files-batch-delete"
disabled={deleting}
onClick={() => void handleBatchDelete()}
title={t('designFiles.deleteSelected', { n: selected.size })}
>
<span>{t('designFiles.deleteSelected', { n: selected.size })}</span>
</button>
</div>
) : (
<div className="df-actions">

View file

@ -301,6 +301,40 @@ export function FileWorkspace({
}
}
async function handleDeleteMany(names: string[]) {
if (names.length === 0) return;
if (!confirm(t('workspace.deleteSelectedFilesConfirm', { n: names.length }))) return;
const deleted: string[] = [];
const failed: string[] = [];
for (const name of names) {
const ok = await deleteProjectFile(projectId, name);
if (ok) deleted.push(name);
else failed.push(name);
}
if (deleted.length > 0) {
await onRefreshFiles();
const deletedSet = new Set(deleted);
const nextTabs = persistedTabs.filter((n) => !deletedSet.has(n));
if (activeTab && deletedSet.has(activeTab)) {
const nextActive = nextTabs[nextTabs.length - 1] ?? null;
onTabsStateChange({ tabs: nextTabs, active: nextActive });
setActiveTab(nextActive ?? DESIGN_FILES_TAB);
} else {
const nextActive =
tabsState.active && deletedSet.has(tabsState.active) ? null : tabsState.active;
onTabsStateChange({ tabs: nextTabs, active: nextActive });
}
setSketches((curr) => {
const next = { ...curr };
for (const name of deleted) delete next[name];
return next;
});
}
if (failed.length > 0) {
alert(t('workspace.deleteSelectedFilesPartial', { n: failed.length }));
}
}
function startNewSketch() {
const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
const name = `sketch-${stamp}.sketch.json`;
@ -483,6 +517,7 @@ export function FileWorkspace({
onOpenFile={openFile}
onOpenLiveArtifact={(tabId) => openFile(tabId)}
onDeleteFile={(name) => void handleDelete(name)}
onDeleteFiles={handleDeleteMany}
onUpload={() => fileInputRef.current?.click()}
onUploadFiles={(picked) => void uploadFiles(picked)}
onPaste={() => setShowPasteDialog(true)}

View file

@ -511,6 +511,8 @@ export const ar: Dict = {
'workspace.showChat': 'Show chat',
'workspace.closeTab': 'إغلاق علامة التبويب',
'workspace.deleteFileConfirm': 'حذف "{name}" من مجلد المشروع؟',
'workspace.deleteSelectedFilesConfirm': 'حذف {n} ملف(ات) محددة من مجلد المشروع؟',
'workspace.deleteSelectedFilesPartial': 'فشل حذف {n} ملف(ات).',
'workspace.openFromDesignFiles': 'فتح ملف من',
'workspace.designFilesLink': 'ملفات التصميم',
'workspace.loadingSketch': 'جاري تحميل الرسم...',
@ -530,6 +532,7 @@ export const ar: Dict = {
'designFiles.openInTab': 'فتح في علامة تبويب',
'designFiles.download': 'تحميل',
'designFiles.downloadSelected': 'Download {n} as ZIP',
'designFiles.deleteSelected': 'حذف {n}',
'designFiles.clearSelection': 'Clear',
'designFiles.selectAll': 'Select all',
'designFiles.dropTitle': '⤓ أسقط الملفات هنا',

View file

@ -465,6 +465,8 @@ export const de: Dict = {
'workspace.showChat': 'Show chat',
'workspace.closeTab': 'Tab schließen',
'workspace.deleteFileConfirm': '„{name}“ aus dem Projektordner löschen?',
'workspace.deleteSelectedFilesConfirm': '{n} ausgewählte Datei(en) aus dem Projektordner löschen?',
'workspace.deleteSelectedFilesPartial': '{n} Datei(en) konnten nicht gelöscht werden.',
'workspace.openFromDesignFiles': 'Datei öffnen aus',
'workspace.designFilesLink': 'Design-Dateien',
'workspace.loadingSketch': 'Sketch wird geladen…',
@ -484,6 +486,7 @@ export const de: Dict = {
'designFiles.openInTab': 'In Tab öffnen',
'designFiles.download': 'Herunterladen',
'designFiles.downloadSelected': 'Download {n} as ZIP',
'designFiles.deleteSelected': '{n} löschen',
'designFiles.clearSelection': 'Clear',
'designFiles.selectAll': 'Select all',
'designFiles.dropTitle': '⤓ Dateien hier ablegen',

View file

@ -522,6 +522,8 @@ export const en: Dict = {
'workspace.showChat': 'Show chat',
'workspace.closeTab': 'Close tab',
'workspace.deleteFileConfirm': 'Delete "{name}" from the project folder?',
'workspace.deleteSelectedFilesConfirm': 'Delete {n} selected file(s) from the project folder?',
'workspace.deleteSelectedFilesPartial': 'Failed to delete {n} file(s).',
'workspace.openFromDesignFiles': 'Open a file from',
'workspace.designFilesLink': 'Design Files',
'workspace.loadingSketch': 'Loading sketch…',
@ -541,6 +543,7 @@ export const en: Dict = {
'designFiles.openInTab': 'Open in tab',
'designFiles.download': 'Download',
'designFiles.downloadSelected': 'Download {n} as ZIP',
'designFiles.deleteSelected': 'Delete {n}',
'designFiles.clearSelection': 'Clear',
'designFiles.selectAll': 'Select all',
'designFiles.dropTitle': '⤓ Drop files here',

View file

@ -466,6 +466,8 @@ export const esES: Dict = {
'workspace.showChat': 'Show chat',
'workspace.closeTab': 'Cerrar pestaña',
'workspace.deleteFileConfirm': '¿Eliminar «{name}» de la carpeta del proyecto?',
'workspace.deleteSelectedFilesConfirm': '¿Eliminar {n} archivo(s) seleccionado(s) de la carpeta del proyecto?',
'workspace.deleteSelectedFilesPartial': 'No se pudieron eliminar {n} archivo(s).',
'workspace.openFromDesignFiles': 'Abre un archivo desde',
'workspace.designFilesLink': 'Archivos de diseño',
'workspace.loadingSketch': 'Cargando boceto…',
@ -485,6 +487,7 @@ export const esES: Dict = {
'designFiles.openInTab': 'Abrir en pestaña',
'designFiles.download': 'Descargar',
'designFiles.downloadSelected': 'Download {n} as ZIP',
'designFiles.deleteSelected': 'Eliminar {n}',
'designFiles.clearSelection': 'Clear',
'designFiles.selectAll': 'Select all',
'designFiles.dropTitle': '⤓ Suelta archivos aquí',

View file

@ -522,6 +522,8 @@ export const fa: Dict = {
'workspace.showChat': 'Show chat',
'workspace.closeTab': 'بستن تب',
'workspace.deleteFileConfirm': 'آیا «{name}» از پوشه پروژه حذف شود؟',
'workspace.deleteSelectedFilesConfirm': 'آیا {n} فایل انتخاب‌شده از پوشه پروژه حذف شوند؟',
'workspace.deleteSelectedFilesPartial': 'حذف {n} فایل ناموفق بود.',
'workspace.openFromDesignFiles': 'باز کردن یک فایل از',
'workspace.designFilesLink': 'فایل‌های طراحی',
'workspace.loadingSketch': 'در حال بارگذاری طرح…',
@ -541,6 +543,7 @@ export const fa: Dict = {
'designFiles.openInTab': 'باز کردن در تب',
'designFiles.download': 'دانلود',
'designFiles.downloadSelected': 'Download {n} as ZIP',
'designFiles.deleteSelected': 'حذف {n}',
'designFiles.clearSelection': 'Clear',
'designFiles.selectAll': 'Select all',
'designFiles.dropTitle': '⤓ فایل‌ها را اینجا رها کنید',

View file

@ -511,6 +511,8 @@ export const fr: Dict = {
'workspace.showChat': 'Show chat',
'workspace.closeTab': 'Fermer l\'onglet',
'workspace.deleteFileConfirm': 'Supprimer « {name} » du dossier du projet ?',
'workspace.deleteSelectedFilesConfirm': 'Supprimer les {n} fichier(s) sélectionné(s) du dossier du projet ?',
'workspace.deleteSelectedFilesPartial': 'Échec de la suppression de {n} fichier(s).',
'workspace.openFromDesignFiles': 'Ouvrir un fichier depuis',
'workspace.designFilesLink': 'Fichiers de design',
'workspace.loadingSketch': 'Chargement du croquis…',
@ -530,6 +532,7 @@ export const fr: Dict = {
'designFiles.openInTab': 'Ouvrir dans un onglet',
'designFiles.download': 'Télécharger',
'designFiles.downloadSelected': 'Download {n} as ZIP',
'designFiles.deleteSelected': 'Supprimer {n}',
'designFiles.clearSelection': 'Clear',
'designFiles.selectAll': 'Select all',
'designFiles.dropTitle': '⤓ Déposez les fichiers ici',

View file

@ -511,6 +511,8 @@ export const hu: Dict = {
'workspace.showChat': 'Show chat',
'workspace.closeTab': 'Lap bezárása',
'workspace.deleteFileConfirm': 'Törlöd a(z) „{name}" fájlt a projektmappából?',
'workspace.deleteSelectedFilesConfirm': 'Törlöd a(z) {n} kijelölt fájlt a projektmappából?',
'workspace.deleteSelectedFilesPartial': '{n} fájl törlése sikertelen.',
'workspace.openFromDesignFiles': 'Nyiss meg egy fájlt innen:',
'workspace.designFilesLink': 'Designfájlok',
'workspace.loadingSketch': 'Vázlat betöltése…',
@ -530,6 +532,7 @@ export const hu: Dict = {
'designFiles.openInTab': 'Megnyitás lapon',
'designFiles.download': 'Letöltés',
'designFiles.downloadSelected': 'Download {n} as ZIP',
'designFiles.deleteSelected': '{n} törlése',
'designFiles.clearSelection': 'Clear',
'designFiles.selectAll': 'Select all',
'designFiles.dropTitle': '⤓ Húzd ide a fájlokat',

View file

@ -511,6 +511,8 @@ export const id: Dict = {
'workspace.showChat': 'Tampilkan chat',
'workspace.closeTab': 'Tutup tab',
'workspace.deleteFileConfirm': 'Hapus "{name}" dari folder proyek?',
'workspace.deleteSelectedFilesConfirm': 'Hapus {n} file terpilih dari folder proyek?',
'workspace.deleteSelectedFilesPartial': 'Gagal menghapus {n} file.',
'workspace.openFromDesignFiles': 'Buka file dari',
'workspace.designFilesLink': 'File desain',
'workspace.loadingSketch': 'Memuat sketsa...',
@ -530,6 +532,7 @@ export const id: Dict = {
'designFiles.openInTab': 'Buka di tab',
'designFiles.download': 'Unduh',
'designFiles.downloadSelected': 'Unduh {n} sebagai ZIP',
'designFiles.deleteSelected': 'Hapus {n}',
'designFiles.clearSelection': 'Bersihkan',
'designFiles.selectAll': 'Pilih semua',
'designFiles.dropTitle': 'Lepaskan file di sini',

View file

@ -464,6 +464,8 @@ export const ja: Dict = {
'workspace.showChat': 'Show chat',
'workspace.closeTab': 'タブを閉じる',
'workspace.deleteFileConfirm': 'プロジェクトフォルダーから "{name}" を削除しますか?',
'workspace.deleteSelectedFilesConfirm': 'プロジェクトフォルダーから選択した {n} 個のファイルを削除しますか?',
'workspace.deleteSelectedFilesPartial': '{n} 個のファイルの削除に失敗しました。',
'workspace.openFromDesignFiles': 'ファイルを開く: ',
'workspace.designFilesLink': 'デザインファイル',
'workspace.loadingSketch': 'スケッチを読み込み中…',
@ -483,6 +485,7 @@ export const ja: Dict = {
'designFiles.openInTab': 'タブで開く',
'designFiles.download': 'ダウンロード',
'designFiles.downloadSelected': 'Download {n} as ZIP',
'designFiles.deleteSelected': '{n} 件を削除',
'designFiles.clearSelection': 'Clear',
'designFiles.selectAll': 'Select all',
'designFiles.dropTitle': '⤓ ファイルをここにドロップ',

View file

@ -511,6 +511,8 @@ export const ko: Dict = {
'workspace.showChat': 'Show chat',
'workspace.closeTab': '탭 닫기',
'workspace.deleteFileConfirm': '프로젝트 폴더에서 "{name}" 파일을 삭제하시겠습니까?',
'workspace.deleteSelectedFilesConfirm': '프로젝트 폴더에서 선택한 {n}개 파일을 삭제하시겠습니까?',
'workspace.deleteSelectedFilesPartial': '{n}개 파일을 삭제하지 못했습니다.',
'workspace.openFromDesignFiles': '디자인 파일 열기',
'workspace.designFilesLink': '디자인 파일',
'workspace.loadingSketch': '스케치 불러오는 중…',
@ -530,6 +532,7 @@ export const ko: Dict = {
'designFiles.openInTab': '탭에서 열기',
'designFiles.download': '다운로드',
'designFiles.downloadSelected': 'Download {n} as ZIP',
'designFiles.deleteSelected': '{n}개 삭제',
'designFiles.clearSelection': 'Clear',
'designFiles.selectAll': 'Select all',
'designFiles.dropTitle': '⤓ 여기에 파일을 놓으세요',

View file

@ -511,6 +511,8 @@ export const pl: Dict = {
'workspace.showChat': 'Show chat',
'workspace.closeTab': 'Zamknij kartę',
'workspace.deleteFileConfirm': 'Usunąć „{name}” z folderu projektu?',
'workspace.deleteSelectedFilesConfirm': 'Usunąć {n} wybranych plików z folderu projektu?',
'workspace.deleteSelectedFilesPartial': 'Nie udało się usunąć {n} plików.',
'workspace.openFromDesignFiles': 'Otwórz plik z',
'workspace.designFilesLink': 'Pliki projektu',
'workspace.loadingSketch': 'Ładowanie szkicu…',
@ -530,6 +532,7 @@ export const pl: Dict = {
'designFiles.openInTab': 'Otwórz w karcie',
'designFiles.download': 'Pobierz',
'designFiles.downloadSelected': 'Download {n} as ZIP',
'designFiles.deleteSelected': 'Usuń {n}',
'designFiles.clearSelection': 'Clear',
'designFiles.selectAll': 'Select all',
'designFiles.dropTitle': '⤓ Upuść pliki tutaj',

View file

@ -521,6 +521,8 @@ export const ptBR: Dict = {
'workspace.showChat': 'Show chat',
'workspace.closeTab': 'Fechar aba',
'workspace.deleteFileConfirm': 'Excluir "{name}" da pasta do projeto?',
'workspace.deleteSelectedFilesConfirm': 'Excluir {n} arquivo(s) selecionado(s) da pasta do projeto?',
'workspace.deleteSelectedFilesPartial': 'Falha ao excluir {n} arquivo(s).',
'workspace.openFromDesignFiles': 'Abra um arquivo em',
'workspace.designFilesLink': 'Arquivos de design',
'workspace.loadingSketch': 'Carregando esboço…',
@ -540,6 +542,7 @@ export const ptBR: Dict = {
'designFiles.openInTab': 'Abrir em aba',
'designFiles.download': 'Baixar',
'designFiles.downloadSelected': 'Download {n} as ZIP',
'designFiles.deleteSelected': 'Excluir {n}',
'designFiles.clearSelection': 'Clear',
'designFiles.selectAll': 'Select all',
'designFiles.dropTitle': '⤓ Solte arquivos aqui',

View file

@ -521,6 +521,8 @@ export const ru: Dict = {
'workspace.showChat': 'Show chat',
'workspace.closeTab': 'Закрыть вкладку',
'workspace.deleteFileConfirm': 'Удалить «{name}» из папки проекта?',
'workspace.deleteSelectedFilesConfirm': 'Удалить {n} выбранных файла(ов) из папки проекта?',
'workspace.deleteSelectedFilesPartial': 'Не удалось удалить {n} файл(ов).',
'workspace.openFromDesignFiles': 'Открыть файл из',
'workspace.designFilesLink': 'Файлы дизайна',
'workspace.loadingSketch': 'Загрузка эскиза…',
@ -540,6 +542,7 @@ export const ru: Dict = {
'designFiles.openInTab': 'Открыть во вкладке',
'designFiles.download': 'Скачать',
'designFiles.downloadSelected': 'Download {n} as ZIP',
'designFiles.deleteSelected': 'Удалить {n}',
'designFiles.clearSelection': 'Clear',
'designFiles.selectAll': 'Select all',
'designFiles.dropTitle': '⤓ Перетащите файлы сюда',

View file

@ -502,6 +502,8 @@ export const tr: Dict = {
'workspace.designFiles': 'Tasarım Dosyaları',
'workspace.closeTab': 'Sekmeyi kapat',
'workspace.deleteFileConfirm': '"{name}"ı proje klasöründen sil?',
'workspace.deleteSelectedFilesConfirm': 'Seçili {n} dosya proje klasöründen silinsin mi?',
'workspace.deleteSelectedFilesPartial': '{n} dosya silinemedi.',
'workspace.openFromDesignFiles': 'bir dosya aç',
'workspace.designFilesLink': 'Tasarım Dosyaları',
'workspace.loadingSketch': 'Taslak yükleniyor…',
@ -521,6 +523,7 @@ export const tr: Dict = {
'designFiles.openInTab': 'Sekmede aç',
'designFiles.download': 'İndir',
'designFiles.downloadSelected': 'Download {n} as ZIP',
'designFiles.deleteSelected': '{n} sil',
'designFiles.clearSelection': 'Clear',
'designFiles.selectAll': 'Select all',
'designFiles.dropTitle': '⤓ Dosyaları buraya sürükleyin',

View file

@ -522,6 +522,8 @@ export const uk: Dict = {
'workspace.showChat': 'Show chat',
'workspace.closeTab': 'Закрити вкладку',
'workspace.deleteFileConfirm': 'Видалити "{name}" з папки проекту?',
'workspace.deleteSelectedFilesConfirm': 'Видалити {n} вибраних файлів з папки проекту?',
'workspace.deleteSelectedFilesPartial': 'Не вдалося видалити {n} файл(ів).',
'workspace.openFromDesignFiles': 'Відкрити файл з',
'workspace.designFilesLink': 'Файли дизайну',
'workspace.loadingSketch': 'Завантаження ескізу…',
@ -541,6 +543,7 @@ export const uk: Dict = {
'designFiles.openInTab': 'Відкрити на вкладці',
'designFiles.download': 'Завантажити',
'designFiles.downloadSelected': 'Download {n} as ZIP',
'designFiles.deleteSelected': 'Видалити {n}',
'designFiles.clearSelection': 'Clear',
'designFiles.selectAll': 'Select all',
'designFiles.dropTitle': '⤓ Перенесіть файли сюди',

View file

@ -513,6 +513,8 @@ export const zhCN: Dict = {
'workspace.showChat': '显示聊天',
'workspace.closeTab': '关闭标签页',
'workspace.deleteFileConfirm': '从项目文件夹中删除「{name}」?',
'workspace.deleteSelectedFilesConfirm': '从项目文件夹中删除选中的 {n} 个文件?',
'workspace.deleteSelectedFilesPartial': '有 {n} 个文件删除失败。',
'workspace.openFromDesignFiles': '请从',
'workspace.designFilesLink': '设计文件',
'workspace.loadingSketch': '正在加载草图…',
@ -531,6 +533,7 @@ export const zhCN: Dict = {
'designFiles.openInTab': '在标签页中打开',
'designFiles.download': '下载',
'designFiles.downloadSelected': '下载选中的 {n} 个文件为 ZIP',
'designFiles.deleteSelected': '删除 {n} 个',
'designFiles.clearSelection': '取消选择',
'designFiles.selectAll': '全选',
'designFiles.dropTitle': '⤓ 把文件拖到这里',

View file

@ -513,6 +513,8 @@ export const zhTW: Dict = {
'workspace.showChat': '顯示聊天',
'workspace.closeTab': '關閉分頁',
'workspace.deleteFileConfirm': '從專案資料夾中刪除「{name}」?',
'workspace.deleteSelectedFilesConfirm': '從專案資料夾中刪除選中的 {n} 個檔案?',
'workspace.deleteSelectedFilesPartial': '有 {n} 個檔案刪除失敗。',
'workspace.openFromDesignFiles': '請從',
'workspace.designFilesLink': '設計檔案',
'workspace.loadingSketch': '正在載入草圖…',
@ -531,6 +533,7 @@ export const zhTW: Dict = {
'designFiles.openInTab': '在分頁中開啟',
'designFiles.download': '下載',
'designFiles.downloadSelected': '下載選中的 {n} 個檔案為 ZIP',
'designFiles.deleteSelected': '刪除 {n} 個',
'designFiles.clearSelection': '取消選擇',
'designFiles.selectAll': '全選',
'designFiles.dropTitle': '⤓ 把檔案拖到這裡',

View file

@ -582,6 +582,8 @@ export interface Dict {
'workspace.showChat': string;
'workspace.closeTab': string;
'workspace.deleteFileConfirm': string;
'workspace.deleteSelectedFilesConfirm': string;
'workspace.deleteSelectedFilesPartial': string;
'workspace.openFromDesignFiles': string;
'workspace.designFilesLink': string;
'workspace.loadingSketch': string;
@ -600,6 +602,7 @@ export interface Dict {
'designFiles.openInTab': string;
'designFiles.download': string;
'designFiles.downloadSelected': string;
'designFiles.deleteSelected': string;
'designFiles.clearSelection': string;
'designFiles.selectAll': string;
'designFiles.dropTitle': string;

View file

@ -4734,6 +4734,8 @@ button.connector-action.is-loading {
gap: 6px;
}
.df-head .df-actions button:hover:not(:disabled) { background: var(--bg-subtle); color: var(--text); }
.df-head .df-actions button.danger { color: var(--red); }
.df-head .df-actions button.danger:hover:not(:disabled) { background: var(--red-bg); color: var(--red); }
.df-body {
flex: 1;