Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions frontend/src/__tests__/ProjectGroupsSection.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() } }))
Expand Down Expand Up @@ -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(
<QueryClientProvider client={queryClient}>
<ProjectGroupsSection projectId={42} />
</QueryClientProvider>,
)

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(
<QueryClientProvider client={queryClient}>
<ProjectGroupsSection projectId={42} />
</QueryClientProvider>,
)

await screen.findByText("QA Team")
expect(screen.getByText(/every existing group is already assigned/i)).toBeInTheDocument()
})
})
14 changes: 13 additions & 1 deletion frontend/src/components/project/ProjectGroupsSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,25 @@ export function ProjectGroupsSection({ projectId }) {
)}
</div>

{candidates.length > 0 && (
{candidates.length > 0 ? (
<AddRow candidates={candidates} onSubmit={(groupId, role) => add.mutate({ groupId, role })} />
) : (
<NoCandidatesHint hasGroups={groups.length > 0} />
)}
</section>
)
}


function NoCandidatesHint({ hasGroups }) {
const { t } = useTranslation("members")
return (
<p className="px-5 py-4 border-t bg-gray-50 dark:bg-gray-900 text-xs text-gray-500 dark:text-gray-400">
{hasGroups ? t("groups.noCandidatesAllAssigned") : t("groups.noCandidatesEmpty")}
</p>
)
}

function AssignmentRow({ assignment, onRoleChange, onRemove }) {
const { t } = useTranslation("members")
return (
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/i18n/locales/ca/members.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/i18n/locales/en/members.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/i18n/locales/es/members.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/i18n/locales/eu/members.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/i18n/locales/gl/members.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
57 changes: 40 additions & 17 deletions frontend/src/pages/MembersPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 (
<div className="pl-14 pr-4 py-4 md:p-8 max-w-2xl xl:max-w-4xl 2xl:max-w-5xl">
<ErrorState onRetry={() => membersQuery.refetch()} />
</div>
<>
<PageHeader crumbs={crumbs}>
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-100">{t("title")}</h1>
</PageHeader>
<PageBody>
<div className="max-w-4xl">
<ErrorState onRetry={() => membersQuery.refetch()} />
</div>
</PageBody>
</>
)
}
if (isLoading) {
return (
<>
<PageHeader crumbs={crumbs}>
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-100">{t("title")}</h1>
</PageHeader>
<PageBody>
<p className="text-gray-500 dark:text-gray-400">{t("loading")}</p>
</PageBody>
</>
)
}
if (isLoading) return <p className="text-gray-500 dark:text-gray-400">{t("loading")}</p>

return (
<div className="pl-14 pr-4 py-4 md:p-8 max-w-2xl xl:max-w-4xl 2xl:max-w-5xl space-y-6">
<Breadcrumbs
crumbs={[
{ label: t("nav:global.projects"), to: "/projects" },
{ label: project?.name ?? "…", to: `/projects/${projectId}` },
{ label: t("nav:project.members") },
]}
/>
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-100">{t("title")}</h1>
<>
<PageHeader crumbs={crumbs}>
<h1 className="text-2xl font-bold text-gray-800 dark:text-gray-100">{t("title")}</h1>
</PageHeader>

<PageBody>
<div className="max-w-4xl space-y-6">
<section className="bg-white dark:bg-gray-900 border rounded-xl shadow-sm overflow-hidden">
<div className="px-5 py-4 border-b flex items-center gap-2">
<Shield size={15} className="text-gray-500 dark:text-gray-400" />
Expand Down Expand Up @@ -142,7 +163,7 @@ export function MembersPage() {
)}

{!usersQuery.isError && addableUsers.length > 0 && (
<form onSubmit={handleAdd} className="px-5 py-4 border-t bg-gray-50 dark:bg-gray-900 flex gap-2">
<form onSubmit={handleAdd} className="px-5 py-4 border-t bg-gray-50 dark:bg-gray-900 flex flex-wrap gap-2">
<select
value={selectedUserId}
onChange={event => setSelectedUserId(event.target.value)}
Expand All @@ -167,7 +188,9 @@ export function MembersPage() {
)}
</section>

<ProjectGroupsSection projectId={projectId} />
</div>
<ProjectGroupsSection projectId={projectId} />
</div>
</PageBody>
</>
)
}
Loading