feat: SHA-based avatar colors, user create/edit, editable names, +Add visibility
- Add hashColor utility for unique avatar colors derived from entity names - Add user creation form with username/displayName/email fields - Add useCreateUser and useUpdateUser mutation hooks - Make display names editable on all detail panes (click to edit) - Protect built-in entities: Admins group and system roles not editable - Make +Add chip more visible with amber border and background - Send empty string instead of null for role description on create - Add .editNameInput CSS for inline name editing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
import type { GroupDetail } from '../../../api/queries/admin/rbac';
|
||||
import { ConfirmDeleteDialog } from '../../../components/admin/ConfirmDeleteDialog';
|
||||
import { MultiSelectDropdown } from './components/MultiSelectDropdown';
|
||||
import { hashColor } from './avatar-colors';
|
||||
import styles from './RbacPage.module.css';
|
||||
|
||||
function getInitials(name: string): string {
|
||||
@@ -140,13 +141,14 @@ export function GroupsTab() {
|
||||
<div className={styles.entityList}>
|
||||
{filtered.map((group) => {
|
||||
const isSelected = group.id === selectedId;
|
||||
const color = hashColor(group.name);
|
||||
return (
|
||||
<div
|
||||
key={group.id}
|
||||
className={`${styles.entityItem} ${isSelected ? styles.entityItemSelected : ''}`}
|
||||
onClick={() => setSelectedId(group.id)}
|
||||
>
|
||||
<div className={`${styles.avatar} ${styles.avatarGroup}`}>
|
||||
<div className={styles.avatar} style={{ background: color.bg, color: color.fg, borderRadius: 8 }}>
|
||||
{getInitials(group.name)}
|
||||
</div>
|
||||
<div className={styles.entityInfo}>
|
||||
@@ -211,6 +213,8 @@ function GroupDetailView({
|
||||
onDeselect: () => void;
|
||||
}) {
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [editingName, setEditingName] = useState(false);
|
||||
const [nameValue, setNameValue] = useState(group.name);
|
||||
const deleteGroup = useDeleteGroup();
|
||||
const updateGroup = useUpdateGroup();
|
||||
const assignRole = useAssignRoleToGroup();
|
||||
@@ -218,6 +222,14 @@ function GroupDetailView({
|
||||
|
||||
const isBuiltIn = group.id === ADMINS_GROUP_ID;
|
||||
|
||||
// Reset editing state when group changes
|
||||
const [prevGroupId, setPrevGroupId] = useState(group.id);
|
||||
if (prevGroupId !== group.id) {
|
||||
setPrevGroupId(group.id);
|
||||
setEditingName(false);
|
||||
setNameValue(group.name);
|
||||
}
|
||||
|
||||
const hierarchyLabel = group.parentGroupId
|
||||
? `Child of ${groupMap.get(group.parentGroupId)?.name ?? 'unknown'}`
|
||||
: 'Top-level group';
|
||||
@@ -254,14 +266,37 @@ function GroupDetailView({
|
||||
return rows;
|
||||
}, [group, groupMap]);
|
||||
|
||||
const color = hashColor(group.name);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.detailHeader}>
|
||||
<div className={styles.detailHeaderInfo}>
|
||||
<div className={`${styles.detailAvatar} ${styles.avatarGroup}`} style={{ borderRadius: 10 }}>
|
||||
<div className={styles.detailAvatar} style={{ background: color.bg, color: color.fg, borderRadius: 10 }}>
|
||||
{getInitials(group.name)}
|
||||
</div>
|
||||
<div className={styles.detailName}>{group.name}</div>
|
||||
{editingName ? (
|
||||
<input
|
||||
className={styles.editNameInput}
|
||||
value={nameValue}
|
||||
onChange={e => setNameValue(e.target.value)}
|
||||
onBlur={() => {
|
||||
if (nameValue.trim() && nameValue !== group.name) {
|
||||
updateGroup.mutate({ id: group.id, name: nameValue.trim() });
|
||||
}
|
||||
setEditingName(false);
|
||||
}}
|
||||
onKeyDown={e => { if (e.key === 'Enter') e.currentTarget.blur(); if (e.key === 'Escape') { setNameValue(group.name); setEditingName(false); } }}
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.detailName}
|
||||
onClick={() => !isBuiltIn && setEditingName(true)}
|
||||
style={{ cursor: isBuiltIn ? 'default' : 'pointer' }}
|
||||
title={isBuiltIn ? undefined : 'Click to edit'}>
|
||||
{group.name}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.detailEmail}>{hierarchyLabel}</div>
|
||||
</div>
|
||||
<button type="button" className={styles.btnDelete}
|
||||
|
||||
Reference in New Issue
Block a user