feat(16-02): display name with fallback in user-detail projections (D-04, UI-SPEC Surface Contract 6)
- ModuleGrantsService.getUserAccess() now selects internalName on the
membership query's group projection (already present via `include:
{ group: true }` on the grant query)
- Both display points (viaGroups names, membership chips' name field)
use internalName ?? name; groups[] sorting now runs over the
displayed name as a result, distinct from GroupsService.listForTenant()
which still sorts by the raw name column
- 3 new test cases: fallback set/unset, sort-by-displayed-name
- No apps/web/ changes (verified via git diff --name-only)
This commit is contained in:
@@ -42,8 +42,8 @@ function makeFakePrisma() {
|
||||
}
|
||||
|
||||
return {
|
||||
__seedGroup(group: { id: string; tenantId: string; name: string }) {
|
||||
groups.set(group.id, group);
|
||||
__seedGroup(group: { id: string; tenantId: string; name: string; internalName?: string | null }) {
|
||||
groups.set(group.id, { internalName: null, ...group });
|
||||
},
|
||||
__seedUser(user: { id: string; tenantId: string }) {
|
||||
users.set(user.id, user);
|
||||
@@ -160,7 +160,7 @@ function makeFakePrisma() {
|
||||
groupId,
|
||||
userId,
|
||||
source: membershipSources.get(`${groupId}::${userId}`) ?? 'MANUAL',
|
||||
group: { id: group.id, name: group.name },
|
||||
group: { id: group.id, name: group.name, internalName: group.internalName ?? null },
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
@@ -505,6 +505,59 @@ describe('ModuleGrantsService.getUserAccess', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('ModuleGrantsService.getUserAccess — Anzeigename mit Fallback (D-04)', () => {
|
||||
it('gesetzter internalName: beide Projektionen (groups[].name, modules[].viaGroups) liefern den internen Namen', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
prisma.__seedGroup({ id: 'g1', tenantId: 't1', name: 'AD-Rohname', internalName: 'Vertrieb' });
|
||||
prisma.__seedUser({ id: 'u1', tenantId: 't1' });
|
||||
prisma.__seedActivation({
|
||||
tenantId: 't1',
|
||||
moduleId: 'mod-1',
|
||||
isActive: true,
|
||||
module: { id: 'mod-1', category: 'ops', name: 'Modul Eins' },
|
||||
});
|
||||
prisma.__seedMembership('g1', 'u1');
|
||||
const service = new ModuleGrantsService(prisma as any);
|
||||
await service.grant('t1', { moduleId: 'mod-1', groupId: 'g1' });
|
||||
|
||||
const result = await service.getUserAccess('t1', 'u1');
|
||||
|
||||
expect(result.groups).toEqual([{ id: 'g1', name: 'Vertrieb', source: 'MANUAL' }]);
|
||||
expect(result.modules[0].viaGroups).toEqual(['Vertrieb']);
|
||||
});
|
||||
|
||||
it('kein internalName (null): beide Projektionen fallen auf name zurueck', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
seedBase(prisma);
|
||||
prisma.__seedMembership('g1', 'u1');
|
||||
const service = new ModuleGrantsService(prisma as any);
|
||||
await service.grant('t1', { moduleId: 'mod-1', groupId: 'g1' });
|
||||
|
||||
const result = await service.getUserAccess('t1', 'u1');
|
||||
|
||||
expect(result.groups).toEqual([{ id: 'g1', name: 'Gruppe A', source: 'MANUAL' }]);
|
||||
expect(result.modules[0].viaGroups).toEqual(['Gruppe A']);
|
||||
});
|
||||
|
||||
it('Sortierung laeuft ueber den angezeigten Namen, nicht ueber die Datenbankspalte name', async () => {
|
||||
const prisma = makeFakePrisma();
|
||||
prisma.__seedUser({ id: 'u1', tenantId: 't1' });
|
||||
// g1 traegt in der DB den Namen "Zebra-AD", zeigt aber "Alpha-Anzeige" an;
|
||||
// g2 traegt "Beta" ohne internalName. Alphabetisch nach ANGEZEIGTEM Namen
|
||||
// muesste g1 (Alpha-Anzeige) vor g2 (Beta) stehen, waehrend eine Sortierung
|
||||
// ueber die rohe name-Spalte g2 (Beta) vor g1 (Zebra-AD) haette gestellt.
|
||||
prisma.__seedGroup({ id: 'g1', tenantId: 't1', name: 'Zebra-AD', internalName: 'Alpha-Anzeige' });
|
||||
prisma.__seedGroup({ id: 'g2', tenantId: 't1', name: 'Beta' });
|
||||
prisma.__seedMembership('g1', 'u1');
|
||||
prisma.__seedMembership('g2', 'u1');
|
||||
const service = new ModuleGrantsService(prisma as any);
|
||||
|
||||
const result = await service.getUserAccess('t1', 'u1');
|
||||
|
||||
expect(result.groups.map((g: any) => g.name)).toEqual(['Alpha-Anzeige', 'Beta']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ModuleGrantsService — Logging (D-23)', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
|
||||
@@ -212,6 +212,16 @@ export class ModuleGrantsService {
|
||||
* Frage (welche Gruppe gewährt dieses Modul, und besteht zusätzlich ein
|
||||
* Direkt-Grant) und behält dafür je Eintrag exakt die Form
|
||||
* { module, viaGroups, direct }.
|
||||
*
|
||||
* Anzeigename mit Fallback (D-04, UI-SPEC Surface Contract 6): beide
|
||||
* Projektionsstellen (viaGroups-Namen, groups[].name) liefern
|
||||
* `group.internalName ?? group.name` — Nullish, nicht Oder, damit ein
|
||||
* bereits auf null normalisierter Leerstring (Task 2) nicht versehentlich
|
||||
* über eine Truthiness-Prüfung nachträglich anders interpretiert wird.
|
||||
* Die Sortierung von `groups` läuft danach automatisch über den
|
||||
* angezeigten Namen, weil sie auf demselben Feld (`name`) aufsetzt —
|
||||
* anders als GroupsService.listForTenant(), das weiterhin über die
|
||||
* Datenbankspalte `name` sortiert.
|
||||
*/
|
||||
async getUserAccess(tenantId: string, userId: string) {
|
||||
await this.assertTargetBelongsToTenant(tenantId, undefined, userId);
|
||||
@@ -237,7 +247,7 @@ export class ModuleGrantsService {
|
||||
// läuft der Mandantenfilter über die Relation `group: { tenantId }`.
|
||||
this.prisma.groupMembership.findMany({
|
||||
where: { userId, group: { tenantId } },
|
||||
include: { group: { select: { id: true, name: true } } },
|
||||
include: { group: { select: { id: true, name: true, internalName: true } } },
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -246,7 +256,7 @@ export class ModuleGrantsService {
|
||||
for (const g of groupGrants as any[]) {
|
||||
if (!g.group) continue;
|
||||
const names = groupNamesByModule.get(g.moduleId) ?? [];
|
||||
names.push(g.group.name);
|
||||
names.push(g.group.internalName ?? g.group.name);
|
||||
groupNamesByModule.set(g.moduleId, names);
|
||||
}
|
||||
|
||||
@@ -261,7 +271,7 @@ export class ModuleGrantsService {
|
||||
.filter((m) => m.group)
|
||||
.map((m) => ({
|
||||
id: m.group.id as string,
|
||||
name: m.group.name as string,
|
||||
name: (m.group.internalName ?? m.group.name) as string,
|
||||
source: m.source as string,
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
Reference in New Issue
Block a user