feat(admin): add context menu for user actions with dynamic positioning
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m31s
All checks were successful
Build & Push Docker Image / build (push) Successful in 6m31s
This commit is contained in:
@@ -230,6 +230,7 @@
|
||||
"presentationRemoveFailed": "Präsentation konnte nicht entfernt werden",
|
||||
"presentationAllowedTypes": "PDF, PPT, PPTX, ODP, DOC, DOCX · max. 50 MB",
|
||||
"presentationCurrent": "Aktuell:",
|
||||
"shareTitle": "Raum teilen",
|
||||
"shareDescription": "Teilen Sie diesen Raum mit anderen Benutzern, damit diese ihn in ihrem Dashboard sehen und beitreten k\u00f6nnen.",
|
||||
"shareSearchPlaceholder": "Benutzer suchen (Name oder E-Mail)...",
|
||||
"shareAdded": "Benutzer hinzugef\u00fcgt",
|
||||
|
||||
@@ -230,6 +230,7 @@
|
||||
"presentationRemoveFailed": "Could not remove presentation",
|
||||
"presentationAllowedTypes": "PDF, PPT, PPTX, ODP, DOC, DOCX · max. 50 MB",
|
||||
"presentationCurrent": "Current:",
|
||||
"shareTitle": "Share Room",
|
||||
"shareDescription": "Share this room with other users so they can see it in their dashboard and join meetings.",
|
||||
"shareSearchPlaceholder": "Search users (name or email)...",
|
||||
"shareAdded": "User added",
|
||||
|
||||
@@ -27,6 +27,8 @@ export default function Admin() {
|
||||
const [showCreateUser, setShowCreateUser] = useState(false);
|
||||
const [creatingUser, setCreatingUser] = useState(false);
|
||||
const [newUser, setNewUser] = useState({ name: '', display_name: '', email: '', password: '', role: 'user' });
|
||||
const menuBtnRefs = useRef({});
|
||||
const [menuPos, setMenuPos] = useState(null);
|
||||
|
||||
// Invite state
|
||||
const [invites, setInvites] = useState([]);
|
||||
@@ -88,6 +90,7 @@ export default function Admin() {
|
||||
toast.error(err.response?.data?.error || t('admin.roleUpdateFailed'));
|
||||
}
|
||||
setOpenMenu(null);
|
||||
setMenuPos(null);
|
||||
};
|
||||
|
||||
const handleDelete = async (userId, userName) => {
|
||||
@@ -100,6 +103,7 @@ export default function Admin() {
|
||||
toast.error(err.response?.data?.error || t('admin.userDeleteFailed'));
|
||||
}
|
||||
setOpenMenu(null);
|
||||
setMenuPos(null);
|
||||
};
|
||||
|
||||
const handleResetPassword = async (e) => {
|
||||
@@ -580,43 +584,32 @@ export default function Admin() {
|
||||
{new Date(u.created_at).toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US')}
|
||||
</td>
|
||||
<td className="px-5 py-4">
|
||||
<div className="flex items-center justify-end relative">
|
||||
<div className="flex items-center justify-end">
|
||||
<button
|
||||
onClick={() => setOpenMenu(openMenu === u.id ? null : u.id)}
|
||||
ref={el => { menuBtnRefs.current[u.id] = el; }}
|
||||
onClick={() => {
|
||||
if (openMenu === u.id) {
|
||||
setOpenMenu(null);
|
||||
setMenuPos(null);
|
||||
} else {
|
||||
const rect = menuBtnRefs.current[u.id]?.getBoundingClientRect();
|
||||
if (rect) {
|
||||
const menuHeight = 130;
|
||||
const spaceAbove = rect.top;
|
||||
if (spaceAbove >= menuHeight) {
|
||||
setMenuPos({ top: rect.top - menuHeight - 4, left: rect.right - 192 });
|
||||
} else {
|
||||
setMenuPos({ top: rect.bottom + 4, left: rect.right - 192 });
|
||||
}
|
||||
}
|
||||
setOpenMenu(u.id);
|
||||
}
|
||||
}}
|
||||
className="p-1.5 rounded-lg hover:bg-th-hover text-th-text-s transition-colors"
|
||||
disabled={u.id === user.id}
|
||||
>
|
||||
<MoreVertical size={16} />
|
||||
</button>
|
||||
|
||||
{openMenu === u.id && u.id !== user.id && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-10" onClick={() => setOpenMenu(null)} />
|
||||
<div className="absolute right-0 bottom-full mb-1 z-20 w-48 bg-th-card rounded-xl border border-th-border shadow-th-lg overflow-hidden">
|
||||
<button
|
||||
onClick={() => handleRoleChange(u.id, u.role === 'admin' ? 'user' : 'admin')}
|
||||
className="w-full flex items-center gap-2 px-4 py-2.5 text-sm text-th-text hover:bg-th-hover transition-colors"
|
||||
>
|
||||
{u.role === 'admin' ? <UserX size={14} /> : <UserCheck size={14} />}
|
||||
{u.role === 'admin' ? t('admin.makeUser') : t('admin.makeAdmin')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setResetPwModal(u.id); setOpenMenu(null); }}
|
||||
className="w-full flex items-center gap-2 px-4 py-2.5 text-sm text-th-text hover:bg-th-hover transition-colors"
|
||||
>
|
||||
<Key size={14} />
|
||||
{t('admin.resetPassword')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(u.id, u.name)}
|
||||
className="w-full flex items-center gap-2 px-4 py-2.5 text-sm text-th-error hover:bg-th-hover transition-colors"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
{t('admin.deleteUser')}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -633,6 +626,43 @@ export default function Admin() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Context menu portal */}
|
||||
{openMenu && menuPos && openMenu !== user.id && (() => {
|
||||
const u = users.find(u => u.id === openMenu);
|
||||
if (!u) return null;
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40" onClick={() => { setOpenMenu(null); setMenuPos(null); }} />
|
||||
<div
|
||||
className="fixed z-50 w-48 bg-th-card rounded-xl border border-th-border shadow-th-lg overflow-hidden"
|
||||
style={{ top: menuPos.top, left: menuPos.left }}
|
||||
>
|
||||
<button
|
||||
onClick={() => handleRoleChange(u.id, u.role === 'admin' ? 'user' : 'admin')}
|
||||
className="w-full flex items-center gap-2 px-4 py-2.5 text-sm text-th-text hover:bg-th-hover transition-colors"
|
||||
>
|
||||
{u.role === 'admin' ? <UserX size={14} /> : <UserCheck size={14} />}
|
||||
{u.role === 'admin' ? t('admin.makeUser') : t('admin.makeAdmin')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setResetPwModal(u.id); setOpenMenu(null); setMenuPos(null); }}
|
||||
className="w-full flex items-center gap-2 px-4 py-2.5 text-sm text-th-text hover:bg-th-hover transition-colors"
|
||||
>
|
||||
<Key size={14} />
|
||||
{t('admin.resetPassword')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { handleDelete(u.id, u.name); setMenuPos(null); }}
|
||||
className="w-full flex items-center gap-2 px-4 py-2.5 text-sm text-th-error hover:bg-th-hover transition-colors"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
{t('admin.deleteUser')}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Reset password modal */}
|
||||
{resetPwModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
|
||||
Reference in New Issue
Block a user