mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-03-08 03:29:49 +01:00
Merge Tag v0.2.16.0
This commit is contained in:
@@ -1,6 +1,17 @@
|
||||
'use client';
|
||||
|
||||
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 { serverSettingsAtom, usersAtom } from '@/lib/atoms';
|
||||
import { useHelpers } from '@/lib/client-helpers';
|
||||
@@ -57,6 +68,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();
|
||||
|
||||
@@ -93,11 +167,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
|
||||
@@ -247,7 +321,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"
|
||||
@@ -263,7 +337,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}
|
||||
@@ -274,6 +348,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"
|
||||
|
||||
@@ -33,62 +33,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"
|
||||
@@ -116,48 +73,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>
|
||||
@@ -187,14 +108,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">
|
||||
@@ -208,8 +127,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} />}
|
||||
@@ -227,12 +144,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);
|
||||
@@ -278,7 +189,6 @@ export default function UserSelectModal({ onClose }: { onClose: () => void }) {
|
||||
onUserSelect={handleUserSelect}
|
||||
onEditUser={handleEditUser}
|
||||
onCreateUser={handleCreateUser}
|
||||
onUserDeleted={handleUserDeleted}
|
||||
/>
|
||||
) : isCreating || isEditing ? (
|
||||
<UserForm
|
||||
|
||||
Reference in New Issue
Block a user