Merge Tag v0.2.16.0

This commit is contained in:
2025-06-04 16:02:37 +02:00
11 changed files with 201 additions and 105 deletions

View File

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

View File

@@ -1,6 +1,17 @@
'use client'; 'use client';
import { createUser, updateUser, updateUserPassword, uploadAvatar } from '@/app/actions/data'; import { createUser, updateUser, updateUserPassword, uploadAvatar } from '@/app/actions/data';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { toast } from '@/hooks/use-toast'; import { toast } from '@/hooks/use-toast';
import { serverSettingsAtom, usersAtom } from '@/lib/atoms'; import { serverSettingsAtom, usersAtom } from '@/lib/atoms';
import { useHelpers } from '@/lib/client-helpers'; import { useHelpers } from '@/lib/client-helpers';
@@ -57,6 +68,69 @@ export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps)
); );
const isEditing = !!user; 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) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -274,6 +348,38 @@ export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps)
</div> </div>
<div className="flex justify-end gap-2 pt-2"> <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 <Button
type="button" type="button"
variant="outline" variant="outline"

View File

@@ -33,62 +33,19 @@ function UserCard({
onEdit, onEdit,
showEdit, showEdit,
isCurrentUser, isCurrentUser,
currentLoggedInUserId, // For "don't delete self" check
onUserDeleted // Callback to update usersAtom
}: { }: {
user: User, user: User,
onSelect: () => void, onSelect: () => void,
onEdit: () => void, onEdit: () => void,
showEdit: boolean, showEdit: boolean,
isCurrentUser: boolean, isCurrentUser: boolean,
currentLoggedInUserId?: string,
onUserDeleted: (userId: string) => void,
}) { }) {
const t = useTranslations('UserSelectModal'); 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 ( return (
<div key={user.id} className="relative group"> <div key={user.id} className="relative group">
<button <button
onClick={onSelect} onClick={onSelect}
disabled={isDeleting} // Disable main button while deleting this user
className={cn( 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", "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" isCurrentUser && "ring-2 ring-primary"
@@ -116,48 +73,12 @@ function UserCard({
e.stopPropagation(); // Prevent card selection e.stopPropagation(); // Prevent card selection
onEdit(); 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" 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')} title={t('editUserTooltip')}
> >
<UserRoundPen className="h-4 w-4" /> <UserRoundPen className="h-4 w-4" />
</button> </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>
)} )}
</div> </div>
@@ -187,14 +108,12 @@ function UserSelectionView({
onUserSelect, onUserSelect,
onEditUser, onEditUser,
onCreateUser, onCreateUser,
onUserDeleted, // Pass through the delete handler
}: { }: {
users: User[], users: User[],
currentUserFromHook?: SafeUser, currentUserFromHook?: SafeUser,
onUserSelect: (userId: string) => void, onUserSelect: (userId: string) => void,
onEditUser: (userId: string) => void, onEditUser: (userId: string) => void,
onCreateUser: () => void, onCreateUser: () => void,
onUserDeleted: (userId: string) => void,
}) { }) {
return ( return (
<div className="grid grid-cols-3 gap-4 p-2 max-h-80 overflow-y-auto"> <div className="grid grid-cols-3 gap-4 p-2 max-h-80 overflow-y-auto">
@@ -208,8 +127,6 @@ function UserSelectionView({
onEdit={() => onEditUser(user.id)} onEdit={() => onEditUser(user.id)}
showEdit={!!currentUserFromHook?.isAdmin} showEdit={!!currentUserFromHook?.isAdmin}
isCurrentUser={false} // This card isn't the currently logged-in user for switching TO 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} />} {currentUserFromHook?.isAdmin && <AddUserButton onClick={onCreateUser} />}
@@ -227,12 +144,6 @@ export default function UserSelectModal({ onClose }: { onClose: () => void }) {
const users = usersData.users; const users = usersData.users;
const { currentUser } = useHelpers(); const { currentUser } = useHelpers();
const handleUserDeleted = (userIdToDelete: string) => {
setUsersData(prevData => ({
...prevData,
users: prevData.users.filter(u => u.id !== userIdToDelete)
}));
};
const handleUserSelect = (userId: string) => { const handleUserSelect = (userId: string) => {
setSelectedUser(userId); setSelectedUser(userId);
@@ -278,7 +189,6 @@ export default function UserSelectModal({ onClose }: { onClose: () => void }) {
onUserSelect={handleUserSelect} onUserSelect={handleUserSelect}
onEditUser={handleEditUser} onEditUser={handleEditUser}
onCreateUser={handleCreateUser} onCreateUser={handleCreateUser}
onUserDeleted={handleUserDeleted}
/> />
) : isCreating || isEditing ? ( ) : isCreating || isEditing ? (
<UserForm <UserForm

View File

@@ -270,6 +270,12 @@
"actionUpdate": "aktualisieren", "actionUpdate": "aktualisieren",
"actionCreate": "erstellen", "actionCreate": "erstellen",
"errorFailedUserAction": "Fehler beim {action} des Benutzers", "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", "errorTitle": "Fehler",
"errorFileSizeLimit": "Die Dateigröße muss kleiner als 5MB sein", "errorFileSizeLimit": "Die Dateigröße muss kleiner als 5MB sein",
"toastAvatarUploadedTitle": "Avatar hochgeladen", "toastAvatarUploadedTitle": "Avatar hochgeladen",
@@ -287,7 +293,13 @@
"disablePasswordLabel": "Passwort deaktivieren", "disablePasswordLabel": "Passwort deaktivieren",
"cancelButton": "Abbrechen", "cancelButton": "Abbrechen",
"saveChangesButton": "Änderungen speichern", "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": { "ViewToggle": {
"habitsLabel": "Gewohnheiten", "habitsLabel": "Gewohnheiten",

View File

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

View File

@@ -270,6 +270,12 @@
"actionUpdate": "actualizar", "actionUpdate": "actualizar",
"actionCreate": "crear", "actionCreate": "crear",
"errorFailedUserAction": "Error al {action} usuario", "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", "errorTitle": "Error",
"errorFileSizeLimit": "El tamaño del archivo debe ser menor a 5MB", "errorFileSizeLimit": "El tamaño del archivo debe ser menor a 5MB",
"toastAvatarUploadedTitle": "Avatar subido", "toastAvatarUploadedTitle": "Avatar subido",
@@ -287,7 +293,13 @@
"disablePasswordLabel": "Desactivar contraseña", "disablePasswordLabel": "Desactivar contraseña",
"cancelButton": "Cancelar", "cancelButton": "Cancelar",
"saveChangesButton": "Guardar cambios", "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": { "ViewToggle": {
"habitsLabel": "Hábitos", "habitsLabel": "Hábitos",

View File

@@ -270,6 +270,12 @@
"actionUpdate": "mise à jour", "actionUpdate": "mise à jour",
"actionCreate": "création", "actionCreate": "création",
"errorFailedUserAction": "Échec de la {action} de l'utilisateur", "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", "errorTitle": "Erreur",
"errorFileSizeLimit": "La taille du fichier doit être inférieure à 5MB", "errorFileSizeLimit": "La taille du fichier doit être inférieure à 5MB",
"toastAvatarUploadedTitle": "Avatar téléchargé", "toastAvatarUploadedTitle": "Avatar téléchargé",
@@ -287,7 +293,13 @@
"disablePasswordLabel": "Désactiver le mot de passe", "disablePasswordLabel": "Désactiver le mot de passe",
"cancelButton": "Annuler", "cancelButton": "Annuler",
"saveChangesButton": "Sauvegarder les modifications", "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": { "ViewToggle": {
"habitsLabel": "Habitudes", "habitsLabel": "Habitudes",

View File

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

View File

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

View File

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

View File

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