diff --git a/frontend/src/__tests__/ProjectGroupsSection.test.jsx b/frontend/src/__tests__/ProjectGroupsSection.test.jsx index 0713241..2bb81c7 100644 --- a/frontend/src/__tests__/ProjectGroupsSection.test.jsx +++ b/frontend/src/__tests__/ProjectGroupsSection.test.jsx @@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest" import { render, screen, fireEvent, waitFor } from "@testing-library/react" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import "../i18n" import { ProjectGroupsSection } from "../components/project/ProjectGroupsSection" vi.mock("sonner", () => ({ toast: { success: vi.fn(), error: vi.fn() } })) @@ -100,4 +101,31 @@ describe("ProjectGroupsSection", () => { await waitFor(() => expect(projectGroupsApi.remove).toHaveBeenCalledWith(42, 10)) }) + + it("hints to create a group when none exist", async () => { + projectGroupsApi.list.mockResolvedValue([]) + api.get.mockResolvedValue({ data: [] }) + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + render( + + + , + ) + + expect(await screen.findByText(/no groups exist yet/i)).toBeInTheDocument() + }) + + it("hints when every existing group is already assigned", async () => { + projectGroupsApi.list.mockResolvedValue(ASSIGNMENTS) + api.get.mockResolvedValue({ data: [{ id: 10, name: "QA Team" }] }) + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + render( + + + , + ) + + await screen.findByText("QA Team") + expect(screen.getByText(/every existing group is already assigned/i)).toBeInTheDocument() + }) }) diff --git a/frontend/src/components/project/ProjectGroupsSection.jsx b/frontend/src/components/project/ProjectGroupsSection.jsx index 0284989..6e2a8ae 100644 --- a/frontend/src/components/project/ProjectGroupsSection.jsx +++ b/frontend/src/components/project/ProjectGroupsSection.jsx @@ -79,13 +79,25 @@ export function ProjectGroupsSection({ projectId }) { )} - {candidates.length > 0 && ( + {candidates.length > 0 ? ( add.mutate({ groupId, role })} /> + ) : ( + 0} /> )} ) } + +function NoCandidatesHint({ hasGroups }) { + const { t } = useTranslation("members") + return ( +

+ {hasGroups ? t("groups.noCandidatesAllAssigned") : t("groups.noCandidatesEmpty")} +

