feat(module-grants): Stufe Verwalten in Freigaben-Matrix und Benutzerdetails, Doku und Changelog
- Matrix und Benutzerdetails liefern und zeigen die Stufe, Auswahlfeld Benutzen/Verwalten - Gruppen mit Verwalten sind in den Benutzerdetails markiert - Administrationshandbuch, Anwenderanleitung und Changelog beschreiben die Stufen Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,11 +17,15 @@ const messages: Record<string, Record<string, unknown>> = {
|
||||
emptyModulesLink: 'Zu Module',
|
||||
adminNote:
|
||||
'ADMIN und SUPER_ADMIN haben immer Zugriff auf alle aktiven Module — diese Matrix betrifft nur die Rolle USER.',
|
||||
levelExplanation: '„Benutzen“: Das Modul öffnen. „Verwalten“: zusätzlich die Einstellungen ändern.',
|
||||
saveError: 'Freigabe konnte nicht gespeichert werden. Bitte erneut versuchen.',
|
||||
noSearchResults: 'Kein Treffer für "{search}" — weder bei den Modulen noch bei den Gruppen.',
|
||||
},
|
||||
'admin.groups.grants': {
|
||||
matrixCheckboxLabel: '{module} für Gruppe {group} {granted, select, true {freigeben} other {entziehen}}',
|
||||
levelUse: 'Benutzen',
|
||||
levelManage: 'Verwalten',
|
||||
levelSelectLabel: 'Stufe für {module} in Gruppe {group}',
|
||||
},
|
||||
'adminModules.activationDialog': {
|
||||
title: 'Modul aktivieren: {module}',
|
||||
@@ -321,6 +325,121 @@ describe('AdminModuleGrantsPage (Permission-Matrix)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('AdminModuleGrantsPage — Freigabestufe (261002-icv)', () => {
|
||||
beforeEach(() => {
|
||||
stubAdmin();
|
||||
});
|
||||
|
||||
const levelMatrix = {
|
||||
...mockMatrix,
|
||||
grants: [
|
||||
{ moduleId: 'm1', groupId: 'g1', level: 'MANAGE' },
|
||||
{ moduleId: 'm2', groupId: 'g1', level: 'USE' },
|
||||
],
|
||||
};
|
||||
|
||||
it('zeigt bei freigegebenen Zellen ein Stufenfeld mit der gelieferten Stufe, bei leeren Zellen keins', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve(levelMatrix) })),
|
||||
);
|
||||
|
||||
render(<AdminModuleGrantsPage />);
|
||||
|
||||
const manage = (await screen.findByLabelText(
|
||||
'Stufe für Ausschreibungs-Radar in Gruppe Alle Benutzer',
|
||||
)) as HTMLSelectElement;
|
||||
expect(manage.value).toBe('MANAGE');
|
||||
const use = screen.getByLabelText('Stufe für DKV Flotte in Gruppe Alle Benutzer') as HTMLSelectElement;
|
||||
expect(use.value).toBe('USE');
|
||||
expect(screen.queryByLabelText('Stufe für Ausschreibungs-Radar in Gruppe Buchhaltung')).toBeNull();
|
||||
});
|
||||
|
||||
it('Wechsel auf Verwalten sendet POST mit level MANAGE', async () => {
|
||||
const fetchMock = vi.fn((_url: string, init?: RequestInit) => {
|
||||
if (init?.method === 'POST') {
|
||||
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
||||
}
|
||||
return Promise.resolve({ ok: true, json: () => Promise.resolve(levelMatrix) });
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
render(<AdminModuleGrantsPage />);
|
||||
|
||||
const select = (await screen.findByLabelText(
|
||||
'Stufe für DKV Flotte in Gruppe Alle Benutzer',
|
||||
)) as HTMLSelectElement;
|
||||
await userEvent.selectOptions(select, 'MANAGE');
|
||||
|
||||
await waitFor(() => {
|
||||
const post = fetchMock.mock.calls.find(([, init]) => init?.method === 'POST');
|
||||
expect(post).toBeTruthy();
|
||||
expect(JSON.parse(String(post?.[1]?.body))).toEqual({
|
||||
moduleId: 'm2',
|
||||
groupId: 'g1',
|
||||
level: 'MANAGE',
|
||||
});
|
||||
});
|
||||
expect(select.value).toBe('MANAGE');
|
||||
});
|
||||
|
||||
it('ein fehlgeschlagener Stufenwechsel setzt das Auswahlfeld zurück', async () => {
|
||||
const fetchMock = vi.fn((_url: string, init?: RequestInit) => {
|
||||
if (init?.method === 'POST') {
|
||||
return Promise.resolve({ ok: false, status: 500, text: () => Promise.resolve('boom') });
|
||||
}
|
||||
return Promise.resolve({ ok: true, json: () => Promise.resolve(levelMatrix) });
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
render(<AdminModuleGrantsPage />);
|
||||
|
||||
const select = (await screen.findByLabelText(
|
||||
'Stufe für DKV Flotte in Gruppe Alle Benutzer',
|
||||
)) as HTMLSelectElement;
|
||||
await userEvent.selectOptions(select, 'MANAGE');
|
||||
|
||||
await waitFor(() => expect(screen.getByText(/500: boom/)).toBeInTheDocument());
|
||||
expect(select.value).toBe('USE');
|
||||
});
|
||||
|
||||
it('Ankreuzen einer leeren Zelle sendet POST ohne level und zeigt Benutzen', async () => {
|
||||
const fetchMock = vi.fn((_url: string, init?: RequestInit) => {
|
||||
if (init?.method === 'POST') {
|
||||
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
||||
}
|
||||
return Promise.resolve({ ok: true, json: () => Promise.resolve(levelMatrix) });
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
render(<AdminModuleGrantsPage />);
|
||||
|
||||
const box = await screen.findByLabelText('Ausschreibungs-Radar für Gruppe Buchhaltung freigeben');
|
||||
await userEvent.click(box);
|
||||
|
||||
await waitFor(() => {
|
||||
const post = fetchMock.mock.calls.find(([, init]) => init?.method === 'POST');
|
||||
expect(post).toBeTruthy();
|
||||
expect(JSON.parse(String(post?.[1]?.body))).toEqual({ moduleId: 'm1', groupId: 'g2' });
|
||||
});
|
||||
const select = (await screen.findByLabelText(
|
||||
'Stufe für Ausschreibungs-Radar in Gruppe Buchhaltung',
|
||||
)) as HTMLSelectElement;
|
||||
expect(select.value).toBe('USE');
|
||||
});
|
||||
|
||||
it('zeigt die Erklärung der Stufen unter der Matrix', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve(levelMatrix) })),
|
||||
);
|
||||
|
||||
render(<AdminModuleGrantsPage />);
|
||||
|
||||
expect(await screen.findByText(/„Verwalten“: zusätzlich die Einstellungen ändern/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ActivateModuleDialog', () => {
|
||||
it('renders all three actions (cancel, configure later, grant now)', async () => {
|
||||
vi.stubGlobal(
|
||||
|
||||
@@ -19,9 +19,13 @@ interface Group {
|
||||
internalName?: string | null;
|
||||
}
|
||||
|
||||
type GrantLevel = 'USE' | 'MANAGE';
|
||||
|
||||
interface GrantPair {
|
||||
moduleId: string;
|
||||
groupId: string;
|
||||
/** Freigabestufe (261002-icv); fehlt sie, gilt Benutzen. */
|
||||
level?: GrantLevel;
|
||||
}
|
||||
|
||||
function cellKey(moduleId: string, groupId: string): string {
|
||||
@@ -35,7 +39,9 @@ function cellKey(moduleId: string, groupId: string): string {
|
||||
* einmal GET /module-grants/matrix (Module, Gruppen, bestehende
|
||||
* Gruppen-Grants in einer Antwort) und schaltet jede Zelle sofort um
|
||||
* (optimistisches UI, Rücksprung bei Fehler -- identisches Verhalten zu
|
||||
* AdminModulesPage.toggleModule).
|
||||
* AdminModulesPage.toggleModule). Seit 261002-icv trägt jede freigegebene
|
||||
* Zelle eine Stufe (Benutzen / Verwalten), die per Auswahlfeld gewechselt
|
||||
* wird.
|
||||
*/
|
||||
export default function AdminModuleGrantsPage() {
|
||||
const t = useTranslations('adminModules.grants');
|
||||
@@ -47,7 +53,7 @@ export default function AdminModuleGrantsPage() {
|
||||
|
||||
const [modules, setModules] = useState<Module[]>([]);
|
||||
const [groups, setGroups] = useState<Group[]>([]);
|
||||
const [grants, setGrants] = useState<Set<string>>(new Set());
|
||||
const [grants, setGrants] = useState<Map<string, GrantLevel>>(new Map());
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [search, setSearch] = useState('');
|
||||
@@ -63,7 +69,11 @@ export default function AdminModuleGrantsPage() {
|
||||
await res.json();
|
||||
setModules(data.modules);
|
||||
setGroups(data.groups);
|
||||
setGrants(new Set(data.grants.map((g) => cellKey(g.moduleId, g.groupId))));
|
||||
setGrants(
|
||||
new Map(
|
||||
data.grants.map((g) => [cellKey(g.moduleId, g.groupId), g.level ?? 'USE'] as const),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// silently fail -- same precedent as the other admin list pages
|
||||
@@ -87,21 +97,22 @@ export default function AdminModuleGrantsPage() {
|
||||
|
||||
// Optimistic toggle -- rolled back below on any failure so the UI never
|
||||
// shows a grant the database does not have (T-15-25).
|
||||
const previousLevel = grants.get(key);
|
||||
setGrants((prev) => {
|
||||
const next = new Set(prev);
|
||||
const next = new Map(prev);
|
||||
if (currentlyGranted) {
|
||||
next.delete(key);
|
||||
} else {
|
||||
next.add(key);
|
||||
next.set(key, 'USE');
|
||||
}
|
||||
return next;
|
||||
});
|
||||
|
||||
const rollback = () => {
|
||||
setGrants((prev) => {
|
||||
const next = new Set(prev);
|
||||
const next = new Map(prev);
|
||||
if (currentlyGranted) {
|
||||
next.add(key);
|
||||
next.set(key, previousLevel ?? 'USE');
|
||||
} else {
|
||||
next.delete(key);
|
||||
}
|
||||
@@ -130,6 +141,37 @@ export default function AdminModuleGrantsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Stufe einer bereits freigegebenen Zelle wechseln (261002-icv): POST mit
|
||||
// `level` — der Server hebt/senkt die Stufe der bestehenden Freigabe.
|
||||
// Optimistisch, bei Fehler Rücksprung auf die vorherige Stufe.
|
||||
const changeLevel = async (moduleId: string, groupId: string, level: GrantLevel) => {
|
||||
const key = cellKey(moduleId, groupId);
|
||||
const previous = grants.get(key) ?? 'USE';
|
||||
if (previous === level) return;
|
||||
setTogglingKey(key);
|
||||
setError(null);
|
||||
setGrants((prev) => new Map(prev).set(key, level));
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/module-grants`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ moduleId, groupId, level }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
setGrants((prev) => new Map(prev).set(key, previous));
|
||||
const body = await res.text().catch(() => '');
|
||||
setError(`${res.status}: ${body || t('saveError')}`);
|
||||
}
|
||||
} catch (err) {
|
||||
setGrants((prev) => new Map(prev).set(key, previous));
|
||||
setError(String(err));
|
||||
} finally {
|
||||
setTogglingKey(null);
|
||||
}
|
||||
};
|
||||
|
||||
const searchLower = search.trim().toLowerCase();
|
||||
|
||||
// Both axes are searched independently, then combined with the following
|
||||
@@ -263,10 +305,11 @@ export default function AdminModuleGrantsPage() {
|
||||
{filteredGroups.map((g) => {
|
||||
const key = cellKey(mod.id, g.id);
|
||||
const isGranted = grants.has(key);
|
||||
const level = grants.get(key) ?? 'USE';
|
||||
const isToggling = togglingKey === key;
|
||||
return (
|
||||
<td key={g.id} className="px-4 py-3 text-center">
|
||||
<span className="relative inline-flex items-center justify-center">
|
||||
<span className="relative inline-flex items-center justify-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isGranted}
|
||||
@@ -283,6 +326,23 @@ export default function AdminModuleGrantsPage() {
|
||||
})}
|
||||
className="h-4 w-4 rounded border-input"
|
||||
/>
|
||||
{isGranted && (
|
||||
<select
|
||||
value={level}
|
||||
disabled={isToggling}
|
||||
onChange={(e) =>
|
||||
changeLevel(mod.id, g.id, e.target.value as GrantLevel)
|
||||
}
|
||||
aria-label={tGrantLabel('levelSelectLabel', {
|
||||
module: mod.name,
|
||||
group: g.name,
|
||||
})}
|
||||
className="h-7 rounded-md border border-input bg-background px-1 text-xs"
|
||||
>
|
||||
<option value="USE">{tGrantLabel('levelUse')}</option>
|
||||
<option value="MANAGE">{tGrantLabel('levelManage')}</option>
|
||||
</select>
|
||||
)}
|
||||
{isToggling && (
|
||||
<span className="absolute -right-4 inline-block h-3 w-3 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent" />
|
||||
)}
|
||||
@@ -299,6 +359,7 @@ export default function AdminModuleGrantsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-muted-foreground">{t('levelExplanation')}</p>
|
||||
<p className="text-xs text-muted-foreground">{t('adminNote')}</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -11,10 +11,16 @@ interface ModuleSummary {
|
||||
category: string;
|
||||
}
|
||||
|
||||
type GrantLevel = 'USE' | 'MANAGE';
|
||||
|
||||
interface ModuleAccessRow {
|
||||
module: ModuleSummary;
|
||||
viaGroups: string[];
|
||||
direct: boolean;
|
||||
/** Stufe der Direkt-Freigabe (261002-icv); null ohne Direkt-Freigabe. */
|
||||
directLevel?: GrantLevel | null;
|
||||
/** Namen der Gruppen, die Verwalten gewähren (Teilmenge von viaGroups). */
|
||||
manageViaGroups?: string[];
|
||||
}
|
||||
|
||||
interface MembershipChip {
|
||||
@@ -37,7 +43,9 @@ interface UserAccessModalProps {
|
||||
/**
|
||||
* Benutzer-Detail-Zugriff (D-16, Surface Contract 3): Gruppenmitgliedschaften
|
||||
* (read-only -- bearbeitet wird ausschliesslich unter /admin/groups) und
|
||||
* Modul-Zugriff mit geerbten Gruppen plus einem Direkt-Toggle.
|
||||
* Modul-Zugriff mit geerbten Gruppen plus einem Direkt-Toggle. Seit 261002-icv
|
||||
* trägt die Direkt-Freigabe eine Stufe (Benutzen / Verwalten, Auswahlfeld),
|
||||
* und Gruppen, die Verwalten gewähren, sind in der Liste markiert.
|
||||
*
|
||||
* Lädt einmal GET /module-grants/users/:userId -- eine Antwort, zwei
|
||||
* Abschnitte: die Gruppenmitgliedschafts-Chips kommen direkt aus den
|
||||
@@ -49,6 +57,7 @@ export function UserAccessModal({ userId, username, onClose }: UserAccessModalPr
|
||||
const t = useTranslations('admin.users.grants');
|
||||
const tCommon = useTranslations('common');
|
||||
const tMembers = useTranslations('admin.groups.members');
|
||||
const tLevel = useTranslations('admin.groups.grants');
|
||||
|
||||
const [rows, setRows] = useState<ModuleAccessRow[] | null>(null);
|
||||
const [groups, setGroups] = useState<MembershipChip[] | null>(null);
|
||||
@@ -84,19 +93,28 @@ export function UserAccessModal({ userId, username, onClose }: UserAccessModalPr
|
||||
const toggleDirect = async (moduleId: string, currentlyDirect: boolean) => {
|
||||
setTogglingModuleId(moduleId);
|
||||
setSaveError(null);
|
||||
const previousLevel = rows?.find((r) => r.module.id === moduleId)?.directLevel ?? null;
|
||||
|
||||
// Optimistic toggle -- rolled back below on failure (T-15-25): the UI
|
||||
// must never show a grant the database does not have.
|
||||
setRows((prev) =>
|
||||
prev
|
||||
? prev.map((r) => (r.module.id === moduleId ? { ...r, direct: !currentlyDirect } : r))
|
||||
? prev.map((r) =>
|
||||
r.module.id === moduleId
|
||||
? { ...r, direct: !currentlyDirect, directLevel: currentlyDirect ? null : 'USE' }
|
||||
: r,
|
||||
)
|
||||
: prev,
|
||||
);
|
||||
|
||||
const rollback = () => {
|
||||
setRows((prev) =>
|
||||
prev
|
||||
? prev.map((r) => (r.module.id === moduleId ? { ...r, direct: currentlyDirect } : r))
|
||||
? prev.map((r) =>
|
||||
r.module.id === moduleId
|
||||
? { ...r, direct: currentlyDirect, directLevel: previousLevel }
|
||||
: r,
|
||||
)
|
||||
: prev,
|
||||
);
|
||||
};
|
||||
@@ -122,6 +140,41 @@ export function UserAccessModal({ userId, username, onClose }: UserAccessModalPr
|
||||
}
|
||||
};
|
||||
|
||||
// Stufe der Direkt-Freigabe wechseln (261002-icv): POST mit `level`,
|
||||
// optimistisch, bei Fehler Rücksprung auf die vorherige Stufe.
|
||||
const changeDirectLevel = async (moduleId: string, level: GrantLevel) => {
|
||||
const previous = rows?.find((r) => r.module.id === moduleId)?.directLevel ?? 'USE';
|
||||
if (previous === level) return;
|
||||
setTogglingModuleId(moduleId);
|
||||
setSaveError(null);
|
||||
const setLevel = (value: GrantLevel) =>
|
||||
setRows((prev) =>
|
||||
prev
|
||||
? prev.map((r) => (r.module.id === moduleId ? { ...r, directLevel: value } : r))
|
||||
: prev,
|
||||
);
|
||||
setLevel(level);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/module-grants`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ moduleId, userId, level }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
setLevel(previous);
|
||||
const body = await res.text().catch(() => '');
|
||||
setSaveError(`${res.status}: ${body || t('saveError')}`);
|
||||
}
|
||||
} catch (err) {
|
||||
setLevel(previous);
|
||||
setSaveError(String(err));
|
||||
} finally {
|
||||
setTogglingModuleId(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
||||
@@ -217,13 +270,18 @@ export function UserAccessModal({ userId, username, onClose }: UserAccessModalPr
|
||||
className="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground"
|
||||
>
|
||||
{name}
|
||||
{row.manageViaGroups?.includes(name) && (
|
||||
<span className="ml-1 font-medium text-foreground">
|
||||
· {t('manageMarker')}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<span className="relative inline-flex items-center justify-center">
|
||||
<span className="relative inline-flex items-center justify-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={row.direct}
|
||||
@@ -239,6 +297,23 @@ export function UserAccessModal({ userId, username, onClose }: UserAccessModalPr
|
||||
})}
|
||||
className="h-4 w-4 rounded border-input"
|
||||
/>
|
||||
{row.direct && (
|
||||
<select
|
||||
value={row.directLevel ?? 'USE'}
|
||||
disabled={isToggling}
|
||||
onChange={(e) =>
|
||||
changeDirectLevel(row.module.id, e.target.value as GrantLevel)
|
||||
}
|
||||
aria-label={t('directLevelLabel', {
|
||||
module: row.module.name,
|
||||
user: username,
|
||||
})}
|
||||
className="h-7 rounded-md border border-input bg-background px-1 text-xs"
|
||||
>
|
||||
<option value="USE">{tLevel('levelUse')}</option>
|
||||
<option value="MANAGE">{tLevel('levelManage')}</option>
|
||||
</select>
|
||||
)}
|
||||
{isToggling && (
|
||||
<span className="absolute -right-4 inline-block h-3 w-3 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent" />
|
||||
)}
|
||||
|
||||
@@ -22,6 +22,12 @@ const messages: Record<string, Record<string, unknown>> = {
|
||||
directCheckboxLabel:
|
||||
'{module} direkt für {user} {granted, select, true {freigeben} other {entziehen}}',
|
||||
saveError: 'Freigabe konnte nicht gespeichert werden. Bitte erneut versuchen.',
|
||||
directLevelLabel: 'Stufe der direkten Freigabe von {module} für {user}',
|
||||
manageMarker: 'Verwalten',
|
||||
},
|
||||
'admin.groups.grants': {
|
||||
levelUse: 'Benutzen',
|
||||
levelManage: 'Verwalten',
|
||||
},
|
||||
'admin.groups.members': {
|
||||
sourceManual: 'Manuell',
|
||||
@@ -285,3 +291,90 @@ describe('UserAccessModal', () => {
|
||||
expect(screen.getByText('LDAP')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('UserAccessModal — Freigabestufe (261002-icv)', () => {
|
||||
const levelAccess = {
|
||||
groups: [{ id: 'g1', name: 'Alle Benutzer', source: 'MANUAL' as const }],
|
||||
modules: [
|
||||
{
|
||||
module: { id: 'm1', name: 'Proxmox', category: 'infrastructure' },
|
||||
viaGroups: ['Alle Benutzer'],
|
||||
direct: false,
|
||||
directLevel: null,
|
||||
manageViaGroups: ['Alle Benutzer'],
|
||||
},
|
||||
{
|
||||
module: { id: 'm2', name: 'DKV Flotte', category: 'fleet' },
|
||||
viaGroups: [],
|
||||
direct: true,
|
||||
directLevel: 'USE' as const,
|
||||
manageViaGroups: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it('zeigt bei der Direkt-Freigabe ein Stufenfeld und wechselt die Stufe per POST mit level', async () => {
|
||||
const fetchMock = vi.fn((_url: string, init?: RequestInit) => {
|
||||
if (init?.method === 'POST') {
|
||||
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
|
||||
}
|
||||
return Promise.resolve({ ok: true, json: () => Promise.resolve(levelAccess) });
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
render(<UserAccessModal userId="u1" username="Maxi Musterfrau" onClose={vi.fn()} />);
|
||||
|
||||
const select = (await screen.findByLabelText(
|
||||
'Stufe der direkten Freigabe von DKV Flotte für Maxi Musterfrau',
|
||||
)) as HTMLSelectElement;
|
||||
expect(select.value).toBe('USE');
|
||||
// Ohne Direkt-Freigabe kein Stufenfeld.
|
||||
expect(
|
||||
screen.queryByLabelText('Stufe der direkten Freigabe von Proxmox für Maxi Musterfrau'),
|
||||
).toBeNull();
|
||||
|
||||
await userEvent.selectOptions(select, 'MANAGE');
|
||||
|
||||
await waitFor(() => {
|
||||
const post = fetchMock.mock.calls.find(([, init]) => init?.method === 'POST');
|
||||
expect(post).toBeTruthy();
|
||||
expect(JSON.parse(String(post?.[1]?.body))).toEqual({
|
||||
moduleId: 'm2',
|
||||
userId: 'u1',
|
||||
level: 'MANAGE',
|
||||
});
|
||||
});
|
||||
expect(select.value).toBe('MANAGE');
|
||||
});
|
||||
|
||||
it('setzt die Stufe bei einem Fehler zurück und zeigt die Meldung', async () => {
|
||||
const fetchMock = vi.fn((_url: string, init?: RequestInit) => {
|
||||
if (init?.method === 'POST') {
|
||||
return Promise.resolve({ ok: false, status: 500, text: () => Promise.resolve('boom') });
|
||||
}
|
||||
return Promise.resolve({ ok: true, json: () => Promise.resolve(levelAccess) });
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
render(<UserAccessModal userId="u1" username="Maxi Musterfrau" onClose={vi.fn()} />);
|
||||
|
||||
const select = (await screen.findByLabelText(
|
||||
'Stufe der direkten Freigabe von DKV Flotte für Maxi Musterfrau',
|
||||
)) as HTMLSelectElement;
|
||||
await userEvent.selectOptions(select, 'MANAGE');
|
||||
|
||||
await waitFor(() => expect(screen.getByText(/500: boom/)).toBeInTheDocument());
|
||||
expect(select.value).toBe('USE');
|
||||
});
|
||||
|
||||
it('markiert eine Gruppe, die Verwalten gewährt, mit „Verwalten“', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve(levelAccess) })),
|
||||
);
|
||||
|
||||
render(<UserAccessModal userId="u1" username="Maxi Musterfrau" onClose={vi.fn()} />);
|
||||
|
||||
expect(await screen.findByText(/· Verwalten/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -553,6 +553,8 @@
|
||||
"noInheritance": "–",
|
||||
"noActiveModules": "Für diesen Mandanten sind keine Module aktiviert.",
|
||||
"directCheckboxLabel": "{module} direkt für {user} {granted, select, true {freigeben} other {entziehen}}",
|
||||
"directLevelLabel": "Stufe der direkten Freigabe von {module} für {user}",
|
||||
"manageMarker": "Verwalten",
|
||||
"saveError": "Freigabe konnte nicht gespeichert werden. Bitte erneut versuchen."
|
||||
},
|
||||
"errors": {
|
||||
@@ -730,7 +732,10 @@
|
||||
"body": "Diese Gruppe hat {memberCount, plural, one {# Mitglied} other {# Mitglieder}} und {grantCount, plural, one {# Modul-Freigabe} other {# Modul-Freigaben}}. Betroffene Benutzer verlieren den Zugriff, sofern sie ihn nicht anderweitig haben. Diese Aktion kann nicht rückgängig gemacht werden."
|
||||
},
|
||||
"grants": {
|
||||
"matrixCheckboxLabel": "{module} für Gruppe {group} {granted, select, true {freigeben} other {entziehen}}"
|
||||
"matrixCheckboxLabel": "{module} für Gruppe {group} {granted, select, true {freigeben} other {entziehen}}",
|
||||
"levelUse": "Benutzen",
|
||||
"levelManage": "Verwalten",
|
||||
"levelSelectLabel": "Stufe für {module} in Gruppe {group}"
|
||||
}
|
||||
},
|
||||
"customModules": {
|
||||
@@ -860,7 +865,8 @@
|
||||
"searchPlaceholder": "Module oder Gruppen durchsuchen...",
|
||||
"emptyModules": "Es sind noch keine Module für diesen Mandanten aktiviert. Aktiviere zuerst ein Modul unter Module.",
|
||||
"emptyModulesLink": "Zu Module",
|
||||
"adminNote": "ADMIN und SUPER_ADMIN haben immer Zugriff auf alle aktiven Module — diese Matrix betrifft nur die Rolle USER.",
|
||||
"levelExplanation": "„Benutzen“: Das Modul öffnen und damit arbeiten. „Verwalten“: zusätzlich die Einstellungen dieses Moduls ändern. Freigaben vergeben und Module aktivieren dürfen weiterhin nur Administratoren. Hat jemand über mehrere Wege Zugriff, gilt die höhere Stufe.",
|
||||
"adminNote": "Administratoren haben immer Zugriff auf alle aktiven Module und dürfen deren Einstellungen ändern – diese Matrix betrifft nur Benutzer ohne Administratorrechte.",
|
||||
"saveError": "Freigabe konnte nicht gespeichert werden. Bitte erneut versuchen.",
|
||||
"noSearchResults": "Kein Treffer für „{search}\" — weder bei den Modulen noch bei den Gruppen."
|
||||
},
|
||||
|
||||
@@ -553,6 +553,8 @@
|
||||
"noInheritance": "–",
|
||||
"noActiveModules": "No modules are activated for this tenant.",
|
||||
"directCheckboxLabel": "{granted, select, true {Grant} other {Revoke}} {module} directly for {user}",
|
||||
"directLevelLabel": "Level of the direct grant of {module} for {user}",
|
||||
"manageMarker": "Manage",
|
||||
"saveError": "Could not save grant. Please try again."
|
||||
},
|
||||
"errors": {
|
||||
@@ -730,7 +732,10 @@
|
||||
"body": "This group has {memberCount, plural, one {# member} other {# members}} and {grantCount, plural, one {# module grant} other {# module grants}}. Affected users will lose access unless they have it another way. This action cannot be undone."
|
||||
},
|
||||
"grants": {
|
||||
"matrixCheckboxLabel": "{granted, select, true {Grant} other {Revoke}} {module} for group {group}"
|
||||
"matrixCheckboxLabel": "{granted, select, true {Grant} other {Revoke}} {module} for group {group}",
|
||||
"levelUse": "Use",
|
||||
"levelManage": "Manage",
|
||||
"levelSelectLabel": "Level for {module} in group {group}"
|
||||
}
|
||||
},
|
||||
"customModules": {
|
||||
@@ -860,7 +865,8 @@
|
||||
"searchPlaceholder": "Search modules or groups...",
|
||||
"emptyModules": "No modules are activated for this tenant yet. Activate a module under Modules first.",
|
||||
"emptyModulesLink": "Go to Modules",
|
||||
"adminNote": "ADMIN and SUPER_ADMIN always have access to all active modules — this matrix only applies to the USER role.",
|
||||
"levelExplanation": "“Use”: open the module and work with it. “Manage”: additionally change the settings of this module. Only administrators may grant access and activate modules. If someone has access through several routes, the higher level applies.",
|
||||
"adminNote": "Administrators always have access to all active modules and may change their settings — this matrix only affects users without administrator rights.",
|
||||
"saveError": "Could not save grant. Please try again.",
|
||||
"noSearchResults": "No match for \"{search}\" — neither in the modules nor in the groups."
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user