fix(quick-260909-ab3): Matrix-Suche filtert nur noch die getroffene Achse
- Gemeinsamer useMemo ermittelt moduleHits/groupHits getrennt; trifft der Suchbegriff nur eine Achse, bleibt die andere vollstaendig sichtbar (WINDOWS #14) statt leerzulaufen - Regressionsschutz fuer WINDOWS #6c erhalten: Spaltensuche findet eine importierte Gruppe weiterhin unter internem UND AD-Namen - Neue Meldung adminModules.grants.noSearchResults, wenn ein Begriff weder Modul noch Gruppe trifft — Tabelle wird dann nicht gerendert - Vier neue Testfaelle in grants-matrix.test.tsx vorab gegen den unveraenderten Bestand rot gelaufen (erwartete Ursachen bestaetigt) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FYZcd3SSmo14QTqWx2KKzU
This commit is contained in:
@@ -18,6 +18,7 @@ const messages: Record<string, Record<string, unknown>> = {
|
|||||||
adminNote:
|
adminNote:
|
||||||
'ADMIN und SUPER_ADMIN haben immer Zugriff auf alle aktiven Module — diese Matrix betrifft nur die Rolle USER.',
|
'ADMIN und SUPER_ADMIN haben immer Zugriff auf alle aktiven Module — diese Matrix betrifft nur die Rolle USER.',
|
||||||
saveError: 'Freigabe konnte nicht gespeichert werden. Bitte erneut versuchen.',
|
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': {
|
'admin.groups.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}}',
|
||||||
@@ -93,6 +94,7 @@ const mockMatrix = {
|
|||||||
groups: [
|
groups: [
|
||||||
{ id: 'g1', name: 'Alle Benutzer' },
|
{ id: 'g1', name: 'Alle Benutzer' },
|
||||||
{ id: 'g2', name: 'Buchhaltung' },
|
{ id: 'g2', name: 'Buchhaltung' },
|
||||||
|
{ id: 'g3', name: 'Claude_VT', internalName: 'Vertrieb' },
|
||||||
],
|
],
|
||||||
grants: [{ moduleId: 'm1', groupId: 'g1' }],
|
grants: [{ moduleId: 'm1', groupId: 'g1' }],
|
||||||
};
|
};
|
||||||
@@ -203,6 +205,102 @@ describe('AdminModuleGrantsPage (Permission-Matrix)', () => {
|
|||||||
expect(screen.getByText('DKV Flotte')).toBeInTheDocument();
|
expect(screen.getByText('DKV Flotte')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('a module-name search leaves all group columns standing (WINDOWS #14)', async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve(mockMatrix) })),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<AdminModuleGrantsPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('DKV Flotte')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchInput = screen.getByPlaceholderText('Module oder Gruppen durchsuchen...');
|
||||||
|
await userEvent.type(searchInput, 'Buchhaltung');
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Ausschreibungs-Radar')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(screen.getByText('DKV Flotte')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Alle Benutzer')).not.toBeInTheDocument();
|
||||||
|
expect(screen.getAllByRole('checkbox').length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('a group-name search leaves all module rows standing (WINDOWS #14)', async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve(mockMatrix) })),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<AdminModuleGrantsPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('DKV Flotte')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchInput = screen.getByPlaceholderText('Module oder Gruppen durchsuchen...');
|
||||||
|
await userEvent.type(searchInput, 'Flotte');
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Alle Benutzer')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(screen.getByText('Buchhaltung')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Ausschreibungs-Radar')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('finds an imported group by both internal and AD name (WINDOWS #6c regression)', async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve(mockMatrix) })),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<AdminModuleGrantsPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('DKV Flotte')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchInput = screen.getByPlaceholderText('Module oder Gruppen durchsuchen...');
|
||||||
|
|
||||||
|
await userEvent.type(searchInput, 'Vertrieb');
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Vertrieb')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(screen.getByText('Ausschreibungs-Radar')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('DKV Flotte')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.clear(searchInput);
|
||||||
|
await userEvent.type(searchInput, 'Claude_VT');
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Vertrieb')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(screen.getByText('Ausschreibungs-Radar')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('DKV Flotte')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows a visible message when the search term matches neither modules nor groups', async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve(mockMatrix) })),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<AdminModuleGrantsPage />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('DKV Flotte')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchInput = screen.getByPlaceholderText('Module oder Gruppen durchsuchen...');
|
||||||
|
await userEvent.type(searchInput, 'zzz');
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/Kein Treffer für "zzz"/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
expect(screen.queryAllByRole('checkbox')).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
it('gives every matrix checkbox an aria-label', async () => {
|
it('gives every matrix checkbox an aria-label', async () => {
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
'fetch',
|
'fetch',
|
||||||
|
|||||||
@@ -133,19 +133,33 @@ export default function AdminModuleGrantsPage() {
|
|||||||
const searchLower = search.trim().toLowerCase();
|
const searchLower = search.trim().toLowerCase();
|
||||||
const matches = (text: string) => searchLower === '' || text.toLowerCase().includes(searchLower);
|
const matches = (text: string) => searchLower === '' || text.toLowerCase().includes(searchLower);
|
||||||
|
|
||||||
const filteredModules = useMemo(
|
// Both axes are searched independently, then combined with the following
|
||||||
() => modules.filter((m) => matches(m.name)),
|
// semantics (WINDOWS #14): a term that hits only one axis must not empty
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// the other axis, or no clickable cell ever remains. See <behavior> in
|
||||||
[modules, searchLower],
|
// 260909-ab3-PLAN.md for the full truth table.
|
||||||
);
|
const { filteredModules, filteredGroups, noMatch } = useMemo(() => {
|
||||||
const filteredGroups = useMemo(
|
if (searchLower === '') {
|
||||||
|
return { filteredModules: modules, filteredGroups: groups, noMatch: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const moduleHits = modules.filter((m) => matches(m.name));
|
||||||
// Matches both the display name (internalName ?? name) and the stored
|
// Matches both the display name (internalName ?? name) and the stored
|
||||||
// AD name — a search for either the internal or the AD-original name
|
// AD name — a search for either the internal or the AD-original name
|
||||||
// must find the group (D-04).
|
// must find the group (D-04).
|
||||||
() => groups.filter((g) => matches(g.internalName ?? g.name) || matches(g.name)),
|
const groupHits = groups.filter((g) => matches(g.internalName ?? g.name) || matches(g.name));
|
||||||
|
|
||||||
|
if (moduleHits.length === 0 && groupHits.length === 0) {
|
||||||
|
return { filteredModules: [], filteredGroups: [], noMatch: true };
|
||||||
|
}
|
||||||
|
if (moduleHits.length > 0 && groupHits.length === 0) {
|
||||||
|
return { filteredModules: moduleHits, filteredGroups: groups, noMatch: false };
|
||||||
|
}
|
||||||
|
if (groupHits.length > 0 && moduleHits.length === 0) {
|
||||||
|
return { filteredModules: modules, filteredGroups: groupHits, noMatch: false };
|
||||||
|
}
|
||||||
|
return { filteredModules: moduleHits, filteredGroups: groupHits, noMatch: false };
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[groups, searchLower],
|
}, [modules, groups, searchLower]);
|
||||||
);
|
|
||||||
|
|
||||||
const groupedModules = useMemo(() => {
|
const groupedModules = useMemo(() => {
|
||||||
const result: { category: string; mods: Module[] }[] = [];
|
const result: { category: string; mods: Module[] }[] = [];
|
||||||
@@ -202,6 +216,11 @@ export default function AdminModuleGrantsPage() {
|
|||||||
className="flex h-9 w-full max-w-sm rounded-md border border-input bg-background px-3 py-1 text-sm"
|
className="flex h-9 w-full max-w-sm rounded-md border border-input bg-background px-3 py-1 text-sm"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{noMatch ? (
|
||||||
|
<div className="rounded-md border border-border bg-card p-6 text-center">
|
||||||
|
<p className="text-sm text-muted-foreground">{t('noSearchResults', { search })}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="overflow-x-auto overflow-y-auto max-h-[70vh] rounded-md border border-border">
|
<div className="overflow-x-auto overflow-y-auto max-h-[70vh] rounded-md border border-border">
|
||||||
<table className="w-full text-sm border-collapse">
|
<table className="w-full text-sm border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -274,6 +293,7 @@ export default function AdminModuleGrantsPage() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<p className="text-xs text-muted-foreground">{t('adminNote')}</p>
|
<p className="text-xs text-muted-foreground">{t('adminNote')}</p>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -457,7 +457,8 @@
|
|||||||
"emptyModules": "Es sind noch keine Module für diesen Mandanten aktiviert. Aktiviere zuerst ein Modul unter Module.",
|
"emptyModules": "Es sind noch keine Module für diesen Mandanten aktiviert. Aktiviere zuerst ein Modul unter Module.",
|
||||||
"emptyModulesLink": "Zu Module",
|
"emptyModulesLink": "Zu Module",
|
||||||
"adminNote": "ADMIN und SUPER_ADMIN haben immer Zugriff auf alle aktiven Module — diese Matrix betrifft nur die Rolle USER.",
|
"adminNote": "ADMIN und SUPER_ADMIN haben immer Zugriff auf alle aktiven Module — diese Matrix betrifft nur die Rolle USER.",
|
||||||
"saveError": "Freigabe konnte nicht gespeichert werden. Bitte erneut versuchen."
|
"saveError": "Freigabe konnte nicht gespeichert werden. Bitte erneut versuchen.",
|
||||||
|
"noSearchResults": "Kein Treffer für „{search}\" — weder bei den Modulen noch bei den Gruppen."
|
||||||
},
|
},
|
||||||
"activationDialog": {
|
"activationDialog": {
|
||||||
"title": "Modul aktivieren: {module}",
|
"title": "Modul aktivieren: {module}",
|
||||||
|
|||||||
@@ -457,7 +457,8 @@
|
|||||||
"emptyModules": "No modules are activated for this tenant yet. Activate a module under Modules first.",
|
"emptyModules": "No modules are activated for this tenant yet. Activate a module under Modules first.",
|
||||||
"emptyModulesLink": "Go to Modules",
|
"emptyModulesLink": "Go to Modules",
|
||||||
"adminNote": "ADMIN and SUPER_ADMIN always have access to all active modules — this matrix only applies to the USER role.",
|
"adminNote": "ADMIN and SUPER_ADMIN always have access to all active modules — this matrix only applies to the USER role.",
|
||||||
"saveError": "Could not save grant. Please try again."
|
"saveError": "Could not save grant. Please try again.",
|
||||||
|
"noSearchResults": "No match for \"{search}\" — neither in the modules nor in the groups."
|
||||||
},
|
},
|
||||||
"activationDialog": {
|
"activationDialog": {
|
||||||
"title": "Activate Module: {module}",
|
"title": "Activate Module: {module}",
|
||||||
|
|||||||
Reference in New Issue
Block a user