From 5cc41c4e92dae1b023ed3333f89225e5e6aed94e Mon Sep 17 00:00:00 2001 From: dongmucat <1127093059@qq.com> Date: Wed, 6 May 2026 14:21:18 +0800 Subject: [PATCH] feat(namespace): implement frontend delete functionality with reason input Add complete frontend implementation for namespace deletion: - API layer: add delete() method to namespaceApi - Hooks layer: add useDeleteNamespace() mutation hook - UI layer: add DeleteNamespaceDialog component with reason input - Add delete button in my-namespaces page (only shown when canDelete is true) - Add i18n keys for delete confirmation, reason input, and success/error messages - Update ManagedNamespace type to include canDelete permission flag - Fix test mocks to include canDelete property The delete dialog requires users to provide a deletion reason before confirming, following the backend requirement. After successful deletion, users are redirected to the my-namespaces page. --- web/src/api/client.ts | 10 ++ web/src/api/types.ts | 1 + .../namespace/delete-namespace-dialog.tsx | 97 +++++++++++++++++++ web/src/features/review/review-paths.test.ts | 4 + web/src/i18n/locales/en.json | 18 ++++ web/src/i18n/locales/zh.json | 18 ++++ web/src/pages/dashboard/my-namespaces.tsx | 55 ++++++++++- web/src/shared/hooks/use-namespace-queries.ts | 11 +++ 8 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 web/src/features/namespace/delete-namespace-dialog.tsx diff --git a/web/src/api/client.ts b/web/src/api/client.ts index 0573fa18..5f40ba0b 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -732,6 +732,16 @@ export const namespaceApi = { headers: await ensureCsrfHeaders(), }) }, + + async delete(slug: string, reason: string): Promise { + await fetchJson(`${WEB_API_PREFIX}/namespaces/${normalizeNamespaceSlug(slug)}`, { + method: 'DELETE', + headers: await ensureCsrfHeaders({ + 'Content-Type': 'application/json', + }), + body: JSON.stringify({ reason: reason.trim() }), + }) + }, } export const tokenApi = { diff --git a/web/src/api/types.ts b/web/src/api/types.ts index 0cba6fcc..fd7714b6 100644 --- a/web/src/api/types.ts +++ b/web/src/api/types.ts @@ -121,6 +121,7 @@ export interface ManagedNamespace extends Namespace { canUnfreeze: boolean canArchive: boolean canRestore: boolean + canDelete: boolean } export interface NamespaceMember { diff --git a/web/src/features/namespace/delete-namespace-dialog.tsx b/web/src/features/namespace/delete-namespace-dialog.tsx new file mode 100644 index 00000000..af64ffaa --- /dev/null +++ b/web/src/features/namespace/delete-namespace-dialog.tsx @@ -0,0 +1,97 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/shared/ui/dialog' +import { Button } from '@/shared/ui/button' +import { Textarea } from '@/shared/ui/textarea' +import { Label } from '@/shared/ui/label' + +interface DeleteNamespaceDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + namespaceName: string + onConfirm: (reason: string) => void | Promise +} + +export function DeleteNamespaceDialog({ + open, + onOpenChange, + namespaceName, + onConfirm, +}: DeleteNamespaceDialogProps) { + const { t } = useTranslation() + const [reason, setReason] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + + const handleConfirm = async () => { + if (!reason.trim()) { + return + } + + setIsSubmitting(true) + try { + await onConfirm(reason) + setReason('') + onOpenChange(false) + } finally { + setIsSubmitting(false) + } + } + + const handleOpenChange = (newOpen: boolean) => { + if (!isSubmitting) { + if (!newOpen) { + setReason('') + } + onOpenChange(newOpen) + } + } + + return ( + + + + {t('namespace.delete.confirm.title')} + + {t('namespace.delete.confirm.description', { name: namespaceName })} + + +
+
+ +