+ ) +} + function AssignmentRow({ assignment, onRoleChange, onRemove }) { const { t } = useTranslation("members") return ( diff --git a/frontend/src/i18n/locales/ca/members.json b/frontend/src/i18n/locales/ca/members.json index 20a3f28..75d0812 100644 --- a/frontend/src/i18n/locales/ca/members.json +++ b/frontend/src/i18n/locales/ca/members.json @@ -36,7 +36,9 @@ "empty": { "title": "Cap grup assignat", "description": "Assigna un grup per donar a tots els seus membres el mateix rol en aquest projecte." - } + }, + "noCandidatesEmpty": "Encara no existeix cap grup. Crea'n un a l'administració d'Usuaris (pestanya Grups) i apareixerà aquí.", + "noCandidatesAllAssigned": "Tots els grups existents ja estan assignats a aquest projecte." }, "tokens": { "title": "Tokens d'API del projecte", diff --git a/frontend/src/i18n/locales/en/members.json b/frontend/src/i18n/locales/en/members.json index e230b79..c50cc28 100644 --- a/frontend/src/i18n/locales/en/members.json +++ b/frontend/src/i18n/locales/en/members.json @@ -36,7 +36,9 @@ "empty": { "title": "No groups assigned", "description": "Assign a group to give every member of it the same role on this project." - } + }, + "noCandidatesEmpty": "No groups exist yet. Create one in the Users admin (Groups tab) and it will appear here.", + "noCandidatesAllAssigned": "Every existing group is already assigned to this project." }, "tokens": { "title": "Project API Tokens", diff --git a/frontend/src/i18n/locales/es/members.json b/frontend/src/i18n/locales/es/members.json index 6515115..5ea7409 100644 --- a/frontend/src/i18n/locales/es/members.json +++ b/frontend/src/i18n/locales/es/members.json @@ -36,7 +36,9 @@ "empty": { "title": "Sin grupos asignados", "description": "Asigna un grupo para dar a todos sus miembros el mismo rol en este proyecto." - } + }, + "noCandidatesEmpty": "Aún no existe ningún grupo. Crea uno en la administración de Usuarios (pestaña Grupos) y aparecerá aquí.", + "noCandidatesAllAssigned": "Todos los grupos existentes ya están asignados a este proyecto." }, "tokens": { "title": "Tokens de API del proyecto", diff --git a/frontend/src/i18n/locales/eu/members.json b/frontend/src/i18n/locales/eu/members.json index b248780..2f4f510 100644 --- a/frontend/src/i18n/locales/eu/members.json +++ b/frontend/src/i18n/locales/eu/members.json @@ -36,7 +36,9 @@ "empty": { "title": "Talderik esleitu gabe", "description": "Esleitu talde bat haren kide guztiei rol bera emateko proiektu honetan." - } + }, + "noCandidatesEmpty": "Oraindik ez dago talderik. Sortu bat Erabiltzaileen administrazioan (Taldeak fitxa) eta hemen agertuko da.", + "noCandidatesAllAssigned": "Lehendik dauden talde guztiak proiektu honi esleituta daude." }, "tokens": { "title": "Proiektuaren API tokenak", diff --git a/frontend/src/i18n/locales/gl/members.json b/frontend/src/i18n/locales/gl/members.json index c809e05..8f2d759 100644 --- a/frontend/src/i18n/locales/gl/members.json +++ b/frontend/src/i18n/locales/gl/members.json @@ -36,7 +36,9 @@ "empty": { "title": "Sen grupos asignados", "description": "Asigna un grupo para darlles a todos os seus membros o mesmo rol neste proxecto." - } + }, + "noCandidatesEmpty": "Aínda non existe ningún grupo. Crea un na administración de Usuarios (pestana Grupos) e aparecerá aquí.", + "noCandidatesAllAssigned": "Todos os grupos existentes xa están asignados a este proxecto." }, "tokens": { "title": "Tokens de API do proxecto", diff --git a/frontend/src/pages/MembersPage.jsx b/frontend/src/pages/MembersPage.jsx index abfe3c2..299e2b3 100644 --- a/frontend/src/pages/MembersPage.jsx +++ b/frontend/src/pages/MembersPage.jsx @@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next" import { Shield, Trash2, Plus } from "lucide-react" import { useMembers, useAddMember, useUpdateMember, useRemoveMember } from "../hooks/useMembers" import { useProject } from "../hooks/useProjects" -import { Breadcrumbs } from "../components/ui/breadcrumbs" +import { PageHeader, PageBody } from "../components/ui/page-header" import { useQuery } from "@tanstack/react-query" import { api } from "../api/client" import { Button } from "../components/ui/button" @@ -73,26 +73,47 @@ export function MembersPage() { } } + const crumbs = [ + { label: t("nav:global.projects"), to: "/projects" }, + { label: project?.name ?? "…", to: `/projects/${projectId}` }, + { label: t("nav:project.members") }, + ] + if (membersQuery.isError) { return ( -
- membersQuery.refetch()} /> -
+ <> + +

{t("title")}

+
+ +
+ membersQuery.refetch()} /> +
+
+ + ) + } + if (isLoading) { + return ( + <> + +

{t("title")}

+
+ +

{t("loading")}

+
+ ) } - if (isLoading) return

{t("loading")}

return ( -
- -

{t("title")}

+ <> + +

{t("title")}

+
+ +
@@ -142,7 +163,7 @@ export function MembersPage() { )} {!usersQuery.isError && addableUsers.length > 0 && ( -
+