feat(16-04): show internalName with AD-name fallback in groups list

Name column renders group.internalName ?? group.name (nullish, not
truthiness, since the backend already normalizes blank values to null)
inside a span carrying title={group.name} so the AD name stays
discoverable on hover once an internal name is set. Badge column is
untouched — it remains the sole imported-vs-local marker per D-07.

Adds two component-test cases covering the fallback and its title
attribute (D-04).
This commit is contained in:
2026-08-06 16:29:15 +02:00
parent 30affbbc7e
commit 6802b48cd8
2 changed files with 60 additions and 1 deletions
@@ -96,6 +96,7 @@ interface Group {
tenantId: string; tenantId: string;
name: string; name: string;
ldapDn: string | null; ldapDn: string | null;
internalName: string | null;
isDefault: boolean; isDefault: boolean;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
@@ -108,6 +109,7 @@ const mockGroups: Group[] = [
tenantId: 't1', tenantId: 't1',
name: 'Alle Benutzer', name: 'Alle Benutzer',
ldapDn: null, ldapDn: null,
internalName: null,
isDefault: true, isDefault: true,
createdAt: '2026-01-01T00:00:00.000Z', createdAt: '2026-01-01T00:00:00.000Z',
updatedAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z',
@@ -118,6 +120,32 @@ const mockGroups: Group[] = [
tenantId: 't1', tenantId: 't1',
name: 'Buchhaltung', name: 'Buchhaltung',
ldapDn: 'CN=Buchhaltung,OU=Gruppen,DC=ctl,DC=local', ldapDn: 'CN=Buchhaltung,OU=Gruppen,DC=ctl,DC=local',
internalName: null,
isDefault: false,
createdAt: '2026-01-01T00:00:00.000Z',
updatedAt: '2026-01-01T00:00:00.000Z',
memberCount: 1,
},
];
const mockGroupsWithInternalName: Group[] = [
{
id: 'g3',
tenantId: 't1',
name: 'AD-Vertrieb',
ldapDn: 'CN=AD-Vertrieb,OU=Gruppen,DC=ctl,DC=local',
internalName: 'Vertrieb',
isDefault: false,
createdAt: '2026-01-01T00:00:00.000Z',
updatedAt: '2026-01-01T00:00:00.000Z',
memberCount: 2,
},
{
id: 'g4',
tenantId: 't1',
name: 'AD-Support',
ldapDn: 'CN=AD-Support,OU=Gruppen,DC=ctl,DC=local',
internalName: null,
isDefault: false, isDefault: false,
createdAt: '2026-01-01T00:00:00.000Z', createdAt: '2026-01-01T00:00:00.000Z',
updatedAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z',
@@ -178,6 +206,35 @@ describe('AdminGroupsPage', () => {
expect(screen.getByText('1')).toBeInTheDocument(); expect(screen.getByText('1')).toBeInTheDocument();
}); });
it('shows the internal name in the name column and the AD name as title when internalName is set (D-04)', async () => {
vi.stubGlobal(
'fetch',
vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve(mockGroupsWithInternalName) })),
);
render(<AdminGroupsPage />);
await waitFor(() => {
expect(screen.getByText('Vertrieb')).toBeInTheDocument();
});
expect(screen.queryByText('AD-Vertrieb')).not.toBeInTheDocument();
expect(screen.getByText('Vertrieb').closest('span')).toHaveAttribute('title', 'AD-Vertrieb');
});
it('falls back to the stored name in the name column when internalName is not set (D-04)', async () => {
vi.stubGlobal(
'fetch',
vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve(mockGroupsWithInternalName) })),
);
render(<AdminGroupsPage />);
await waitFor(() => {
expect(screen.getByText('AD-Support')).toBeInTheDocument();
});
expect(screen.getByText('AD-Support').closest('span')).toHaveAttribute('title', 'AD-Support');
});
it('toggles the default-group star optimistically and persists via PATCH', async () => { it('toggles the default-group star optimistically and persists via PATCH', async () => {
const fetchMock = vi.fn((url: string, init?: RequestInit) => { const fetchMock = vi.fn((url: string, init?: RequestInit) => {
if (typeof url === 'string' && url.endsWith('/groups') && (!init || init.method === undefined)) { if (typeof url === 'string' && url.endsWith('/groups') && (!init || init.method === undefined)) {
@@ -177,7 +177,9 @@ export default function AdminGroupsPage() {
<tbody className="divide-y divide-border"> <tbody className="divide-y divide-border">
{groups.map((group) => ( {groups.map((group) => (
<tr key={group.id} className="hover:bg-muted/30 transition-colors"> <tr key={group.id} className="hover:bg-muted/30 transition-colors">
<td className="px-4 py-3 font-medium text-foreground">{group.name}</td> <td className="px-4 py-3 font-medium text-foreground">
<span title={group.name}>{group.internalName ?? group.name}</span>
</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span <span
className={`inline-block rounded-full px-2 py-0.5 text-xs font-medium ${ className={`inline-block rounded-full px-2 py-0.5 text-xs font-medium ${