mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
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:
parent
96b6db14c2
commit
38b59fa4d9
27 changed files with 311 additions and 10 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -330,6 +330,7 @@ export function EntryView({
|
|||
designSystems={designSystems}
|
||||
projects={projects}
|
||||
templates={templates}
|
||||
onDeleteTemplate={onDeleteTemplate}
|
||||
promptTemplates={promptTemplates}
|
||||
defaultDesignSystemId={defaultDesignSystemId}
|
||||
connectors={connectors}
|
||||
|
|
|
|||
|
|
@ -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 } : {})}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -538,6 +538,11 @@ export const ar: Dict = {
|
|||
'عدل أي شيء - تغييراتك تنتقل إلى موجز الوكيل.',
|
||||
'newproj.promptTemplateBodyEmpty':
|
||||
'جسم فارغ - لن يحصل الوكيل على مرجع قالب.',
|
||||
'newproj.deleteTemplateTitle': 'حذف القالب',
|
||||
'newproj.deleteTemplateConfirm': 'هل تريد حذف "{name}"؟ لا يمكن التراجع عن هذا الإجراء.',
|
||||
'newproj.deleteTemplateConfirmCta': 'حذف القالب',
|
||||
'newproj.deleteTemplateError':
|
||||
'تعذر حذف هذا القالب. يرجى المحاولة مرة أخرى.',
|
||||
|
||||
'designs.subRecent': 'الأخيرة',
|
||||
'designs.subYours': 'تصاميمك',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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': 'هیچ — آزاد',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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': 'プロジェクトをフィルター',
|
||||
|
|
|
|||
|
|
@ -538,6 +538,11 @@ export const ko: Dict = {
|
|||
'내용을 수정할 수 있습니다. 변경된 내용은 에이전트의 브리프에 반영됩니다.',
|
||||
'newproj.promptTemplateBodyEmpty':
|
||||
'본문이 비어있습니다 — 에이전트에게 전달될 템플릿 참조가 없습니다.',
|
||||
'newproj.deleteTemplateTitle': '템플릿 삭제',
|
||||
'newproj.deleteTemplateConfirm': '"{name}" 템플릿을 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.',
|
||||
'newproj.deleteTemplateConfirmCta': '템플릿 삭제',
|
||||
'newproj.deleteTemplateError':
|
||||
'템플릿을 삭제할 수 없습니다. 다시 시도해 주세요.',
|
||||
|
||||
'designs.subRecent': '최근 항목',
|
||||
'designs.subYours': '내 디자인',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -550,6 +550,11 @@ export const ru: Dict = {
|
|||
'Меняйте всё что нужно — правки попадут в бриф агента.',
|
||||
'newproj.promptTemplateBodyEmpty':
|
||||
'Пустое тело — агент не получит шаблонную референцию.',
|
||||
'newproj.deleteTemplateTitle': 'Удалить шаблон',
|
||||
'newproj.deleteTemplateConfirm': 'Удалить «{name}»? Это действие невозможно отменить.',
|
||||
'newproj.deleteTemplateConfirmCta': 'Удалить шаблон',
|
||||
'newproj.deleteTemplateError':
|
||||
'Не удалось удалить шаблон. Попробуйте ещё раз.',
|
||||
|
||||
'designs.subRecent': 'Недавние',
|
||||
'designs.subYours': 'Ваши дизайны',
|
||||
|
|
|
|||
|
|
@ -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': 'ตัวกรอง',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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': 'Поки що немає підключених конекторів',
|
||||
|
|
|
|||
|
|
@ -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': '筛选项目',
|
||||
|
|
|
|||
|
|
@ -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': '篩選專案',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue