diff --git a/apps/web/src/components/EntryShell.tsx b/apps/web/src/components/EntryShell.tsx index 622a83d17..5fcebb371 100644 --- a/apps/web/src/components/EntryShell.tsx +++ b/apps/web/src/components/EntryShell.tsx @@ -201,6 +201,7 @@ interface Props { designSystems: DesignSystemSummary[]; projects: Project[]; templates: ProjectTemplate[]; + onDeleteTemplate?: (id: string) => Promise; 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} diff --git a/apps/web/src/components/EntryView.tsx b/apps/web/src/components/EntryView.tsx index e74431815..013b8cc23 100644 --- a/apps/web/src/components/EntryView.tsx +++ b/apps/web/src/components/EntryView.tsx @@ -330,6 +330,7 @@ export function EntryView({ designSystems={designSystems} projects={projects} templates={templates} + onDeleteTemplate={onDeleteTemplate} promptTemplates={promptTemplates} defaultDesignSystemId={defaultDesignSystemId} connectors={connectors} diff --git a/apps/web/src/components/NewProjectModal.tsx b/apps/web/src/components/NewProjectModal.tsx index 6f5f4a408..80a53ebfa 100644 --- a/apps/web/src/components/NewProjectModal.tsx +++ b/apps/web/src/components/NewProjectModal.tsx @@ -26,6 +26,7 @@ interface Props { designSystems: DesignSystemSummary[]; defaultDesignSystemId: string | null; templates: ProjectTemplate[]; + onDeleteTemplate?: (id: string) => Promise; promptTemplates: PromptTemplateSummary[]; mediaProviders?: Record; 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 } : {})} diff --git a/apps/web/src/components/NewProjectPanel.tsx b/apps/web/src/components/NewProjectPanel.tsx index bcb256eec..61ce53441 100644 --- a/apps/web/src/components/NewProjectPanel.tsx +++ b/apps/web/src/components/NewProjectPanel.tsx @@ -1325,6 +1325,37 @@ function TemplatePicker({ onDelete?: (id: string) => Promise; }) { 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 (
@@ -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({ })}
)} + {confirmDelete ? ( +
+
e.stopPropagation()} + role="alertdialog" + aria-modal="true" + > +

{t('newproj.deleteTemplateTitle')}

+

+ {t('newproj.deleteTemplateConfirm', { name: confirmDelete.name })} +

+ {deleteError ? ( +

+ {t('newproj.deleteTemplateError')} +

+ ) : null} +
+ + +
+
+
+ ) : null} ); } diff --git a/apps/web/src/i18n/locales/ar.ts b/apps/web/src/i18n/locales/ar.ts index e82ad8a68..eb97e57b5 100644 --- a/apps/web/src/i18n/locales/ar.ts +++ b/apps/web/src/i18n/locales/ar.ts @@ -538,6 +538,11 @@ export const ar: Dict = { 'عدل أي شيء - تغييراتك تنتقل إلى موجز الوكيل.', 'newproj.promptTemplateBodyEmpty': 'جسم فارغ - لن يحصل الوكيل على مرجع قالب.', + 'newproj.deleteTemplateTitle': 'حذف القالب', + 'newproj.deleteTemplateConfirm': 'هل تريد حذف "{name}"؟ لا يمكن التراجع عن هذا الإجراء.', + 'newproj.deleteTemplateConfirmCta': 'حذف القالب', + 'newproj.deleteTemplateError': + 'تعذر حذف هذا القالب. يرجى المحاولة مرة أخرى.', 'designs.subRecent': 'الأخيرة', 'designs.subYours': 'تصاميمك', diff --git a/apps/web/src/i18n/locales/de.ts b/apps/web/src/i18n/locales/de.ts index c9b570c59..3048e6135 100644 --- a/apps/web/src/i18n/locales/de.ts +++ b/apps/web/src/i18n/locales/de.ts @@ -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', diff --git a/apps/web/src/i18n/locales/en.ts b/apps/web/src/i18n/locales/en.ts index 0ff7a3963..c96a0f499 100644 --- a/apps/web/src/i18n/locales/en.ts +++ b/apps/web/src/i18n/locales/en.ts @@ -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', diff --git a/apps/web/src/i18n/locales/es-ES.ts b/apps/web/src/i18n/locales/es-ES.ts index 6e92fae8e..d3a1ef6d0 100644 --- a/apps/web/src/i18n/locales/es-ES.ts +++ b/apps/web/src/i18n/locales/es-ES.ts @@ -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', diff --git a/apps/web/src/i18n/locales/fa.ts b/apps/web/src/i18n/locales/fa.ts index 6df20e314..3236a874c 100644 --- a/apps/web/src/i18n/locales/fa.ts +++ b/apps/web/src/i18n/locales/fa.ts @@ -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': 'هیچ — آزاد', diff --git a/apps/web/src/i18n/locales/fr.ts b/apps/web/src/i18n/locales/fr.ts index 76ac3f118..e70a7f2e8 100644 --- a/apps/web/src/i18n/locales/fr.ts +++ b/apps/web/src/i18n/locales/fr.ts @@ -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', diff --git a/apps/web/src/i18n/locales/hu.ts b/apps/web/src/i18n/locales/hu.ts index 8557a2310..0a85938f6 100644 --- a/apps/web/src/i18n/locales/hu.ts +++ b/apps/web/src/i18n/locales/hu.ts @@ -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', diff --git a/apps/web/src/i18n/locales/id.ts b/apps/web/src/i18n/locales/id.ts index 78d24424f..4942a7220 100644 --- a/apps/web/src/i18n/locales/id.ts +++ b/apps/web/src/i18n/locales/id.ts @@ -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', diff --git a/apps/web/src/i18n/locales/it.ts b/apps/web/src/i18n/locales/it.ts index 07dc582d6..de6fb9125 100644 --- a/apps/web/src/i18n/locales/it.ts +++ b/apps/web/src/i18n/locales/it.ts @@ -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', diff --git a/apps/web/src/i18n/locales/ja.ts b/apps/web/src/i18n/locales/ja.ts index bf330f9ee..62d76ffb6 100644 --- a/apps/web/src/i18n/locales/ja.ts +++ b/apps/web/src/i18n/locales/ja.ts @@ -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': 'プロジェクトをフィルター', diff --git a/apps/web/src/i18n/locales/ko.ts b/apps/web/src/i18n/locales/ko.ts index e480aae77..a6a0ed89d 100644 --- a/apps/web/src/i18n/locales/ko.ts +++ b/apps/web/src/i18n/locales/ko.ts @@ -538,6 +538,11 @@ export const ko: Dict = { '내용을 수정할 수 있습니다. 변경된 내용은 에이전트의 브리프에 반영됩니다.', 'newproj.promptTemplateBodyEmpty': '본문이 비어있습니다 — 에이전트에게 전달될 템플릿 참조가 없습니다.', + 'newproj.deleteTemplateTitle': '템플릿 삭제', + 'newproj.deleteTemplateConfirm': '"{name}" 템플릿을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.', + 'newproj.deleteTemplateConfirmCta': '템플릿 삭제', + 'newproj.deleteTemplateError': + '템플릿을 삭제할 수 없습니다. 다시 시도해 주세요.', 'designs.subRecent': '최근 항목', 'designs.subYours': '내 디자인', diff --git a/apps/web/src/i18n/locales/pl.ts b/apps/web/src/i18n/locales/pl.ts index f9b933e92..3c41cd84d 100644 --- a/apps/web/src/i18n/locales/pl.ts +++ b/apps/web/src/i18n/locales/pl.ts @@ -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', diff --git a/apps/web/src/i18n/locales/pt-BR.ts b/apps/web/src/i18n/locales/pt-BR.ts index e7fd7078e..6f36a8127 100644 --- a/apps/web/src/i18n/locales/pt-BR.ts +++ b/apps/web/src/i18n/locales/pt-BR.ts @@ -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', diff --git a/apps/web/src/i18n/locales/ru.ts b/apps/web/src/i18n/locales/ru.ts index 29cd34d60..fb1e08144 100644 --- a/apps/web/src/i18n/locales/ru.ts +++ b/apps/web/src/i18n/locales/ru.ts @@ -550,6 +550,11 @@ export const ru: Dict = { 'Меняйте всё что нужно — правки попадут в бриф агента.', 'newproj.promptTemplateBodyEmpty': 'Пустое тело — агент не получит шаблонную референцию.', + 'newproj.deleteTemplateTitle': 'Удалить шаблон', + 'newproj.deleteTemplateConfirm': 'Удалить «{name}»? Это действие невозможно отменить.', + 'newproj.deleteTemplateConfirmCta': 'Удалить шаблон', + 'newproj.deleteTemplateError': + 'Не удалось удалить шаблон. Попробуйте ещё раз.', 'designs.subRecent': 'Недавние', 'designs.subYours': 'Ваши дизайны', diff --git a/apps/web/src/i18n/locales/th.ts b/apps/web/src/i18n/locales/th.ts index 9b02a4bec..fd3621552 100644 --- a/apps/web/src/i18n/locales/th.ts +++ b/apps/web/src/i18n/locales/th.ts @@ -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': 'ตัวกรอง', diff --git a/apps/web/src/i18n/locales/tr.ts b/apps/web/src/i18n/locales/tr.ts index 4f21647c4..e554c340e 100644 --- a/apps/web/src/i18n/locales/tr.ts +++ b/apps/web/src/i18n/locales/tr.ts @@ -528,6 +528,11 @@ export const tr: Dict = { 'İstediğin her şeyi düzenle — değişikliklerin ajan brief’ine 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', diff --git a/apps/web/src/i18n/locales/uk.ts b/apps/web/src/i18n/locales/uk.ts index 672147661..945cc74a8 100644 --- a/apps/web/src/i18n/locales/uk.ts +++ b/apps/web/src/i18n/locales/uk.ts @@ -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': 'Поки що немає підключених конекторів', diff --git a/apps/web/src/i18n/locales/zh-CN.ts b/apps/web/src/i18n/locales/zh-CN.ts index cc3678668..4e1ca6097 100644 --- a/apps/web/src/i18n/locales/zh-CN.ts +++ b/apps/web/src/i18n/locales/zh-CN.ts @@ -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': '筛选项目', diff --git a/apps/web/src/i18n/locales/zh-TW.ts b/apps/web/src/i18n/locales/zh-TW.ts index ac7e3c34f..2835e7455 100644 --- a/apps/web/src/i18n/locales/zh-TW.ts +++ b/apps/web/src/i18n/locales/zh-TW.ts @@ -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': '篩選專案', diff --git a/apps/web/src/i18n/types.ts b/apps/web/src/i18n/types.ts index f2dda9dc5..489a68fd6 100644 --- a/apps/web/src/i18n/types.ts +++ b/apps/web/src/i18n/types.ts @@ -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; diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 49807b57d..26e67e97b 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -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; diff --git a/apps/web/tests/components/NewProjectModal.test.tsx b/apps/web/tests/components/NewProjectModal.test.tsx index 1bf793cc0..1072493f1 100644 --- a/apps/web/tests/components/NewProjectModal.test.tsx +++ b/apps/web/tests/components/NewProjectModal.test.tsx @@ -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( + {}} + 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'); + }); +}); diff --git a/apps/web/tests/components/NewProjectPanel.test.tsx b/apps/web/tests/components/NewProjectPanel.test.tsx index e694e5891..7c3116f9b 100644 --- a/apps/web/tests/components/NewProjectPanel.test.tsx +++ b/apps/web/tests/components/NewProjectPanel.test.tsx @@ -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( { ); 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( + , + ); + + 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( + , + ); + + 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((resolve) => { resolveDelete = resolve; }), + ); + render( + , + ); + + 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()); + }); });