Skip to content

Commit 9100990

Browse files
authored
Merge pull request #1637 from thunderstore-io/team-settings-pt5-disband
Use API request to determine if user can leave or disband a team
2 parents 1a1217c + 53b482d commit 9100990

File tree

14 files changed

+214
-49
lines changed

14 files changed

+214
-49
lines changed

apps/cyberstorm-remix/app/settings/teams/team/tabs/Settings/Settings.tsx

Lines changed: 60 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,23 @@ import {
2626
} from "@thunderstore/thunderstore-api";
2727
import { ApiAction } from "@thunderstore/ts-api-react-actions";
2828

29-
import { NotLoggedIn } from "app/commonComponents/NotLoggedIn/NotLoggedIn";
3029
import { type OutletContextShape } from "app/root";
3130
import { makeTeamSettingsTabLoader } from "cyberstorm/utils/dapperClientLoaders";
31+
import { isTeamOwner } from "cyberstorm/utils/permissions";
3232
import { useStrongForm } from "cyberstorm/utils/StrongForm/useStrongForm";
3333

3434
export const clientLoader = makeTeamSettingsTabLoader(
35-
// TODO: add end point for checking can leave/disband status.
36-
async (dapper, teamName) => ({ teamName })
35+
async (dapper, teamName) => ({
36+
permissions: dapper.getCurrentUserTeamPermissions(teamName),
37+
})
3738
);
3839

3940
export default function Settings() {
40-
const { teamName } = useLoaderData<typeof clientLoader>();
41+
const { permissions, teamName } = useLoaderData<typeof clientLoader>();
4142
const outletContext = useOutletContext() as OutletContextShape;
4243
const toast = useToast();
4344
const navigate = useNavigate();
4445

45-
const currentUser = outletContext.currentUser?.username;
46-
if (!currentUser) return <NotLoggedIn />;
47-
4846
async function moveToTeams() {
4947
toast.addToast({
5048
csVariant: "info",
@@ -56,56 +54,51 @@ export default function Settings() {
5654

5755
return (
5856
<Suspense fallback={<div>Loading...</div>}>
59-
<Await resolve={teamName}>
60-
{(resolvedTeamName) => (
57+
<Await resolve={permissions}>
58+
{(resolvedPermissions) => (
6159
<div className="settings-items">
6260
<div className="settings-items__item">
6361
<div className="settings-items__meta">
6462
<p className="settings-items__title">Leave team</p>
65-
<p className="settings-items__description">Leave your team</p>
63+
<p className="settings-items__description">
64+
Resign from the team
65+
</p>
6666
</div>
6767
<div className="settings-items__content">
68-
<NewAlert csVariant="danger">
69-
You cannot currently leave this team as you are it&apos;s last
70-
owner.
71-
</NewAlert>
72-
<p>
73-
If you are the owner of the team, you can only leave if the
74-
team has another owner assigned.
75-
</p>
76-
<LeaveTeamForm
77-
userName={currentUser}
78-
teamName={resolvedTeamName}
79-
toast={toast}
80-
config={outletContext.requestConfig}
81-
updateTrigger={moveToTeams}
82-
/>
68+
{resolvedPermissions.can_leave_team ? (
69+
<LeaveTeamForm
70+
userName={outletContext.currentUser?.username ?? ""}
71+
teamName={teamName}
72+
toast={toast}
73+
config={outletContext.requestConfig}
74+
updateTrigger={moveToTeams}
75+
/>
76+
) : (
77+
<LastOwnerAlert />
78+
)}
8379
</div>
8480
</div>
8581
<div className="settings-items__separator" />
8682
<div className="settings-items__item">
8783
<div className="settings-items__meta">
8884
<p className="settings-items__title">Disband team</p>
8985
<p className="settings-items__description">
90-
Disband your team completely
86+
Remove the team completely
9187
</p>
9288
</div>
9389
<div className="settings-items__content">
94-
<NewAlert csVariant="danger">
95-
You cannot currently disband this team as it has packages.
96-
</NewAlert>
97-
<p>You are about to disband the team {resolvedTeamName}.</p>
98-
<p>
99-
Be aware you can currently only disband teams with no
100-
packages. If you need to archive a team with existing pages,
101-
contact Mythic#0001 on the Thunderstore Discord.
102-
</p>
103-
<DisbandTeamForm
104-
teamName={resolvedTeamName}
105-
updateTrigger={moveToTeams}
106-
config={outletContext.requestConfig}
107-
toast={toast}
108-
/>
90+
{resolvedPermissions.can_disband_team ? (
91+
<DisbandTeamForm
92+
teamName={teamName}
93+
updateTrigger={moveToTeams}
94+
config={outletContext.requestConfig}
95+
toast={toast}
96+
/>
97+
) : isTeamOwner(teamName, outletContext.currentUser) ? (
98+
<TeamHasPackagesAlert />
99+
) : (
100+
<NotTeamOwnerAlert />
101+
)}
109102
</div>
110103
</div>
111104
</div>
@@ -115,6 +108,28 @@ export default function Settings() {
115108
);
116109
}
117110

111+
const LastOwnerAlert = () => (
112+
<NewAlert csVariant="info">
113+
You cannot currently leave this team as you are its last owner.
114+
<br />
115+
To leave the team, you need to assign another owner to it. Alternatively,
116+
you can disband the whole team.
117+
</NewAlert>
118+
);
119+
120+
const TeamHasPackagesAlert = () => (
121+
<NewAlert csVariant="info">
122+
You cannot currently disband this team as it has packages.
123+
<br />
124+
If you need to archive this team, contact #support in the{" "}
125+
<a href="https://discord.thunderstore.io/">Thunderstore Discord</a>.
126+
</NewAlert>
127+
);
128+
129+
const NotTeamOwnerAlert = () => (
130+
<NewAlert csVariant="info">Only team owners can disband teams.</NewAlert>
131+
);
132+
118133
function LeaveTeamForm(props: {
119134
userName: string;
120135
teamName: string;
@@ -147,7 +162,7 @@ function LeaveTeamForm(props: {
147162
<Modal
148163
open={open}
149164
onOpenChange={setOpen}
150-
titleContent="Leave team"
165+
titleContent="Leave team?"
151166
csSize="small"
152167
trigger={
153168
<NewButton
@@ -170,7 +185,7 @@ function LeaveTeamForm(props: {
170185
team={teamName}
171186
csVariant="cyber"
172187
>
173-
{teamName}
188+
{teamName}.
174189
</NewLink>
175190
</span>
176191
</Modal.Body>
@@ -269,7 +284,7 @@ function DisbandTeamForm(props: {
269284
<Modal
270285
open={open}
271286
onOpenChange={setOpen}
272-
titleContent="Disband team"
287+
titleContent="Disband team?"
273288
csSize="small"
274289
trigger={
275290
<NewButton
@@ -292,7 +307,7 @@ function DisbandTeamForm(props: {
292307
team={teamName}
293308
csVariant="cyber"
294309
>
295-
{teamName}
310+
{teamName}.
296311
</NewLink>
297312
</div>
298313
<div>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { assert, describe, it } from "vitest";
2+
3+
import type { CurrentUser } from "@thunderstore/dapper/types";
4+
5+
import { isTeamOwner } from "../permissions";
6+
7+
describe("utils.permissions.isTeamOwner", () => {
8+
it("returns false if user is unauthenticated", () => {
9+
const actual = isTeamOwner("test-team", undefined);
10+
11+
assert.isFalse(actual);
12+
});
13+
14+
it("returns false if user does not belong to team", () => {
15+
const user = {
16+
teams_full: [{ name: "other-team", role: "owner", member_count: 1 }],
17+
};
18+
19+
const actual = isTeamOwner("test-team", user as CurrentUser);
20+
21+
assert.isFalse(actual);
22+
});
23+
24+
it("returns false if user is non-owner member", () => {
25+
const user = {
26+
teams_full: [{ name: "test-team", role: "member", member_count: 2 }],
27+
};
28+
29+
const actual = isTeamOwner("test-team", user as CurrentUser);
30+
31+
assert.isFalse(actual);
32+
});
33+
34+
it("returns true if user is owner of team", () => {
35+
const user = {
36+
teams_full: [
37+
{ name: "other-team", role: "member", member_count: 1 },
38+
{ name: "test-team", role: "owner", member_count: 3 },
39+
],
40+
};
41+
42+
const actual = isTeamOwner("test-team", user as CurrentUser);
43+
44+
assert.isTrue(actual);
45+
});
46+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { CurrentUser } from "@thunderstore/dapper/types";
2+
3+
export const isTeamOwner = (
4+
teamName: string,
5+
currentUser: CurrentUser | undefined
6+
) =>
7+
currentUser?.teams_full?.find((t) => t.name === teamName)?.role === "owner";

packages/dapper-fake/src/fakers/user.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,12 @@ const getFakeOAuthConnection = (provider: string) => ({
4242
username: faker.internet.userName(),
4343
avatar: faker.helpers.maybe(getFakeImg) ?? null,
4444
});
45+
46+
export const getFakeCurrentUserTeamPermissions = async (teamName: string) => {
47+
setSeed(teamName);
48+
49+
return {
50+
can_disband_team: faker.datatype.boolean(),
51+
can_leave_team: faker.datatype.boolean(),
52+
};
53+
};

packages/dapper-fake/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ import {
2121
getFakeTeamMembers,
2222
postFakeTeamCreate,
2323
} from "./fakers/team";
24-
import { getFakeCurrentUser } from "./fakers/user";
24+
import {
25+
getFakeCurrentUser,
26+
getFakeCurrentUserTeamPermissions,
27+
} from "./fakers/user";
2528
import { postFakePackageSubmissionMetadata } from "./fakers/submission";
2629
import { getFakePackageSubmissionStatus } from "./fakers/submission";
2730

@@ -30,6 +33,7 @@ export class DapperFake implements DapperInterface {
3033
public getCommunity = getFakeCommunity;
3134
public getCommunityFilters = getFakeCommunityFilters;
3235
public getCurrentUser = getFakeCurrentUser;
36+
public getCurrentUserTeamPermissions = getFakeCurrentUserTeamPermissions;
3337
public getPackageChangelog = getFakeChangelog;
3438
public getPackagePermissions = getFakePackagePermissions;
3539
public getPackageListingDetails = getFakePackageListingDetails;

packages/dapper-ts/src/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import { getDynamicHTML } from "./methods/dynamicHTML";
55
import { getCommunities, getCommunity } from "./methods/communities";
66
import { getCommunityFilters } from "./methods/communityFilters";
77
import { getRatedPackages } from "./methods/ratedPackages";
8-
import { getCurrentUser } from "./methods/currentUser";
8+
import {
9+
getCurrentUser,
10+
getCurrentUserTeamPermissions,
11+
} from "./methods/currentUser";
912
import {
1013
getPackageChangelog,
1114
getPackageReadme,
@@ -48,6 +51,8 @@ export class DapperTs implements DapperTsInterface {
4851
this.getCommunityFilters = this.getCommunityFilters.bind(this);
4952
this.getRatedPackages = this.getRatedPackages.bind(this);
5053
this.getCurrentUser = this.getCurrentUser.bind(this);
54+
this.getCurrentUserTeamPermissions =
55+
this.getCurrentUserTeamPermissions.bind(this);
5156
this.getPackageChangelog = this.getPackageChangelog.bind(this);
5257
this.getPackageListings = this.getPackageListings.bind(this);
5358
this.getPackageListingDetails = this.getPackageListingDetails.bind(this);
@@ -76,6 +81,7 @@ export class DapperTs implements DapperTsInterface {
7681
public getCommunityFilters = getCommunityFilters;
7782
public getRatedPackages = getRatedPackages;
7883
public getCurrentUser = getCurrentUser;
84+
public getCurrentUserTeamPermissions = getCurrentUserTeamPermissions;
7985
public getPackageChangelog = getPackageChangelog;
8086
public getPackageListings = getPackageListings;
8187
public getPackageListingDetails = getPackageListingDetails;

packages/dapper-ts/src/methods/currentUser.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { ApiError, fetchCurrentUser } from "@thunderstore/thunderstore-api";
1+
import {
2+
ApiError,
3+
fetchCurrentUser,
4+
fetchCurrentUserTeamPermissions,
5+
} from "@thunderstore/thunderstore-api";
26

37
import { DapperTsInterface } from "../index";
48

@@ -22,3 +26,15 @@ export async function getCurrentUser(this: DapperTsInterface) {
2226
}
2327
}
2428
}
29+
30+
export async function getCurrentUserTeamPermissions(
31+
this: DapperTsInterface,
32+
teamName: string
33+
) {
34+
return await fetchCurrentUserTeamPermissions({
35+
config: this.config,
36+
params: { team_name: teamName },
37+
data: {},
38+
queryParams: {},
39+
});
40+
}

packages/dapper/src/dapper.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface DapperInterface {
55
getCommunity: methods.GetCommunity;
66
getCommunityFilters: methods.GetCommunityFilters;
77
getCurrentUser: methods.GetCurrentUser;
8+
getCurrentUserTeamPermissions: methods.GetCurrentUserTeamPermissions;
89
getPackageChangelog: methods.GetPackageChangelog;
910
getPackageListingDetails: methods.GetPackageListingDetails;
1011
getPackageListings: methods.GetPackageListings;

packages/dapper/src/types/methods.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
import { type PackageListingType } from "./props";
1616
import { type HTMLContentResponse, type MarkdownResponse } from "./shared";
1717
import { type TeamDetails, type ServiceAccount, type TeamMember } from "./team";
18-
import { type CurrentUser } from "./user";
18+
import { type CurrentUser, type CurrentUserTeamPermissions } from "./user";
1919

2020
export type GetCommunities = (
2121
page?: number,
@@ -31,6 +31,10 @@ export type GetCommunityFilters = (
3131

3232
export type GetCurrentUser = () => Promise<null | CurrentUser>;
3333

34+
export type GetCurrentUserTeamPermissions = (
35+
teamName: string
36+
) => Promise<CurrentUserTeamPermissions>;
37+
3438
export type GetPackageChangelog = (
3539
namespace: string,
3640
name: string,

packages/dapper/src/types/user.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,8 @@ interface Badge {
4646
description: string;
4747
imageSource: string;
4848
}
49+
50+
export interface CurrentUserTeamPermissions {
51+
can_disband_team: boolean;
52+
can_leave_team: boolean;
53+
}

0 commit comments

Comments
 (0)