Skip to content

Commit 208b052

Browse files
committed
[TOOL-3476] Dashboard: Add Transfer Project in Project settings page (#6316)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on enhancing the project management features by introducing team member retrieval, project transfer capabilities, and UI improvements in the `ProjectGeneralSettingsPage`. It also updates the handling of team roles and member information. ### Detailed summary - Updated `AccountTeamsUI` to handle empty image string for avatars. - Added `getMemberById` function to fetch team member details. - Replaced `getMembers` with `getMemberById` in `Page` component. - Enhanced `DangerSettingCard` to include `isDisabled` prop. - Refactored `ProjectGeneralSettingsPage` to manage team roles and project transfers. - Introduced `TransferProject` component for transferring projects between teams. - Updated storybook stories to include owner and member account scenarios. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 514c1a8 commit 208b052

File tree

7 files changed

+322
-56
lines changed

7 files changed

+322
-56
lines changed

apps/dashboard/src/@/api/team-members.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,26 @@ export async function getMembers(teamSlug: string) {
4747

4848
return undefined;
4949
}
50+
51+
export async function getMemberById(teamSlug: string, memberId: string) {
52+
const token = await getAuthToken();
53+
54+
if (!token) {
55+
return undefined;
56+
}
57+
58+
const res = await fetch(
59+
`${API_SERVER_URL}/v1/teams/${teamSlug}/members/${memberId}`,
60+
{
61+
headers: {
62+
Authorization: `Bearer ${token}`,
63+
},
64+
},
65+
);
66+
67+
if (res.ok) {
68+
return (await res.json())?.result as TeamMember;
69+
}
70+
71+
return undefined;
72+
}

apps/dashboard/src/@/components/blocks/DangerSettingCard.tsx

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
DialogClose,
77
DialogContent,
88
DialogDescription,
9-
DialogFooter,
109
DialogHeader,
1110
DialogTitle,
1211
DialogTrigger,
@@ -17,13 +16,14 @@ export function DangerSettingCard(props: {
1716
title: string;
1817
className?: string;
1918
footerClassName?: string;
20-
description: string;
19+
description: React.ReactNode;
2120
buttonLabel: string;
2221
buttonOnClick: () => void;
22+
isDisabled?: boolean;
2323
isPending: boolean;
2424
confirmationDialog: {
2525
title: string;
26-
description: string;
26+
description: React.ReactNode;
2727
};
2828
children?: React.ReactNode;
2929
}) {
@@ -55,28 +55,30 @@ export function DangerSettingCard(props: {
5555
<Button
5656
variant="destructive"
5757
className="gap-2 bg-red-600 font-semibold text-white hover:bg-red-600/80"
58-
disabled={props.isPending}
58+
disabled={props.isDisabled || props.isPending}
5959
>
6060
{props.isPending && <Spinner className="size-3" />}
6161
{props.buttonLabel}
6262
</Button>
6363
</DialogTrigger>
6464

6565
<DialogContent
66-
className="z-[10001]"
66+
className="z-[10001] overflow-hidden p-0"
6767
dialogOverlayClassName="z-[10000]"
6868
>
69-
<DialogHeader className="pr-10">
70-
<DialogTitle className="leading-snug">
71-
{props.confirmationDialog.title}
72-
</DialogTitle>
69+
<div className="p-6">
70+
<DialogHeader className="pr-10">
71+
<DialogTitle className="leading-snug">
72+
{props.confirmationDialog.title}
73+
</DialogTitle>
7374

74-
<DialogDescription>
75-
{props.confirmationDialog.description}
76-
</DialogDescription>
77-
</DialogHeader>
75+
<DialogDescription>
76+
{props.confirmationDialog.description}
77+
</DialogDescription>
78+
</DialogHeader>
79+
</div>
7880

79-
<DialogFooter className="mt-4 gap-4 lg:gap-2">
81+
<div className="flex justify-end gap-4 border-t bg-card p-6 lg:gap-2">
8082
<DialogClose asChild>
8183
<Button variant="outline">Cancel</Button>
8284
</DialogClose>
@@ -90,7 +92,7 @@ export function DangerSettingCard(props: {
9092
{props.isPending && <Spinner className="size-3" />}
9193
{props.buttonLabel}
9294
</Button>
93-
</DialogFooter>
95+
</div>
9496
</DialogContent>
9597
</Dialog>
9698
</div>

apps/dashboard/src/app/account/overview/AccountTeamsUI.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ function TeamRow(props: {
108108
<div className="flex items-center gap-4">
109109
<GradientAvatar
110110
className="size-8"
111-
src={props.team.image || undefined}
111+
src={props.team.image || ""}
112112
id={props.team.id}
113113
client={props.client}
114114
/>

apps/dashboard/src/app/account/page.tsx

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { getTeams } from "@/api/team";
2-
import { getMembers } from "@/api/team-members";
2+
import { getMemberById } from "@/api/team-members";
33
import { getThirdwebClient } from "@/constants/thirdweb.server";
4+
import { notFound } from "next/navigation";
45
import { getAuthToken } from "../api/lib/getAuthToken";
56
import { loginRedirect } from "../login/loginRedirect";
67
import { AccountTeamsUI } from "./overview/AccountTeamsUI";
@@ -17,28 +18,20 @@ export default async function Page() {
1718
loginRedirect("/account");
1819
}
1920

20-
const teamsWithRole = (
21-
await Promise.all(
22-
teams.map(async (team) => {
23-
const members = await getMembers(team.slug);
24-
if (!members) {
25-
return {
26-
team,
27-
role: "MEMBER" as const,
28-
};
29-
}
21+
const teamsWithRole = await Promise.all(
22+
teams.map(async (team) => {
23+
const member = await getMemberById(team.slug, account.id);
3024

31-
const accountMemberInfo = members.find(
32-
(m) => m.accountId === account.id,
33-
);
25+
if (!member) {
26+
notFound();
27+
}
3428

35-
return {
36-
team,
37-
role: accountMemberInfo?.role || "MEMBER",
38-
};
39-
}),
40-
)
41-
).filter((x) => !!x);
29+
return {
30+
team,
31+
role: member.role,
32+
};
33+
}),
34+
);
4235

4336
return (
4437
<div className="flex grow flex-col">

apps/dashboard/src/app/team/[team_slug]/[project_slug]/settings/ProjectGeneralSettingsPage.stories.tsx

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1+
import { getThirdwebClient } from "@/constants/thirdweb.server";
12
import type { Meta, StoryObj } from "@storybook/react";
23
import { Toaster } from "sonner";
3-
import { projectStub } from "../../../../../stories/stubs";
4-
import { mobileViewport } from "../../../../../stories/utils";
4+
import { projectStub, teamStub } from "../../../../../stories/stubs";
55
import { ProjectGeneralSettingsPageUI } from "./ProjectGeneralSettingsPage";
66

77
const meta = {
8-
title: "Project/Settings/General",
8+
title: "Project/Settings",
99
component: Story,
1010
parameters: {
1111
nextjs: {
@@ -17,32 +17,56 @@ const meta = {
1717
export default meta;
1818
type Story = StoryObj<typeof meta>;
1919

20-
export const Desktop: Story = {
21-
args: {},
20+
export const OwnerAccount: Story = {
21+
args: {
22+
isOwnerAccount: true,
23+
},
2224
};
2325

24-
export const Mobile: Story = {
25-
args: {},
26-
parameters: {
27-
viewport: mobileViewport("iphone14"),
26+
export const MemberAccount: Story = {
27+
args: {
28+
isOwnerAccount: false,
2829
},
2930
};
3031

31-
function Story() {
32+
function Story(props: {
33+
isOwnerAccount: boolean;
34+
}) {
35+
const currentTeam = teamStub("currentTeam", "free");
3236
return (
3337
<div className="mx-auto w-full max-w-[1100px] px-4 py-6">
3438
<ProjectGeneralSettingsPageUI
39+
isOwnerAccount={props.isOwnerAccount}
40+
transferProject={async (newTeam) => {
41+
await new Promise((resolve) => setTimeout(resolve, 1000));
42+
console.log("transferProject", newTeam);
43+
}}
44+
client={getThirdwebClient()}
45+
teamsWithRole={[
46+
{
47+
role: props.isOwnerAccount ? "OWNER" : "MEMBER",
48+
team: currentTeam,
49+
},
50+
{
51+
role: "OWNER",
52+
team: teamStub("bar", "growth"),
53+
},
54+
{
55+
role: "MEMBER",
56+
team: teamStub("baz", "starter"),
57+
},
58+
]}
3559
updateProject={async (params) => {
3660
await new Promise((resolve) => setTimeout(resolve, 1000));
3761
console.log("updateProject", params);
38-
return projectStub("foo", "team-1");
62+
return projectStub("foo", "currentTeam");
3963
}}
4064
deleteProject={async () => {
4165
await new Promise((resolve) => setTimeout(resolve, 1000));
4266
console.log("deleteProject");
4367
}}
44-
project={projectStub("foo", "team-1")}
45-
teamSlug="foo"
68+
project={projectStub("foo", currentTeam.id)}
69+
teamSlug={currentTeam.slug}
4670
onKeyUpdated={undefined}
4771
rotateSecretKey={async () => {
4872
await new Promise((resolve) => setTimeout(resolve, 1000));

0 commit comments

Comments
 (0)