diff --git a/src/components/ai-edition/Modals.tsx b/src/components/ai-edition/Modals.tsx index 739ca3f54..749aa8719 100644 --- a/src/components/ai-edition/Modals.tsx +++ b/src/components/ai-edition/Modals.tsx @@ -9,6 +9,7 @@ import { Plus, RefreshCw, RotateCcw, + Trash2, Triangle, X, } from "lucide-react"; @@ -133,6 +134,7 @@ interface OpenProjectModalProps extends BaseModalProps { projects: ProjectItem[]; activeProjectId: string | null; onSelect: (id: string) => void; + onDelete: (id: string) => void; onBrowse: () => void; } @@ -142,10 +144,19 @@ export function OpenProjectModal({ projects, activeProjectId, onSelect, + onDelete, onBrowse, }: OpenProjectModalProps) { const t = useScopedT("editor"); + const tc = useScopedT("common"); const [query, setQuery] = useState(""); + // Deleting a project cannot be undone, so the trash icon arms an inline + // confirm on its own row instead of deleting on the first click. One row at a + // time, and closing the dialog disarms it. + const [confirmId, setConfirmId] = useState(null); + useEffect(() => { + if (!open) setConfirmId(null); + }, [open]); const filtered = projects.filter((p) => p.title.toLowerCase().includes(query.toLowerCase())); return ( { const isActive = p.id === activeProjectId; - return ( - + -
+ ); + } + return ( +
+
-
- + + + + + ); }) )} diff --git a/src/components/ai-edition/NewEditorShell.tsx b/src/components/ai-edition/NewEditorShell.tsx index 208942e27..db3d34154 100644 --- a/src/components/ai-edition/NewEditorShell.tsx +++ b/src/components/ai-edition/NewEditorShell.tsx @@ -608,6 +608,27 @@ export function NewEditorShell() { [createProject], ); + // The only way to get rid of a project short of deleting its file by hand. + // Media is left alone on purpose: a recording can back several projects, and + // the dialog says so before it asks. + const handleDeleteProject = useCallback(async (id: string) => { + try { + const result = await nativeBridgeClient.aiEdition.delete(id); + if (!result.success) throw new Error(result.error ?? "Failed to delete project"); + setProjectSummaries((prev) => prev.filter((p) => p.id !== id)); + // Deleting the project that is open leaves the editor pointing at a file + // that no longer exists — any save from there would recreate it. + if (useProjectStore.getState().projectId === id) { + useProjectStore.getState().clear(); + } + toast.success("Project deleted"); + } catch (err) { + toast.error("Could not delete project", { + description: err instanceof Error ? err.message : String(err), + }); + } + }, []); + const handleSave = useCallback(async () => { const doc = useProjectStore.getState().document; if (!doc) return; @@ -1271,6 +1292,7 @@ export function NewEditorShell() { projects={projectSummaries} activeProjectId={projectId} onSelect={handleSelectProject} + onDelete={handleDeleteProject} onBrowse={handleBrowseProject} /> {ui}); +} + +const projects = [ + { id: "proj_one", title: "First project", updatedAt: "2026-08-12T10:00:00.000Z" }, + { id: "proj_two", title: "Second project", updatedAt: "2026-08-13T10:00:00.000Z" }, +]; + +function renderModal(overrides: { onSelect?: () => void; onDelete?: () => void } = {}) { + const onSelect = overrides.onSelect ?? vi.fn(); + const onDelete = overrides.onDelete ?? vi.fn(); + renderWithI18n( + , + ); + return { onSelect, onDelete }; +} + +describe("OpenProjectModal delete", () => { + afterEach(() => { + cleanup(); + vi.clearAllMocks(); + }); + + it("asks before deleting instead of deleting on the first click", () => { + const { onDelete } = renderModal(); + + fireEvent.click(screen.getAllByRole("button", { name: /delete project/i })[0]); + + expect(onDelete).not.toHaveBeenCalled(); + expect(screen.getByText(/recordings are kept/i)).toBeInTheDocument(); + }); + + it("deletes the project the confirm was armed on", () => { + const { onDelete } = renderModal(); + + fireEvent.click(screen.getAllByRole("button", { name: /delete project/i })[1]); + fireEvent.click(screen.getByRole("button", { name: /^delete$/i })); + + expect(onDelete).toHaveBeenCalledWith("proj_two"); + }); + + it("cancels back to the row", () => { + const { onDelete } = renderModal(); + + fireEvent.click(screen.getAllByRole("button", { name: /delete project/i })[0]); + fireEvent.click(screen.getByRole("button", { name: /cancel/i })); + + expect(onDelete).not.toHaveBeenCalled(); + expect(screen.getByText("First project")).toBeInTheDocument(); + expect(screen.queryByText(/recordings are kept/i)).not.toBeInTheDocument(); + }); + + // The delete button sits next to the row's own button, not inside it — a + // nested button would swallow the click that opens the project. + it("still opens a project when the row is clicked", () => { + const { onSelect } = renderModal(); + + fireEvent.click(screen.getByText("First project")); + + expect(onSelect).toHaveBeenCalledWith("proj_one"); + }); +}); diff --git a/src/i18n/locales/ar/editor.json b/src/i18n/locales/ar/editor.json index fb3d6875a..d2a469a56 100644 --- a/src/i18n/locales/ar/editor.json +++ b/src/i18n/locales/ar/editor.json @@ -311,7 +311,9 @@ "noMatches": "لا توجد مشاريع تطابق \"{{query}}\".", "navigateHint": "للتنقل ·", "openHint": "للفتح", - "browseFiles": "تصفح الملفات…" + "browseFiles": "تصفح الملفات…", + "deleteProject": "حذف المشروع", + "confirmDelete": "هل تريد حذف هذا المشروع؟ سيتم الاحتفاظ بتسجيلاتك." }, "editClipDialog": { "title": "تعديل المقطع", diff --git a/src/i18n/locales/en/editor.json b/src/i18n/locales/en/editor.json index fe7fe12f9..390f687bc 100644 --- a/src/i18n/locales/en/editor.json +++ b/src/i18n/locales/en/editor.json @@ -311,7 +311,9 @@ "noMatches": "No projects match \"{{query}}\".", "navigateHint": "to navigate ·", "openHint": "to open", - "browseFiles": "Browse files…" + "browseFiles": "Browse files…", + "deleteProject": "Delete project", + "confirmDelete": "Delete this project? Your recordings are kept." }, "editClipDialog": { "title": "Edit clip", diff --git a/src/i18n/locales/es/editor.json b/src/i18n/locales/es/editor.json index efc550fa7..a3bb887f5 100644 --- a/src/i18n/locales/es/editor.json +++ b/src/i18n/locales/es/editor.json @@ -311,7 +311,9 @@ "noMatches": "Ningún proyecto coincide con \"{{query}}\".", "navigateHint": "para navegar ·", "openHint": "para abrir", - "browseFiles": "Explorar archivos…" + "browseFiles": "Explorar archivos…", + "deleteProject": "Eliminar proyecto", + "confirmDelete": "¿Eliminar este proyecto? Tus grabaciones se conservan." }, "editClipDialog": { "title": "Editar clip", diff --git a/src/i18n/locales/fr/editor.json b/src/i18n/locales/fr/editor.json index 152b139d9..245ee857e 100644 --- a/src/i18n/locales/fr/editor.json +++ b/src/i18n/locales/fr/editor.json @@ -311,7 +311,9 @@ "noMatches": "Aucun projet ne correspond à « {{query}} ».", "navigateHint": "pour naviguer ·", "openHint": "pour ouvrir", - "browseFiles": "Parcourir les fichiers…" + "browseFiles": "Parcourir les fichiers…", + "deleteProject": "Supprimer le projet", + "confirmDelete": "Supprimer ce projet ? Vos enregistrements sont conservés." }, "editClipDialog": { "title": "Modifier le clip", diff --git a/src/i18n/locales/it/editor.json b/src/i18n/locales/it/editor.json index 8462d90a4..ab2e9f465 100644 --- a/src/i18n/locales/it/editor.json +++ b/src/i18n/locales/it/editor.json @@ -311,7 +311,9 @@ "noMatches": "Nessun progetto corrisponde a \"{{query}}\".", "navigateHint": "per navigare ·", "openHint": "per aprire", - "browseFiles": "Sfoglia file…" + "browseFiles": "Sfoglia file…", + "deleteProject": "Elimina progetto", + "confirmDelete": "Eliminare questo progetto? Le tue registrazioni vengono conservate." }, "editClipDialog": { "title": "Modifica clip", diff --git a/src/i18n/locales/ja-JP/editor.json b/src/i18n/locales/ja-JP/editor.json index 0ec709cce..d4cb0efea 100644 --- a/src/i18n/locales/ja-JP/editor.json +++ b/src/i18n/locales/ja-JP/editor.json @@ -311,7 +311,9 @@ "noMatches": "「{{query}}」に一致するプロジェクトがありません。", "navigateHint": "で移動 ·", "openHint": "で開く", - "browseFiles": "ファイルを参照…" + "browseFiles": "ファイルを参照…", + "deleteProject": "プロジェクトを削除", + "confirmDelete": "このプロジェクトを削除しますか?録画ファイルは保持されます。" }, "editClipDialog": { "title": "クリップを編集", diff --git a/src/i18n/locales/ko-KR/editor.json b/src/i18n/locales/ko-KR/editor.json index f6abeea22..d06b5a097 100644 --- a/src/i18n/locales/ko-KR/editor.json +++ b/src/i18n/locales/ko-KR/editor.json @@ -311,7 +311,9 @@ "noMatches": "\"{{query}}\"와(과) 일치하는 프로젝트가 없습니다.", "navigateHint": "탐색 ·", "openHint": "열기", - "browseFiles": "파일 찾아보기…" + "browseFiles": "파일 찾아보기…", + "deleteProject": "프로젝트 삭제", + "confirmDelete": "이 프로젝트를 삭제할까요? 녹화 파일은 유지됩니다." }, "editClipDialog": { "title": "클립 편집", diff --git a/src/i18n/locales/pt-BR/editor.json b/src/i18n/locales/pt-BR/editor.json index 71af170d5..4c0b3ddc2 100644 --- a/src/i18n/locales/pt-BR/editor.json +++ b/src/i18n/locales/pt-BR/editor.json @@ -311,7 +311,9 @@ "noMatches": "Nenhum projeto corresponde a \"{{query}}\".", "navigateHint": "para navegar ·", "openHint": "para abrir", - "browseFiles": "Procurar arquivos…" + "browseFiles": "Procurar arquivos…", + "deleteProject": "Excluir projeto", + "confirmDelete": "Excluir este projeto? Suas gravações são mantidas." }, "editClipDialog": { "title": "Editar clipe", diff --git a/src/i18n/locales/ru/editor.json b/src/i18n/locales/ru/editor.json index 451c4d062..89a3e6fc7 100644 --- a/src/i18n/locales/ru/editor.json +++ b/src/i18n/locales/ru/editor.json @@ -311,7 +311,9 @@ "noMatches": "Нет проектов, соответствующих «{{query}}».", "navigateHint": "для навигации ·", "openHint": "чтобы открыть", - "browseFiles": "Обзор файлов…" + "browseFiles": "Обзор файлов…", + "deleteProject": "Удалить проект", + "confirmDelete": "Удалить этот проект? Ваши записи сохранятся." }, "editClipDialog": { "title": "Изменить клип", diff --git a/src/i18n/locales/tr/editor.json b/src/i18n/locales/tr/editor.json index becba505e..0a6797954 100644 --- a/src/i18n/locales/tr/editor.json +++ b/src/i18n/locales/tr/editor.json @@ -311,7 +311,9 @@ "noMatches": "\"{{query}}\" ile eşleşen proje yok.", "navigateHint": "gezinmek için ·", "openHint": "açmak için", - "browseFiles": "Dosyalara göz at…" + "browseFiles": "Dosyalara göz at…", + "deleteProject": "Projeyi sil", + "confirmDelete": "Bu proje silinsin mi? Kayıtlarınız korunur." }, "editClipDialog": { "title": "Klibi düzenle", diff --git a/src/i18n/locales/vi/editor.json b/src/i18n/locales/vi/editor.json index 8fa562492..1456ffe09 100644 --- a/src/i18n/locales/vi/editor.json +++ b/src/i18n/locales/vi/editor.json @@ -311,7 +311,9 @@ "noMatches": "Không có dự án nào khớp với \"{{query}}\".", "navigateHint": "để điều hướng ·", "openHint": "để mở", - "browseFiles": "Duyệt tệp…" + "browseFiles": "Duyệt tệp…", + "deleteProject": "Xoá dự án", + "confirmDelete": "Xoá dự án này? Các bản ghi của bạn vẫn được giữ lại." }, "editClipDialog": { "title": "Chỉnh sửa clip", diff --git a/src/i18n/locales/zh-CN/editor.json b/src/i18n/locales/zh-CN/editor.json index dd629f57f..0c61fddb9 100644 --- a/src/i18n/locales/zh-CN/editor.json +++ b/src/i18n/locales/zh-CN/editor.json @@ -311,7 +311,9 @@ "noMatches": "没有与 \"{{query}}\" 匹配的项目。", "navigateHint": "导航 ·", "openHint": "打开", - "browseFiles": "浏览文件…" + "browseFiles": "浏览文件…", + "deleteProject": "删除项目", + "confirmDelete": "删除此项目?您的录制文件会保留。" }, "editClipDialog": { "title": "编辑片段", diff --git a/src/i18n/locales/zh-TW/editor.json b/src/i18n/locales/zh-TW/editor.json index 865096d0f..29e9b4ef4 100644 --- a/src/i18n/locales/zh-TW/editor.json +++ b/src/i18n/locales/zh-TW/editor.json @@ -311,7 +311,9 @@ "noMatches": "沒有符合 \"{{query}}\" 的專案。", "navigateHint": "導覽 ·", "openHint": "開啟", - "browseFiles": "瀏覽檔案…" + "browseFiles": "瀏覽檔案…", + "deleteProject": "刪除專案", + "confirmDelete": "刪除此專案?您的錄影檔案會保留。" }, "editClipDialog": { "title": "編輯片段",