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",
|
"presentationRemoveFailed": "Präsentation konnte nicht entfernt werden",
|
||||||
"presentationAllowedTypes": "PDF, PPT, PPTX, ODP, DOC, DOCX · max. 50 MB",
|
"presentationAllowedTypes": "PDF, PPT, PPTX, ODP, DOC, DOCX · max. 50 MB",
|
||||||
"presentationCurrent": "Aktuell:",
|
"presentationCurrent": "Aktuell:",
|
||||||
|
"shareTitle": "Raum teilen",
|
||||||
"shareDescription": "Teilen Sie diesen Raum mit anderen Benutzern, damit diese ihn in ihrem Dashboard sehen und beitreten k\u00f6nnen.",
|
"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)...",
|
"shareSearchPlaceholder": "Benutzer suchen (Name oder E-Mail)...",
|
||||||
"shareAdded": "Benutzer hinzugef\u00fcgt",
|
"shareAdded": "Benutzer hinzugef\u00fcgt",
|
||||||
|
|||||||
@@ -230,6 +230,7 @@
|
|||||||
"presentationRemoveFailed": "Could not remove presentation",
|
"presentationRemoveFailed": "Could not remove presentation",
|
||||||
"presentationAllowedTypes": "PDF, PPT, PPTX, ODP, DOC, DOCX · max. 50 MB",
|
"presentationAllowedTypes": "PDF, PPT, PPTX, ODP, DOC, DOCX · max. 50 MB",
|
||||||
"presentationCurrent": "Current:",
|
"presentationCurrent": "Current:",
|
||||||
|
"shareTitle": "Share Room",
|
||||||
"shareDescription": "Share this room with other users so they can see it in their dashboard and join meetings.",
|
"shareDescription": "Share this room with other users so they can see it in their dashboard and join meetings.",
|
||||||
"shareSearchPlaceholder": "Search users (name or email)...",
|
"shareSearchPlaceholder": "Search users (name or email)...",
|
||||||
"shareAdded": "User added",
|
"shareAdded": "User added",
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export default function Admin() {
|
|||||||
const [showCreateUser, setShowCreateUser] = useState(false);
|
const [showCreateUser, setShowCreateUser] = useState(false);
|
||||||
const [creatingUser, setCreatingUser] = useState(false);
|
const [creatingUser, setCreatingUser] = useState(false);
|
||||||
const [newUser, setNewUser] = useState({ name: '', display_name: '', email: '', password: '', role: 'user' });
|
const [newUser, setNewUser] = useState({ name: '', display_name: '', email: '', password: '', role: 'user' });
|
||||||
|
const menuBtnRefs = useRef({});
|
||||||
|
const [menuPos, setMenuPos] = useState(null);
|
||||||
|
|
||||||
// Invite state
|
// Invite state
|
||||||
const [invites, setInvites] = useState([]);
|
const [invites, setInvites] = useState([]);
|
||||||
@@ -88,6 +90,7 @@ export default function Admin() {
|
|||||||
toast.error(err.response?.data?.error || t('admin.roleUpdateFailed'));
|
toast.error(err.response?.data?.error || t('admin.roleUpdateFailed'));
|
||||||
}
|
}
|
||||||
setOpenMenu(null);
|
setOpenMenu(null);
|
||||||
|
setMenuPos(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (userId, userName) => {
|
const handleDelete = async (userId, userName) => {
|
||||||
@@ -100,6 +103,7 @@ export default function Admin() {
|
|||||||
toast.error(err.response?.data?.error || t('admin.userDeleteFailed'));
|
toast.error(err.response?.data?.error || t('admin.userDeleteFailed'));
|
||||||
}
|
}
|
||||||
setOpenMenu(null);
|
setOpenMenu(null);
|
||||||
|
setMenuPos(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResetPassword = async (e) => {
|
const handleResetPassword = async (e) => {
|
||||||
@@ -580,43 +584,32 @@ export default function Admin() {
|
|||||||
{new Date(u.created_at).toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US')}
|
{new Date(u.created_at).toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US')}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-5 py-4">
|
<td className="px-5 py-4">
|
||||||
<div className="flex items-center justify-end relative">
|
<div className="flex items-center justify-end">
|
||||||
<button
|
<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"
|
className="p-1.5 rounded-lg hover:bg-th-hover text-th-text-s transition-colors"
|
||||||
disabled={u.id === user.id}
|
disabled={u.id === user.id}
|
||||||
>
|
>
|
||||||
<MoreVertical size={16} />
|
<MoreVertical size={16} />
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -633,6 +626,43 @@ export default function Admin() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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 */}
|
{/* Reset password modal */}
|
||||||
{resetPwModal && (
|
{resetPwModal && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user