feat: Move delete account button to user edit modal (#144)

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
This commit is contained in:
Doh
2025-05-25 17:30:41 -04:00
committed by GitHub
parent 82f45343ae
commit 1a286a99f4
11 changed files with 201 additions and 105 deletions

View File

@@ -1,5 +1,12 @@
# Changelog
## Version 0.2.16
### Improved
* move delete user button to user form
* disable deleting user on demo instance
## Version 0.2.15
### Improved

View File

@@ -5,6 +5,17 @@ import { passwordSchema, usernameSchema } from '@/lib/zod';
import { useTranslations } from 'next-intl';
import { Input } from './ui/input';
import { Button } from './ui/button';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Label } from './ui/label';
import { Switch } from './ui/switch';
import { Permission } from '@/lib/types';
@@ -58,6 +69,69 @@ export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps)
);
const isEditing = !!user;
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const handleDeleteUser = async () => {
if (!user) return;
if (serverSettings.isDemo) {
toast({
title: t('errorTitle'),
description: t('toastDemoDeleteDisabled'),
variant: 'destructive',
});
return;
}
if (currentUser && currentUser.id === user.id) {
toast({
title: t('errorTitle'),
description: t('toastCannotDeleteSelf'),
variant: 'destructive',
});
return;
}
setIsDeleting(true);
try {
const response = await fetch('/api/user/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: user.id }),
});
if (response.ok) {
setUsersData(prev => ({
...prev,
users: prev.users.filter(u => u.id !== user.id),
}));
toast({
title: t('toastUserDeletedTitle'),
description: t('toastUserDeletedDescription', { username: user.username }),
variant: 'default'
});
onSuccess();
} else {
const errorData = await response.json();
toast({
title: t('errorTitle'),
description: errorData.error || t('genericError'),
variant: 'destructive',
});
}
} catch (error) {
toast({
title: t('errorTitle'),
description: t('networkError'),
variant: 'destructive',
});
} finally {
setIsDeleting(false);
setShowDeleteConfirm(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -94,11 +168,11 @@ export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps)
setUsersData(prev => ({
...prev,
users: prev.users.map(u =>
u.id === user.id ? {
...u,
username,
avatarPath,
permissions,
u.id === user.id ? {
...u,
username,
avatarPath,
permissions,
isAdmin,
password: disablePassword ? '' : (password || u.password) // use the correct password to update atom
} : u
@@ -248,7 +322,7 @@ export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps)
<p className="text-sm text-red-500">{t('demoPasswordDisabledMessage')}</p>
)}
</div>
<div className="flex items-center space-x-2">
<Switch
id="disable-password"
@@ -264,7 +338,7 @@ export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps)
<p className="text-sm text-red-500 bg-red-50 dark:bg-red-950/50 p-2 rounded">{error}</p>
)}
{currentUser && currentUser.isAdmin && <PermissionSelector
permissions={permissions}
isAdmin={isAdmin}
@@ -275,6 +349,38 @@ export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps)
</div>
<div className="flex justify-end gap-2 pt-2">
{isEditing && (
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
<AlertDialogTrigger asChild>
<Button
type="button"
variant="destructive"
className="mr-auto"
disabled={serverSettings.isDemo || isDeleting}
>
{isDeleting ? t('deletingButtonText') : t('deleteAccountButton')}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('areYouSure')}</AlertDialogTitle>
<AlertDialogDescription>
{t('deleteUserConfirmation', { username: user.username })}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>{t('cancel')}</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteUser}
disabled={isDeleting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isDeleting ? t('deletingButtonText') : t('confirmDeleteButtonText')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
<Button
type="button"
variant="outline"

View File

@@ -36,62 +36,19 @@ function UserCard({
onEdit,
showEdit,
isCurrentUser,
currentLoggedInUserId, // For "don't delete self" check
onUserDeleted // Callback to update usersAtom
}: {
user: User,
onSelect: () => void,
onEdit: () => void,
showEdit: boolean,
isCurrentUser: boolean,
currentLoggedInUserId?: string,
onUserDeleted: (userId: string) => void,
}) {
const t = useTranslations('UserSelectModal');
const tWarning = useTranslations('Warning');
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const handleDeleteUser = async () => {
setIsDeleting(true);
try {
const response = await fetch('/api/user/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: user.id }),
});
if (response.ok) {
toast({
title: t('deleteUserSuccessTitle'),
description: t('deleteUserSuccessDescription', { username: user.username }),
});
onUserDeleted(user.id);
} else {
const errorData = await response.json();
toast({
title: t('deleteUserErrorTitle'),
description: errorData.error || t('genericError'),
variant: 'destructive',
});
}
} catch (error) {
toast({
title: t('deleteUserErrorTitle'),
description: t('networkError'),
variant: 'destructive',
});
} finally {
setIsDeleting(false);
setShowDeleteConfirm(false);
}
};
return (
<div key={user.id} className="relative group">
<button
onClick={onSelect}
disabled={isDeleting} // Disable main button while deleting this user
className={cn(
"flex flex-col items-center gap-2 p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors w-full",
isCurrentUser && "ring-2 ring-primary"
@@ -119,48 +76,12 @@ function UserCard({
e.stopPropagation(); // Prevent card selection
onEdit();
}}
disabled={isDeleting}
className="p-1 rounded-full bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 transition-colors"
title={t('editUserTooltip')}
>
<UserRoundPen className="h-4 w-4" />
</button>
)}
{showEdit && user.id !== currentLoggedInUserId && (
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
<AlertDialogTrigger asChild>
<button
onClick={(e) => {
e.stopPropagation(); // Prevent card selection
setShowDeleteConfirm(true);
}}
disabled={isDeleting}
className="p-1 rounded-full bg-red-200 hover:bg-red-300 dark:bg-red-700 dark:hover:bg-red-600 transition-colors text-red-600 dark:text-red-300"
title={t('deleteUserTooltip')}
>
<Trash2 className="h-4 w-4" />
</button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{tWarning('areYouSure')}</AlertDialogTitle>
<AlertDialogDescription>
{t('deleteUserConfirmation', { username: user.username })}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={(e) => { e.stopPropagation(); setShowDeleteConfirm(false);}} disabled={isDeleting}>{tWarning('cancel')}</AlertDialogCancel>
<AlertDialogAction
onClick={(e) => { e.stopPropagation(); handleDeleteUser();}}
disabled={isDeleting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isDeleting ? t('deletingButtonText') : t('confirmDeleteButtonText')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
</div>
)}
</div>
@@ -190,14 +111,12 @@ function UserSelectionView({
onUserSelect,
onEditUser,
onCreateUser,
onUserDeleted, // Pass through the delete handler
}: {
users: User[],
currentUserFromHook?: SafeUser,
onUserSelect: (userId: string) => void,
onEditUser: (userId: string) => void,
onCreateUser: () => void,
onUserDeleted: (userId: string) => void,
}) {
return (
<div className="grid grid-cols-3 gap-4 p-2 max-h-80 overflow-y-auto">
@@ -211,8 +130,6 @@ function UserSelectionView({
onEdit={() => onEditUser(user.id)}
showEdit={!!currentUserFromHook?.isAdmin}
isCurrentUser={false} // This card isn't the currently logged-in user for switching TO
currentLoggedInUserId={currentUserFromHook?.id} // For the "don't delete self" check
onUserDeleted={onUserDeleted}
/>
))}
{currentUserFromHook?.isAdmin && <AddUserButton onClick={onCreateUser} />}
@@ -230,12 +147,6 @@ export default function UserSelectModal({ onClose }: { onClose: () => void }) {
const users = usersData.users;
const { currentUser } = useHelpers();
const handleUserDeleted = (userIdToDelete: string) => {
setUsersData(prevData => ({
...prevData,
users: prevData.users.filter(u => u.id !== userIdToDelete)
}));
};
const handleUserSelect = (userId: string) => {
setSelectedUser(userId);
@@ -281,7 +192,6 @@ export default function UserSelectModal({ onClose }: { onClose: () => void }) {
onUserSelect={handleUserSelect}
onEditUser={handleEditUser}
onCreateUser={handleCreateUser}
onUserDeleted={handleUserDeleted}
/>
) : isCreating || isEditing ? (
<UserForm

View File

@@ -270,6 +270,12 @@
"actionUpdate": "aktualisieren",
"actionCreate": "erstellen",
"errorFailedUserAction": "Fehler beim {action} des Benutzers",
"toastDemoDeleteDisabled": "Löschen ist in der Demo-Instanz deaktiviert",
"toastCannotDeleteSelf": "Sie können Ihr eigenes Konto nicht löschen",
"confirmDeleteUser": "Sind Sie sicher, dass Sie den Benutzer {username} löschen möchten?",
"toastUserDeletedTitle": "Benutzer gelöscht",
"toastUserDeletedDescription": "Benutzer {username} wurde erfolgreich gelöscht",
"toastDeleteUserFailed": "Fehler beim Löschen des Benutzers: {error}",
"errorTitle": "Fehler",
"errorFileSizeLimit": "Die Dateigröße muss kleiner als 5MB sein",
"toastAvatarUploadedTitle": "Avatar hochgeladen",
@@ -287,7 +293,13 @@
"disablePasswordLabel": "Passwort deaktivieren",
"cancelButton": "Abbrechen",
"saveChangesButton": "Änderungen speichern",
"createUserButton": "Benutzer erstellen"
"createUserButton": "Benutzer erstellen",
"deleteAccountButton": "Konto löschen",
"deletingButtonText": "Wird gelöscht...",
"areYouSure": "Sind Sie sicher?",
"deleteUserConfirmation": "Sind Sie sicher, dass Sie den Benutzer {username} löschen möchten?",
"cancel": "Abbrechen",
"confirmDeleteButtonText": "Löschen"
},
"ViewToggle": {
"habitsLabel": "Gewohnheiten",

View File

@@ -287,7 +287,8 @@
"disablePasswordLabel": "Disable password",
"cancelButton": "Cancel",
"saveChangesButton": "Save Changes",
"createUserButton": "Create User"
"createUserButton": "Create User",
"deleteAccountButton": "Delete Account"
},
"ViewToggle": {
"habitsLabel": "Habits",

View File

@@ -270,6 +270,12 @@
"actionUpdate": "actualizar",
"actionCreate": "crear",
"errorFailedUserAction": "Error al {action} usuario",
"toastDemoDeleteDisabled": "La eliminación está deshabilitada en la instancia demo",
"toastCannotDeleteSelf": "No puedes eliminar tu propia cuenta",
"confirmDeleteUser": "¿Estás seguro de que deseas eliminar al usuario {username}?",
"toastUserDeletedTitle": "Usuario eliminado",
"toastUserDeletedDescription": "El usuario {username} ha sido eliminado correctamente",
"toastDeleteUserFailed": "Error al eliminar el usuario: {error}",
"errorTitle": "Error",
"errorFileSizeLimit": "El tamaño del archivo debe ser menor a 5MB",
"toastAvatarUploadedTitle": "Avatar subido",
@@ -287,7 +293,13 @@
"disablePasswordLabel": "Desactivar contraseña",
"cancelButton": "Cancelar",
"saveChangesButton": "Guardar cambios",
"createUserButton": "Crear usuario"
"createUserButton": "Crear usuario",
"deleteAccountButton": "Eliminar cuenta",
"deletingButtonText": "Eliminando...",
"areYouSure": "¿Estás seguro?",
"deleteUserConfirmation": "¿Estás seguro de que deseas eliminar al usuario {username}?",
"cancel": "Cancelar",
"confirmDeleteButtonText": "Eliminar"
},
"ViewToggle": {
"habitsLabel": "Hábitos",

View File

@@ -270,6 +270,12 @@
"actionUpdate": "mise à jour",
"actionCreate": "création",
"errorFailedUserAction": "Échec de la {action} de l'utilisateur",
"toastDemoDeleteDisabled": "La suppression est désactivée dans la version de démonstration",
"toastCannotDeleteSelf": "Vous ne pouvez pas supprimer votre propre compte",
"confirmDeleteUser": "Êtes-vous sûr de vouloir supprimer l'utilisateur {username}?",
"toastUserDeletedTitle": "Utilisateur supprimé",
"toastUserDeletedDescription": "L'utilisateur {username} a été supprimé avec succès",
"toastDeleteUserFailed": "Échec de la suppression de l'utilisateur : {error}",
"errorTitle": "Erreur",
"errorFileSizeLimit": "La taille du fichier doit être inférieure à 5MB",
"toastAvatarUploadedTitle": "Avatar téléchargé",
@@ -287,7 +293,13 @@
"disablePasswordLabel": "Désactiver le mot de passe",
"cancelButton": "Annuler",
"saveChangesButton": "Sauvegarder les modifications",
"createUserButton": "Créer un utilisateur"
"createUserButton": "Créer un utilisateur",
"deleteAccountButton": "Supprimer le compte",
"deletingButtonText": "Suppression en cours...",
"areYouSure": "Êtes-vous sûr ?",
"deleteUserConfirmation": "Êtes-vous sûr de vouloir supprimer l'utilisateur {username} ?",
"cancel": "Annuler",
"confirmDeleteButtonText": "Supprimer"
},
"ViewToggle": {
"habitsLabel": "Habitudes",

View File

@@ -270,6 +270,12 @@
"actionUpdate": "更新",
"actionCreate": "作成",
"errorFailedUserAction": "ユーザーの{action}に失敗しました",
"toastDemoDeleteDisabled": "デモインスタンスでは削除が無効になっています",
"toastCannotDeleteSelf": "自分のアカウントは削除できません",
"confirmDeleteUser": "ユーザー {username} を削除してもよろしいですか?",
"toastUserDeletedTitle": "ユーザーが削除されました",
"toastUserDeletedDescription": "ユーザー {username} は正常に削除されました",
"toastDeleteUserFailed": "ユーザーの削除に失敗しました: {error}",
"errorTitle": "エラー",
"errorFileSizeLimit": "ファイルサイズは5MB以下である必要があります",
"toastAvatarUploadedTitle": "アバターをアップロードしました",
@@ -287,7 +293,13 @@
"disablePasswordLabel": "パスワードを無効化",
"cancelButton": "キャンセル",
"saveChangesButton": "変更を保存",
"createUserButton": "ユーザーを作成"
"createUserButton": "ユーザーを作成",
"deleteAccountButton": "アカウントを削除",
"deletingButtonText": "削除中...",
"areYouSure": "本当によろしいですか?",
"deleteUserConfirmation": "ユーザー {username} を削除してもよろしいですか?",
"cancel": "キャンセル",
"confirmDeleteButtonText": "削除"
},
"ViewToggle": {
"habitsLabel": "習慣",

View File

@@ -270,6 +270,12 @@
"actionUpdate": "обновить",
"actionCreate": "создать",
"errorFailedUserAction": "Не удалось {action} пользователя",
"toastDemoDeleteDisabled": "Удаление отключено в демо-версии",
"toastCannotDeleteSelf": "Вы не можете удалить свою учетную запись",
"confirmDeleteUser": "Вы уверены, что хотите удалить пользователя {username}?",
"toastUserDeletedTitle": "Пользователь удален",
"toastUserDeletedDescription": "Пользователь {username} успешно удален",
"toastDeleteUserFailed": "Не удалось удалить пользователя: {error}",
"errorTitle": "Ошибка",
"errorFileSizeLimit": "Размер файла должен быть менее 5 МБ",
"toastAvatarUploadedTitle": "Аватар загружен",
@@ -287,7 +293,13 @@
"disablePasswordLabel": "Отключить пароль",
"cancelButton": "Отмена",
"saveChangesButton": "Сохранить изменения",
"createUserButton": "Создать пользователя"
"createUserButton": "Создать пользователя",
"deleteAccountButton": "Удалить аккаунт",
"deletingButtonText": "Удаление...",
"areYouSure": "Вы уверены?",
"deleteUserConfirmation": "Вы уверены, что хотите удалить пользователя {username}?",
"cancel": "Отмена",
"confirmDeleteButtonText": "Удалить"
},
"ViewToggle": {
"habitsLabel": "Привычки",

View File

@@ -270,6 +270,12 @@
"actionUpdate": "更新",
"actionCreate": "创建",
"errorFailedUserAction": "用户 {action} 失败",
"toastDemoDeleteDisabled": "在演示实例中删除已禁用",
"toastCannotDeleteSelf": "您不能删除自己的帐户",
"confirmDeleteUser": "您确定要删除用户 {username} 吗?",
"toastUserDeletedTitle": "用户已删除",
"toastUserDeletedDescription": "用户 {username} 已成功删除",
"toastDeleteUserFailed": "删除用户失败: {error}",
"errorTitle": "错误",
"errorFileSizeLimit": "文件大小必须小于 5MB",
"toastAvatarUploadedTitle": "头像已上传",
@@ -287,7 +293,13 @@
"disablePasswordLabel": "禁用密码",
"cancelButton": "取消",
"saveChangesButton": "保存更改",
"createUserButton": "创建用户"
"createUserButton": "创建用户",
"deleteAccountButton": "删除账户",
"deletingButtonText": "正在删除...",
"areYouSure": "您确定吗?",
"deleteUserConfirmation": "您确定要删除用户 {username} 吗?",
"cancel": "取消",
"confirmDeleteButtonText": "删除"
},
"ViewToggle": {
"habitsLabel": "习惯",

View File

@@ -1,6 +1,6 @@
{
"name": "habittrove",
"version": "0.2.15",
"version": "0.2.16",
"private": true,
"scripts": {
"dev": "next dev --turbopack",