From a173c5b6ce60f2c1c03ac887813b597989fe0520 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:12:23 +0100 Subject: [PATCH] feat: add GroupsTab to User Management --- src/pages/Admin/UserManagement/GroupsTab.tsx | 230 +++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 src/pages/Admin/UserManagement/GroupsTab.tsx diff --git a/src/pages/Admin/UserManagement/GroupsTab.tsx b/src/pages/Admin/UserManagement/GroupsTab.tsx new file mode 100644 index 0000000..1143414 --- /dev/null +++ b/src/pages/Admin/UserManagement/GroupsTab.tsx @@ -0,0 +1,230 @@ +import { useState, useMemo } from 'react' +import { Avatar } from '../../../design-system/primitives/Avatar/Avatar' +import { Badge } from '../../../design-system/primitives/Badge/Badge' +import { Button } from '../../../design-system/primitives/Button/Button' +import { Input } from '../../../design-system/primitives/Input/Input' +import { Select } from '../../../design-system/primitives/Select/Select' +import { MonoText } from '../../../design-system/primitives/MonoText/MonoText' +import { SectionHeader } from '../../../design-system/primitives/SectionHeader/SectionHeader' +import { Tag } from '../../../design-system/primitives/Tag/Tag' +import { InlineEdit } from '../../../design-system/primitives/InlineEdit/InlineEdit' +import { MultiSelect } from '../../../design-system/composites/MultiSelect/MultiSelect' +import { ConfirmDialog } from '../../../design-system/composites/ConfirmDialog/ConfirmDialog' +import { MOCK_GROUPS, MOCK_USERS, MOCK_ROLES, getChildGroups, type MockGroup } from './rbacMocks' +import styles from './UserManagement.module.css' + +export function GroupsTab() { + const [groups, setGroups] = useState(MOCK_GROUPS) + const [search, setSearch] = useState('') + const [selectedId, setSelectedId] = useState(null) + const [creating, setCreating] = useState(false) + const [deleteTarget, setDeleteTarget] = useState(null) + + const [newName, setNewName] = useState('') + const [newParent, setNewParent] = useState('') + + const filtered = useMemo(() => { + if (!search) return groups + const q = search.toLowerCase() + return groups.filter((g) => g.name.toLowerCase().includes(q)) + }, [groups, search]) + + const selected = groups.find((g) => g.id === selectedId) ?? null + + function handleCreate() { + if (!newName.trim()) return + const newGroup: MockGroup = { + id: `grp-${Date.now()}`, + name: newName.trim(), + parentId: newParent || null, + builtIn: false, + directRoles: [], + memberUserIds: [], + } + setGroups((prev) => [...prev, newGroup]) + setCreating(false) + setNewName(''); setNewParent('') + setSelectedId(newGroup.id) + } + + function handleDelete() { + if (!deleteTarget) return + setGroups((prev) => prev.filter((g) => g.id !== deleteTarget.id)) + if (selectedId === deleteTarget.id) setSelectedId(null) + setDeleteTarget(null) + } + + function updateGroup(id: string, patch: Partial) { + setGroups((prev) => prev.map((g) => g.id === id ? { ...g, ...patch } : g)) + } + + const children = selected ? getChildGroups(selected.id) : [] + const members = selected ? MOCK_USERS.filter((u) => u.directGroups.includes(selected.id)) : [] + const parent = selected?.parentId ? groups.find((g) => g.id === selected.parentId) : null + const availableRoles = MOCK_ROLES.filter((r) => !selected?.directRoles.includes(r.name)) + .map((r) => ({ value: r.name, label: r.name })) + + const parentOptions = [ + { value: '', label: 'Top-level' }, + ...groups.filter((g) => g.id !== selectedId).map((g) => ({ value: g.id, label: g.name })), + ] + + return ( + <> +
+
+
+ setSearch(e.target.value)} + onClear={() => setSearch('')} + className={styles.listHeaderSearch} + /> + +
+ + {creating && ( +
+ setNewName(e.target.value)} /> +