fix(web): confirm before deleting saved template in New Project (#2330)

* fix(web): confirm before deleting saved template in New Project

EntryShell and NewProjectModal were dropping the onDeleteTemplate prop, so the trash button in the template picker was inert; thread it through and gate it behind an alertdialog confirm.

Fixes #2237

* fix(web/i18n): translate deleteTemplate keys across 18 locales

The initial commit shipped English placeholders for the 4 new newproj.deleteTemplate* keys; replace with localized strings matching each locale's existing designs.deleteConfirm style.

* fix(web): gate confirm-dialog backdrop on in-flight delete + style error message

Backdrop now no-ops while `deleting` is true (matching the Cancel and Delete button gating), preventing a false-cancellation race where the backgrounded DELETE keeps running and leaks a stale `deleteError=true` into the next dialog open; also adds the missing `.modal-confirm-error` rule so the inline failure copy renders as a destructive state instead of default body styling.
This commit is contained in:
Ethan Guo 2026-05-20 14:50:36 +08:00 committed by GitHub
parent 96b6db14c2
commit 38b59fa4d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 311 additions and 10 deletions

View file

@ -201,6 +201,7 @@ interface Props {
designSystems: DesignSystemSummary[];
projects: Project[];
templates: ProjectTemplate[];
onDeleteTemplate?: (id: string) => Promise<boolean>;
promptTemplates: PromptTemplateSummary[];
defaultDesignSystemId: string | null;
connectors: ConnectorDetail[];
@ -316,6 +317,7 @@ export function EntryShell({
designSystems,
projects,
templates,
onDeleteTemplate,
promptTemplates,
defaultDesignSystemId,
connectors,
@ -983,6 +985,7 @@ export function EntryShell({
designSystems={designSystems}
defaultDesignSystemId={defaultDesignSystemId}
templates={templates}
{...(onDeleteTemplate ? { onDeleteTemplate } : {})}
promptTemplates={promptTemplates}
connectors={connectors}
connectorsLoading={connectorsLoading}

View file

@ -330,6 +330,7 @@ export function EntryView({
designSystems={designSystems}
projects={projects}
templates={templates}
onDeleteTemplate={onDeleteTemplate}
promptTemplates={promptTemplates}
defaultDesignSystemId={defaultDesignSystemId}
connectors={connectors}

View file

@ -26,6 +26,7 @@ interface Props {
designSystems: DesignSystemSummary[];
defaultDesignSystemId: string | null;
templates: ProjectTemplate[];
onDeleteTemplate?: (id: string) => Promise<boolean>;
promptTemplates: PromptTemplateSummary[];
mediaProviders?: Record<string, MediaProviderCredentials>;
connectors?: ConnectorDetail[];
@ -45,6 +46,7 @@ export function NewProjectModal({
designSystems,
defaultDesignSystemId,
templates,
onDeleteTemplate,
promptTemplates,
mediaProviders,
connectors,
@ -115,6 +117,7 @@ export function NewProjectModal({
designSystems={designSystems}
defaultDesignSystemId={defaultDesignSystemId}
templates={templates}
{...(onDeleteTemplate ? { onDeleteTemplate } : {})}
promptTemplates={promptTemplates}
{...(mediaProviders ? { mediaProviders } : {})}
{...(connectors ? { connectors } : {})}

View file

@ -1325,6 +1325,37 @@ function TemplatePicker({
onDelete?: (id: string) => Promise<boolean>;
}) {
const t = useT();
const [confirmDelete, setConfirmDelete] = useState<
{ id: string; name: string } | null
>(null);
const [deleting, setDeleting] = useState(false);
const [deleteError, setDeleteError] = useState(false);
function closeConfirm() {
setConfirmDelete(null);
setDeleting(false);
setDeleteError(false);
}
async function runDelete() {
if (!confirmDelete || !onDelete) return;
setDeleting(true);
setDeleteError(false);
let ok = false;
try {
ok = await onDelete(confirmDelete.id);
} catch {
ok = false;
}
if (ok) {
if (value === confirmDelete.id) onChange(null);
closeConfirm();
} else {
setDeleting(false);
setDeleteError(true);
}
}
return (
<div className="newproj-section">
<label className="newproj-label">{t('newproj.templateLabel')}</label>
@ -1350,10 +1381,7 @@ function TemplatePicker({
key={tpl.id}
active={value === tpl.id}
onClick={() => onChange(tpl.id)}
onDelete={onDelete ? async () => {
const ok = await onDelete(tpl.id);
if (ok && value === tpl.id) onChange(null);
} : () => {}}
onDelete={onDelete ? () => setConfirmDelete({ id: tpl.id, name: tpl.name }) : () => {}}
name={tpl.name}
description={tpl.description ?? fallbackDesc}
/>
@ -1361,6 +1389,43 @@ function TemplatePicker({
})}
</div>
)}
{confirmDelete ? (
<div
className="modal-backdrop"
onClick={deleting ? undefined : closeConfirm}
>
<div
className="modal modal-confirm"
onClick={(e) => e.stopPropagation()}
role="alertdialog"
aria-modal="true"
>
<h2>{t('newproj.deleteTemplateTitle')}</h2>
<p className="modal-confirm-message">
{t('newproj.deleteTemplateConfirm', { name: confirmDelete.name })}
</p>
{deleteError ? (
<p className="modal-confirm-error" role="alert">
{t('newproj.deleteTemplateError')}
</p>
) : null}
<div className="row">
<button type="button" onClick={closeConfirm} disabled={deleting}>
{t('common.cancel')}
</button>
<button
type="button"
className="primary danger"
autoFocus
disabled={deleting}
onClick={runDelete}
>
{t('newproj.deleteTemplateConfirmCta')}
</button>
</div>
</div>
</div>
) : null}
</div>
);
}

View file

@ -538,6 +538,11 @@ export const ar: Dict = {
'عدل أي شيء - تغييراتك تنتقل إلى موجز الوكيل.',
'newproj.promptTemplateBodyEmpty':
'جسم فارغ - لن يحصل الوكيل على مرجع قالب.',
'newproj.deleteTemplateTitle': 'حذف القالب',
'newproj.deleteTemplateConfirm': 'هل تريد حذف "{name}"؟ لا يمكن التراجع عن هذا الإجراء.',
'newproj.deleteTemplateConfirmCta': 'حذف القالب',
'newproj.deleteTemplateError':
'تعذر حذف هذا القالب. يرجى المحاولة مرة أخرى.',
'designs.subRecent': 'الأخيرة',
'designs.subYours': 'تصاميمك',

View file

@ -426,6 +426,11 @@ export const de: Dict = {
'Beliebig editierbar — deine Änderungen fließen in das Agenten-Briefing ein.',
'newproj.promptTemplateBodyEmpty':
'Leerer Body — der Agent erhält keine Vorlagenreferenz.',
'newproj.deleteTemplateTitle': 'Template löschen',
'newproj.deleteTemplateConfirm': '„{name}" löschen? Dies kann nicht rückgängig gemacht werden.',
'newproj.deleteTemplateConfirmCta': 'Template löschen',
'newproj.deleteTemplateError':
'Dieses Template konnte nicht gelöscht werden. Bitte erneut versuchen.',
'designs.subRecent': 'Aktuell',
'designs.subYours': 'Ihre Designs',

View file

@ -991,6 +991,11 @@ export const en: Dict = {
'Edit anything — your changes carry into the agent\'s brief.',
'newproj.promptTemplateBodyEmpty':
'Empty body — the agent will get no template reference.',
'newproj.deleteTemplateTitle': 'Delete template',
'newproj.deleteTemplateConfirm': 'Delete "{name}"? This cannot be undone.',
'newproj.deleteTemplateConfirmCta': 'Delete template',
'newproj.deleteTemplateError':
'Could not delete this template. Please try again.',
'designs.subRecent': 'Recent',
'designs.subYours': 'Your designs',

View file

@ -427,6 +427,11 @@ export const esES: Dict = {
'Edita lo que quieras — tus cambios se incorporan al briefing del agente.',
'newproj.promptTemplateBodyEmpty':
'Cuerpo vacío — el agente no recibirá ninguna referencia de plantilla.',
'newproj.deleteTemplateTitle': 'Eliminar plantilla',
'newproj.deleteTemplateConfirm': '¿Eliminar «{name}»? Esta acción no se puede deshacer.',
'newproj.deleteTemplateConfirmCta': 'Eliminar plantilla',
'newproj.deleteTemplateError':
'No se pudo eliminar esta plantilla. Inténtalo de nuevo.',
'designs.subRecent': 'Recientes',
'designs.subYours': 'Tus diseños',

View file

@ -541,6 +541,11 @@ export const fa: Dict = {
'newproj.promptTemplateOptimizeHint':
'هر چیزی را می‌توانید ویرایش کنید — تغییرات شما به بریف ایجنت اضافه می‌شود.',
'newproj.promptTemplateBodyEmpty': 'متن خالی است — ایجنت هیچ مرجع قالبی دریافت نمی‌کند.',
'newproj.deleteTemplateTitle': 'حذف قالب',
'newproj.deleteTemplateConfirm': 'آیا «{name}» حذف شود؟ این عمل قابل بازگشت نیست.',
'newproj.deleteTemplateConfirmCta': 'حذف قالب',
'newproj.deleteTemplateError':
'حذف این قالب ممکن نشد. لطفاً دوباره تلاش کنید.',
'newproj.dsModeSingle': 'تکی',
'newproj.dsModeMulti': 'چندگانه',
'newproj.dsNoneTitle': 'هیچ — آزاد',

View file

@ -538,6 +538,11 @@ export const fr: Dict = {
'Modifiez ce que vous voulez — vos modifications sont transmises au brief de l\'agent.',
'newproj.promptTemplateBodyEmpty':
'Corps vide — l\'agent ne recevra aucune référence de modèle.',
'newproj.deleteTemplateTitle': 'Supprimer le modèle',
'newproj.deleteTemplateConfirm': 'Supprimer « {name} » ? Cette action est irréversible.',
'newproj.deleteTemplateConfirmCta': 'Supprimer le modèle',
'newproj.deleteTemplateError':
'Impossible de supprimer ce modèle. Veuillez réessayer.',
'designs.subRecent': 'Récents',
'designs.subYours': 'Vos designs',

View file

@ -538,6 +538,11 @@ export const hu: Dict = {
'Bármit módosíthatsz — a változtatásaid beépülnek az ügynök briefjébe.',
'newproj.promptTemplateBodyEmpty':
'Üres törzs — az ügynök nem kap sablonhivatkozást.',
'newproj.deleteTemplateTitle': 'Sablon törlése',
'newproj.deleteTemplateConfirm': 'Törlöd a(z) „{name}" sablont? Ez nem vonható vissza.',
'newproj.deleteTemplateConfirmCta': 'Sablon törlése',
'newproj.deleteTemplateError':
'A sablon törlése nem sikerült. Próbáld újra.',
'designs.subRecent': 'Legutóbbi',
'designs.subYours': 'A te terveid',

View file

@ -647,6 +647,11 @@ export const id: Dict = {
'newproj.promptTemplateOptimizeHint': 'Edit prompt sebelum membuat proyek agar sesuai dengan kebutuhanmu.',
'newproj.promptTemplateBodyEmpty': 'Prompt kosong - agent tidak menerima referensi templat.',
'newproj.deleteTemplateTitle': 'Hapus templat',
'newproj.deleteTemplateConfirm': 'Hapus "{name}"? Tindakan ini tidak dapat dibatalkan.',
'newproj.deleteTemplateConfirmCta': 'Hapus templat',
'newproj.deleteTemplateError':
'Tidak dapat menghapus templat ini. Silakan coba lagi.',
'designs.subRecent': 'Terbaru',
'designs.subYours': 'Desainmu',
'designs.filterAria': 'Filter proyek',

View file

@ -506,6 +506,11 @@ export const it: Dict = {
'Modifica ciò che vuoi — le tue modifiche vengono trasmesse al brief dell\'agente.',
'newproj.promptTemplateBodyEmpty':
'Corpo vuoto — l\'agente non riceverà alcun riferimento di modello.',
'newproj.deleteTemplateTitle': 'Elimina modello',
'newproj.deleteTemplateConfirm': 'Eliminare « {name} » ? Questa operazione non può essere annullata.',
'newproj.deleteTemplateConfirmCta': 'Elimina modello',
'newproj.deleteTemplateError':
'Impossibile eliminare questo modello. Riprova.',
'designs.subRecent': 'Recenti',
'designs.subYours': 'I tuoi design',

View file

@ -426,6 +426,11 @@ export const ja: Dict = {
'自由に編集できます — 変更内容はエージェントのブリーフに反映されます。',
'newproj.promptTemplateBodyEmpty': '本文が空です — エージェントはテンプレート参照を受け取りません。',
'newproj.deleteTemplateTitle': 'テンプレートを削除',
'newproj.deleteTemplateConfirm': '"{name}" を削除しますか?この操作は取り消せません。',
'newproj.deleteTemplateConfirmCta': 'テンプレートを削除',
'newproj.deleteTemplateError':
'テンプレートを削除できませんでした。もう一度お試しください。',
'designs.subRecent': '最近',
'designs.subYours': 'あなたのデザイン',
'designs.filterAria': 'プロジェクトをフィルター',

View file

@ -538,6 +538,11 @@ export const ko: Dict = {
'내용을 수정할 수 있습니다. 변경된 내용은 에이전트의 브리프에 반영됩니다.',
'newproj.promptTemplateBodyEmpty':
'본문이 비어있습니다 — 에이전트에게 전달될 템플릿 참조가 없습니다.',
'newproj.deleteTemplateTitle': '템플릿 삭제',
'newproj.deleteTemplateConfirm': '"{name}" 템플릿을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.',
'newproj.deleteTemplateConfirmCta': '템플릿 삭제',
'newproj.deleteTemplateError':
'템플릿을 삭제할 수 없습니다. 다시 시도해 주세요.',
'designs.subRecent': '최근 항목',
'designs.subYours': '내 디자인',

View file

@ -538,6 +538,11 @@ export const pl: Dict = {
'Możesz edytować wszystko — Twoje zmiany zostaną uwzględnione w instrukcjach dla agenta.',
'newproj.promptTemplateBodyEmpty':
'Pusta treść — agent nie otrzyma referencji do szablonu.',
'newproj.deleteTemplateTitle': 'Usuń szablon',
'newproj.deleteTemplateConfirm': 'Usunąć „{name}"? Tej operacji nie można cofnąć.',
'newproj.deleteTemplateConfirmCta': 'Usuń szablon',
'newproj.deleteTemplateError':
'Nie udało się usunąć tego szablonu. Spróbuj ponownie.',
'designs.subRecent': 'Ostatnie',
'designs.subYours': 'Twoje projekty',

View file

@ -550,6 +550,11 @@ export const ptBR: Dict = {
'Edite o que quiser — suas alterações entram no briefing do agente.',
'newproj.promptTemplateBodyEmpty':
'Corpo vazio — o agente não receberá nenhuma referência de modelo.',
'newproj.deleteTemplateTitle': 'Excluir template',
'newproj.deleteTemplateConfirm': 'Excluir "{name}"? Esta ação não pode ser desfeita.',
'newproj.deleteTemplateConfirmCta': 'Excluir template',
'newproj.deleteTemplateError':
'Não foi possível excluir este template. Tente novamente.',
'designs.subRecent': 'Recentes',
'designs.subYours': 'Seus designs',

View file

@ -550,6 +550,11 @@ export const ru: Dict = {
'Меняйте всё что нужно — правки попадут в бриф агента.',
'newproj.promptTemplateBodyEmpty':
'Пустое тело — агент не получит шаблонную референцию.',
'newproj.deleteTemplateTitle': 'Удалить шаблон',
'newproj.deleteTemplateConfirm': 'Удалить «{name}»? Это действие невозможно отменить.',
'newproj.deleteTemplateConfirmCta': 'Удалить шаблон',
'newproj.deleteTemplateError':
'Не удалось удалить шаблон. Попробуйте ещё раз.',
'designs.subRecent': 'Недавние',
'designs.subYours': 'Ваши дизайны',

View file

@ -506,6 +506,11 @@ export const th: Dict = {
'newproj.promptTemplateOptimizeHint': 'การปรับแก้ของคุณจะส่งผลต่อเอเจนต์ด้วย',
'newproj.promptTemplateBodyEmpty': 'เนื้อหาว่างเปล่า เอเจนต์จะไม่ได้ข้อมูลอ้างอิง',
'newproj.deleteTemplateTitle': 'ลบเทมเพลต',
'newproj.deleteTemplateConfirm': 'ลบ "{name}" ใช่ไหม การลบนี้ไม่สามารถย้อนกลับได้',
'newproj.deleteTemplateConfirmCta': 'ลบเทมเพลต',
'newproj.deleteTemplateError':
'ไม่สามารถลบเทมเพลตนี้ได้ โปรดลองอีกครั้ง',
'designs.subRecent': 'ล่าสุด',
'designs.subYours': 'ดีไซน์ของคุณ',
'designs.filterAria': 'ตัวกรอง',

View file

@ -528,6 +528,11 @@ export const tr: Dict = {
'İstediğin her şeyi düzenle — değişikliklerin ajan briefine taşınır.',
'newproj.promptTemplateBodyEmpty': 'Gövde boş — ajana şablon referansı gitmeyecek.',
'newproj.deleteTemplateTitle': 'Şablonu sil',
'newproj.deleteTemplateConfirm': '"{name}" silinsin mi? Bu işlem geri alınamaz.',
'newproj.deleteTemplateConfirmCta': 'Şablonu sil',
'newproj.deleteTemplateError':
'Bu şablon silinemedi. Lütfen tekrar deneyin.',
'designs.subRecent': 'Yakında',
'designs.subYours': 'Tasarımların',
'designs.filterAria': 'Projeleri filtrele',

View file

@ -542,6 +542,11 @@ export const uk: Dict = {
'Редагуйте що завгодно — ваші зміни переносяться в брифінг агента.',
'newproj.promptTemplateBodyEmpty':
'Порожній текст — агент не отримає посилання на шаблон.',
'newproj.deleteTemplateTitle': 'Видалити шаблон',
'newproj.deleteTemplateConfirm': 'Видалити «{name}»? Цю дію не можна скасувати.',
'newproj.deleteTemplateConfirmCta': 'Видалити шаблон',
'newproj.deleteTemplateError':
'Не вдалося видалити цей шаблон. Спробуйте ще раз.',
'newproj.connectorsLabel': 'Конектори',
'newproj.connectorsHint': 'Надайте агенту доступ до підключених джерел даних для цього live-артефакту.',
'newproj.connectorsEmptyTitle': 'Поки що немає підключених конекторів',

View file

@ -987,6 +987,11 @@ export const zhCN: Dict = {
'可以任意编辑 — 修改后的内容会作为 agent 生成时的参考。',
'newproj.promptTemplateBodyEmpty': '正文为空 — agent 不会拿到模板参考。',
'newproj.deleteTemplateTitle': '删除模板',
'newproj.deleteTemplateConfirm': '确定删除「{name}」?此操作无法撤销。',
'newproj.deleteTemplateConfirmCta': '删除模板',
'newproj.deleteTemplateError':
'无法删除该模板,请重试。',
'designs.subRecent': '最近',
'designs.subYours': '我的设计',
'designs.filterAria': '筛选项目',

View file

@ -608,6 +608,11 @@ export const zhTW: Dict = {
'可隨意編輯 — 修改後的內容會作為 agent 生成時的參考。',
'newproj.promptTemplateBodyEmpty': '內容為空 — agent 不會拿到範本參考。',
'newproj.deleteTemplateTitle': '刪除範本',
'newproj.deleteTemplateConfirm': '確定刪除「{name}」?此操作無法復原。',
'newproj.deleteTemplateConfirmCta': '刪除範本',
'newproj.deleteTemplateError':
'無法刪除此範本,請重試。',
'designs.subRecent': '最近',
'designs.subYours': '我的設計',
'designs.filterAria': '篩選專案',

View file

@ -1249,6 +1249,10 @@ export interface Dict {
'newproj.promptTemplateBodyLabel': string;
'newproj.promptTemplateOptimizeHint': string;
'newproj.promptTemplateBodyEmpty': string;
'newproj.deleteTemplateTitle': string;
'newproj.deleteTemplateConfirm': string;
'newproj.deleteTemplateConfirmCta': string;
'newproj.deleteTemplateError': string;
// Prompt templates
'promptTemplates.searchPlaceholder': string;

View file

@ -2701,6 +2701,12 @@ a.avatar-item:visited {
color: var(--text-strong);
line-height: 1.55;
}
.modal-confirm-error {
margin: 0;
font-size: 13px;
color: var(--red);
line-height: 1.55;
}
.modal-confirm .row { margin-top: 8px; }
.modal-confirm .row button {
padding: 8px 18px;

View file

@ -1,10 +1,14 @@
// @vitest-environment jsdom
import { cleanup, render, screen } from '@testing-library/react';
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { NewProjectModal } from '../../src/components/NewProjectModal';
import type { DesignSystemSummary, SkillSummary } from '../../src/types';
import type {
DesignSystemSummary,
ProjectTemplate,
SkillSummary,
} from '../../src/types';
const skills: SkillSummary[] = [
{
@ -77,3 +81,39 @@ describe('NewProjectModal layout', () => {
expect(screen.getByTestId('create-project')).toBeTruthy();
});
});
describe('NewProjectModal template deletion plumbing', () => {
it('forwards onDeleteTemplate to the inner panel', async () => {
const templates: ProjectTemplate[] = [
{
id: 'tmpl-landing',
name: 'Landing Page',
description: 'A saved landing page starter.',
files: [{ name: 'prototype/App.jsx', content: '' }],
createdAt: 1714867200000,
},
];
const onDelete = vi.fn().mockResolvedValue(true);
render(
<NewProjectModal
open
skills={skills}
designSystems={designSystems}
defaultDesignSystemId="clay"
templates={templates}
promptTemplates={[]}
onDeleteTemplate={onDelete}
onCreate={() => {}}
onClose={() => {}}
/>,
);
fireEvent.click(screen.getByRole('tab', { name: 'From template' }));
fireEvent.click(screen.getByLabelText(/delete template/i));
await screen.findByRole('alertdialog');
fireEvent.click(screen.getByRole('button', { name: 'Delete template' }));
expect(onDelete).toHaveBeenCalledWith('tmpl-landing');
});
});

View file

@ -1,6 +1,6 @@
// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { renderToStaticMarkup } from 'react-dom/server';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
@ -667,7 +667,7 @@ describe('NewProjectPanel template deletion', () => {
Element.prototype.scrollIntoView = () => {};
});
it('calls onDeleteTemplate when user clicks delete button', async () => {
it('calls onDeleteTemplate only after the user confirms in the dialog', async () => {
const onDelete = vi.fn().mockResolvedValue(true);
render(
<NewProjectPanel
@ -682,8 +682,92 @@ describe('NewProjectPanel template deletion', () => {
);
fireEvent.click(screen.getByRole('tab', { name: 'From template' }));
const deleteBtn = screen.getByLabelText(/delete template/i);
fireEvent.click(deleteBtn);
fireEvent.click(screen.getByLabelText(/delete template/i));
expect(onDelete).not.toHaveBeenCalled();
const dialog = await screen.findByRole('alertdialog');
expect(dialog.textContent).toContain('Landing Page');
fireEvent.click(screen.getByRole('button', { name: 'Delete template' }));
expect(onDelete).toHaveBeenCalledWith('tmpl-landing');
});
it('does not call onDeleteTemplate when the user cancels the confirmation', async () => {
const onDelete = vi.fn().mockResolvedValue(true);
render(
<NewProjectPanel
skills={skills}
designSystems={designSystems}
defaultDesignSystemId="clay"
templates={templates}
onDeleteTemplate={onDelete}
promptTemplates={[]}
onCreate={vi.fn()}
/>,
);
fireEvent.click(screen.getByRole('tab', { name: 'From template' }));
fireEvent.click(screen.getByLabelText(/delete template/i));
await screen.findByRole('alertdialog');
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
expect(onDelete).not.toHaveBeenCalled();
expect(screen.queryByRole('alertdialog')).toBeNull();
});
it('keeps the confirm dialog open with an inline error when onDeleteTemplate returns false', async () => {
const onDelete = vi.fn().mockResolvedValue(false);
render(
<NewProjectPanel
skills={skills}
designSystems={designSystems}
defaultDesignSystemId="clay"
templates={templates}
onDeleteTemplate={onDelete}
promptTemplates={[]}
onCreate={vi.fn()}
/>,
);
fireEvent.click(screen.getByRole('tab', { name: 'From template' }));
fireEvent.click(screen.getByLabelText(/delete template/i));
await screen.findByRole('alertdialog');
fireEvent.click(screen.getByRole('button', { name: 'Delete template' }));
await screen.findByText('Could not delete this template. Please try again.');
expect(screen.queryByRole('alertdialog')).not.toBeNull();
expect(onDelete).toHaveBeenCalledWith('tmpl-landing');
});
it('does not close the confirm dialog when the backdrop is clicked mid-delete', async () => {
let resolveDelete: (value: boolean) => void = () => {};
const onDelete = vi.fn(
() => new Promise<boolean>((resolve) => { resolveDelete = resolve; }),
);
render(
<NewProjectPanel
skills={skills}
designSystems={designSystems}
defaultDesignSystemId="clay"
templates={templates}
onDeleteTemplate={onDelete}
promptTemplates={[]}
onCreate={vi.fn()}
/>,
);
fireEvent.click(screen.getByRole('tab', { name: 'From template' }));
fireEvent.click(screen.getByLabelText(/delete template/i));
const dialog = await screen.findByRole('alertdialog');
fireEvent.click(screen.getByRole('button', { name: 'Delete template' }));
const backdrop = dialog.parentElement!;
fireEvent.click(backdrop);
expect(screen.queryByRole('alertdialog')).not.toBeNull();
expect(onDelete).toHaveBeenCalledTimes(1);
resolveDelete(true);
await waitFor(() => expect(screen.queryByRole('alertdialog')).toBeNull());
});
});