Skip to content

Commit 07b9708

Browse files
authored
Show teams with no members yet in the user directory (#7855)
# Description of Changes `buildDirectory` dropped any team whose member list was empty, so creating a team with the optional owner field blank looked like it had failed: the form succeeded, the roster was unchanged, and there was no row to add the first member from. The team was only reachable through the legacy Teams settings screen and the team-selection dropdowns, which is not where anyone goes looking for a team they just made. Removing the filter is enough. `DataTable` already renders a group header for an empty group, and "Add to team" is the first team action, so a new team appears as a section with 0 people and an immediate way to fill it. The internal system team is already excluded server-side by the teams endpoint, so nothing unwanted surfaces. **Before:** create a team with no owner → nothing appears in Users. **After:** the team appears with 0 people and an Add to team action. --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [x] I have performed a self-review of my own code - [x] Every comment I added says something the code does not ([guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/CODE_COMMENTS.md)) - [x] My changes generate no new warnings ### Testing (if applicable) - [x] New `directory.test.ts` covers the empty team, sort order, and that admins/guests still stay out of team sections - [x] New `UsersDirectory.test.tsx` case asserts the empty team renders with its Add to team action - [x] All three fail on `main` and pass here - [x] `npx vitest run src/portal` passes: 91 files, 584 tests - [x] `tsc --noEmit` (proprietary variant), `oxlint --max-warnings=0` and `oxfmt` all clean
1 parent 12043a2 commit 07b9708

3 files changed

Lines changed: 78 additions & 3 deletions

File tree

frontend/editor/src/portal/components/users/UsersDirectory.test.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,16 @@ describe("UsersDirectory — remove action gating", () => {
9393
).not.toBeInTheDocument();
9494
expect(screen.queryByText("Rename team")).not.toBeInTheDocument();
9595
});
96+
97+
it("lists a team with no members yet, so its first member can be added", () => {
98+
renderDirectory(selfHostedCaps, [
99+
...TEAMS,
100+
{ id: 2, name: "Brand new", userCount: 0, owners: [] },
101+
]);
102+
103+
expect(screen.getByText("Brand new team")).toBeInTheDocument();
104+
expect(screen.getAllByText("Add to team")).toHaveLength(2);
105+
});
96106
});
97107

98108
describe("flavor capabilities — invitations + remove scope", () => {
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { describe, expect, it } from "vitest";
2+
import { buildDirectory } from "@portal/components/users/directory";
3+
import type { Member } from "@portal/api/users";
4+
import type { Team } from "@portal/api/teams";
5+
6+
const member = (overrides: Partial<Member> = {}): Member => ({
7+
id: "2",
8+
name: "Priya",
9+
email: "priya@acme.com",
10+
username: "priya@acme.com",
11+
role: "member",
12+
status: "active",
13+
lastActive: "-",
14+
teamId: 1,
15+
teamName: "Acme",
16+
...overrides,
17+
});
18+
19+
const team = (id: number, name: string): Team => ({
20+
id,
21+
name,
22+
userCount: 0,
23+
owners: [],
24+
});
25+
26+
describe("buildDirectory", () => {
27+
it("keeps a team that has no members yet", () => {
28+
const dir = buildDirectory(
29+
[member()],
30+
[team(1, "Acme"), team(2, "Brand new")],
31+
);
32+
33+
expect(dir.teams.map((t) => t.name)).toEqual(["Acme", "Brand new"]);
34+
expect(dir.teams[1].members).toEqual([]);
35+
});
36+
37+
it("sorts teams by name regardless of how many members they have", () => {
38+
const dir = buildDirectory(
39+
[member()],
40+
[team(3, "Zephyr"), team(1, "Acme"), team(2, "Meridian")],
41+
);
42+
43+
expect(dir.teams.map((t) => t.name)).toEqual([
44+
"Acme",
45+
"Meridian",
46+
"Zephyr",
47+
]);
48+
});
49+
50+
it("still keeps admins and guests out of their team's section", () => {
51+
const dir = buildDirectory(
52+
[
53+
member({ id: "1", username: "root", role: "admin" }),
54+
member({ id: "3", username: "vendor", role: "guest" }),
55+
member(),
56+
],
57+
[team(1, "Acme")],
58+
);
59+
60+
expect(dir.organization).toHaveLength(1);
61+
expect(dir.guests).toHaveLength(1);
62+
expect(dir.teams[0].members.map((m) => m.username)).toEqual([
63+
"priya@acme.com",
64+
]);
65+
});
66+
});

frontend/editor/src/portal/components/users/directory.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ export interface Directory {
2222
/**
2323
* Group the flat roster into the directory shape the Users page renders:
2424
* admins are the Organization owners, web-only users are Guests, and everyone
25-
* else sits under their team. Empty teams are omitted from the roster (they're
26-
* still creatable / visible via team management).
25+
* else sits under their team. A team with no members still gets a section, so
26+
* a newly created one can be found and given its first member.
2727
*/
2828
export function buildDirectory(members: Member[], teams: Team[]): Directory {
2929
const organization = members.filter((m) => m.role === "admin");
@@ -46,7 +46,6 @@ export function buildDirectory(members: Member[], teams: Team[]): Directory {
4646
members: byTeam.get(t.id) ?? [],
4747
isPersonal: t.isPersonal,
4848
}))
49-
.filter((g) => g.members.length > 0)
5049
.sort((a, b) => a.name.localeCompare(b.name));
5150

5251
return { organization, teams: teamGroups, guests };

0 commit comments

Comments
 (0)