Skip to content

Commit 3c6668b

Browse files
committed
[TOOL-4833] Add Manage Contract button in new public contract pages (#7434)
<!-- ## 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 handling of `projectSlug` and `teamSlug` properties across various components in the application, improving navigation and data management related to teams and projects. ### Detailed summary - Added `projectSlug` and `teamSlug` props in several components including `cards.tsx`, `DeployedContractsPage.tsx`, and `layout.tsx`. - Updated links to include `teamSlug` and `projectSlug` for better navigation. - Introduced new API function `getContractImportedProjects` to fetch projects with imported contracts. - Created `TeamSelectorCard` and `ProjectAndTeamSelectorCard` components for selecting teams and projects. - Improved UI elements such as buttons and tooltips in `ContractHeaderUI` and `ImportModal`. - Enhanced type definitions for better type safety and clarity in components. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a team and project selector UI, enabling users to easily select teams or projects from visual cards. - Added a project and team selector for associating contracts, allowing users to view, select, or import contracts into projects across teams. - Enhanced contract management by providing direct navigation to contract pages based on project selection. - Improved contract view links to include team and project information in URLs for clearer navigation. - Added functionality to fetch projects that have imported specific contracts within teams. - **UI Improvements** - Updated button styles with rounded edges for a polished look. - Added copy address and manage contract buttons with icons and tooltips for enhanced usability. - **Storybook/Documentation** - Added Storybook stories to visually test and demonstrate the new team and project selector components under various scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 6aa2765 commit 3c6668b

File tree

16 files changed

+728
-70
lines changed

16 files changed

+728
-70
lines changed

apps/dashboard/src/@/api/getProjectContracts.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import "server-only";
2+
import { getAddress } from "thirdweb";
23
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
34

45
export type ProjectContract = {
@@ -43,3 +44,43 @@ export async function getProjectContracts(options: {
4344

4445
return data.result;
4546
}
47+
48+
export type PartialProject = {
49+
id: string;
50+
name: string;
51+
slug: string;
52+
image: string | null;
53+
};
54+
55+
/**
56+
* get a list of projects within a team that have a given contract imported
57+
*/
58+
export async function getContractImportedProjects(options: {
59+
teamId: string;
60+
authToken: string;
61+
chainId: number;
62+
contractAddress: string;
63+
}) {
64+
const url = new URL(
65+
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamId}/projects/contracts/lookup?chainId=${options.chainId}&contractAddress=${getAddress(options.contractAddress)}`,
66+
);
67+
68+
const res = await fetch(url, {
69+
headers: {
70+
Authorization: `Bearer ${options.authToken}`,
71+
},
72+
});
73+
74+
if (!res.ok) {
75+
const errorMessage = await res.text();
76+
console.error("Error fetching: /projects/contracts/lookup");
77+
console.error(errorMessage);
78+
return [];
79+
}
80+
81+
const data = (await res.json()) as {
82+
result: PartialProject[];
83+
};
84+
85+
return data.result;
86+
}

apps/dashboard/src/@/components/contracts/import-contract/modal.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ type ImportModalProps = {
3737
onClose: () => void;
3838
teamId: string;
3939
projectId: string;
40+
projectSlug: string;
41+
teamSlug: string;
4042
client: ThirdwebClient;
4143
type: "contract" | "asset";
4244
onSuccess?: () => void;
@@ -69,7 +71,9 @@ export const ImportModal: React.FC<ImportModalProps> = (props) => {
6971
client={props.client}
7072
onSuccess={props.onSuccess}
7173
projectId={props.projectId}
74+
projectSlug={props.projectSlug}
7275
teamId={props.teamId}
76+
teamSlug={props.teamSlug}
7377
type={props.type}
7478
/>
7579
</DialogContent>
@@ -96,6 +100,8 @@ const importFormSchema = z.object({
96100
function ImportForm(props: {
97101
teamId: string;
98102
projectId: string;
103+
teamSlug: string;
104+
projectSlug: string;
99105
client: ThirdwebClient;
100106
type: "contract" | "asset";
101107
onSuccess?: () => void;
@@ -216,7 +222,7 @@ function ImportForm(props: {
216222
addContractToProject.data?.result ? (
217223
<Button asChild className="gap-2">
218224
<Link
219-
href={`/${chainSlug}/${addContractToProject.data.result.contractAddress}`}
225+
href={`/team/${props.teamSlug}/${props.projectSlug}/contract/${chainSlug}/${addContractToProject.data.result.contractAddress}`}
220226
rel="noopener noreferrer"
221227
target="_blank"
222228
>

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_layout/primary-dashboard-button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export const PrimaryDashboardButton: React.FC<AddToDashboardCardProps> = ({
8383
// if user is on a project page
8484
if (projectMeta) {
8585
return (
86-
<Button asChild variant="default">
86+
<Button asChild className="rounded-full" variant="default">
8787
<Link
8888
className="gap-2"
8989
href={`/${contractInfo.chainSlug}/${contractAddress}`}

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/public-pages/erc20/_components/ContractHeader.tsx

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ExternalLinkIcon, GlobeIcon } from "lucide-react";
1+
import { ExternalLinkIcon, GlobeIcon, Settings2Icon } from "lucide-react";
22
import Link from "next/link";
33
import { useMemo } from "react";
44
import type { ThirdwebContract } from "thirdweb";
@@ -111,6 +111,13 @@ export function ContractHeaderUI(props: {
111111
)}
112112
</Link>
113113

114+
<CopyAddressButton
115+
address={props.clientContract.address}
116+
className="rounded-full bg-card w-[30px] h-[30px] p-0 [&>span]:hidden [&>svg]:text-foreground"
117+
copyIconPosition="left"
118+
variant="outline"
119+
/>
120+
114121
{socialUrls
115122
.toSorted((a, b) => {
116123
const aIcon = platformToIcons[a.name.toLowerCase()];
@@ -140,12 +147,29 @@ export function ContractHeaderUI(props: {
140147

141148
{/* bottom row */}
142149
<div className="flex flex-row flex-wrap items-center gap-2">
143-
<CopyAddressButton
144-
address={props.clientContract.address}
145-
className="rounded-full bg-card px-2.5 py-1.5 text-xs"
146-
copyIconPosition="left"
147-
variant="outline"
148-
/>
150+
<ToolTipLabel
151+
contentClassName="max-w-[300px]"
152+
label={
153+
<>
154+
View this contract in thirdweb dashboard to view contract
155+
management interface
156+
</>
157+
}
158+
>
159+
<Button
160+
asChild
161+
className="rounded-full bg-card gap-1.5 text-xs py-1.5 px-2.5 h-auto"
162+
size="sm"
163+
variant="outline"
164+
>
165+
<Link
166+
href={`/team/~/~/contract/${props.chainMetadata.slug}/${props.clientContract.address}`}
167+
>
168+
<Settings2Icon className="size-3.5 text-muted-foreground" />
169+
Manage Contract
170+
</Link>
171+
</Button>
172+
</ToolTipLabel>
149173

150174
{explorersToShow?.map((validBlockExplorer) => (
151175
<BadgeLink

apps/dashboard/src/app/(app)/account/contracts/DeployedContractsPageHeader.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { Button } from "@/components/ui/button";
1010
export function DeployedContractsPageHeader(props: {
1111
teamId: string;
1212
projectId: string;
13+
projectSlug: string;
14+
teamSlug: string;
1315
client: ThirdwebClient;
1416
}) {
1517
const [importModalOpen, setImportModalOpen] = useState(false);
@@ -23,7 +25,9 @@ export function DeployedContractsPageHeader(props: {
2325
setImportModalOpen(false);
2426
}}
2527
projectId={props.projectId}
28+
projectSlug={props.projectSlug}
2629
teamId={props.teamId}
30+
teamSlug={props.teamSlug}
2731
type="contract"
2832
/>
2933

apps/dashboard/src/app/(app)/account/contracts/_components/DeployViaCLIOrImportCard.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { Button } from "@/components/ui/button";
1010
export function DeployViaCLIOrImportCard(props: {
1111
teamId: string;
1212
projectId: string;
13+
projectSlug: string;
14+
teamSlug: string;
1315
client: ThirdwebClient;
1416
}) {
1517
const [importModalOpen, setImportModalOpen] = useState(false);
@@ -23,7 +25,9 @@ export function DeployViaCLIOrImportCard(props: {
2325
setImportModalOpen(false);
2426
}}
2527
projectId={props.projectId}
28+
projectSlug={props.projectSlug}
2629
teamId={props.teamId}
30+
teamSlug={props.teamSlug}
2731
type="contract"
2832
/>
2933

apps/dashboard/src/app/(app)/account/contracts/_components/DeployedContractsPage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ export function DeployedContractsPage(props: {
2323
<DeployViaCLIOrImportCard
2424
client={props.client}
2525
projectId={props.projectId}
26+
projectSlug={props.projectSlug}
2627
teamId={props.teamId}
28+
teamSlug={props.teamSlug}
2729
/>
2830
</div>
2931
);

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/contracts/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ export default async function Layout(props: {
4343
<DeployedContractsPageHeader
4444
client={client}
4545
projectId={project.id}
46+
projectSlug={params.project_slug}
4647
teamId={team.id}
48+
teamSlug={params.team_slug}
4749
/>
4850
<TabPathLinks
4951
links={[

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/cards.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ export function Cards(props: {
3333
reportAssetImportSuccessful();
3434
}}
3535
projectId={props.projectId}
36+
projectSlug={props.projectSlug}
3637
teamId={props.teamId}
38+
teamSlug={props.teamSlug}
3739
type="asset"
3840
/>
3941

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type { Meta, StoryObj } from "@storybook/nextjs";
2+
import { teamStub } from "@/storybook/stubs";
3+
import { storybookThirdwebClient } from "@/storybook/utils";
4+
import { TeamSelectorCard } from "./team-selector";
5+
6+
const meta: Meta<typeof TeamSelectorCard> = {
7+
component: TeamSelectorCard,
8+
decorators: [
9+
(Story) => (
10+
<div className="py-20 flex justify-center items-center">
11+
<Story />
12+
</div>
13+
),
14+
],
15+
parameters: {
16+
nextjs: {
17+
appDirectory: true,
18+
},
19+
},
20+
title: "selectors/TeamSelectorCard",
21+
};
22+
23+
export default meta;
24+
type Story = StoryObj<typeof TeamSelectorCard>;
25+
26+
export const TwoTeams: Story = {
27+
args: {
28+
client: storybookThirdwebClient,
29+
paths: undefined,
30+
searchParams: "",
31+
teams: [teamStub("1", "free"), teamStub("2", "starter")],
32+
},
33+
};
34+
35+
export const FiveTeams: Story = {
36+
args: {
37+
client: storybookThirdwebClient,
38+
paths: undefined,
39+
searchParams: "",
40+
teams: [
41+
teamStub("1", "free"),
42+
teamStub("2", "starter"),
43+
teamStub("3", "growth"),
44+
teamStub("4", "pro"),
45+
teamStub("5", "scale"),
46+
],
47+
},
48+
};
49+
50+
export const WithSearchParams: Story = {
51+
args: {
52+
client: storybookThirdwebClient,
53+
paths: undefined,
54+
searchParams: "tab=overview&section=analytics",
55+
teams: [
56+
teamStub("1", "free"),
57+
teamStub("2", "starter"),
58+
teamStub("3", "growth"),
59+
],
60+
},
61+
};
62+
63+
export const WithPaths: Story = {
64+
args: {
65+
client: storybookThirdwebClient,
66+
paths: ["projects", "123", "settings"],
67+
searchParams: "",
68+
teams: [teamStub("1", "free"), teamStub("2", "starter")],
69+
},
70+
};
71+
72+
export const WithPathsAndSearchParams: Story = {
73+
args: {
74+
client: storybookThirdwebClient,
75+
paths: ["projects", "123", "settings"],
76+
searchParams: "tab=overview&section=analytics",
77+
teams: [
78+
teamStub("1", "free"),
79+
teamStub("2", "starter"),
80+
teamStub("3", "growth"),
81+
teamStub("4", "pro"),
82+
],
83+
},
84+
};

0 commit comments

Comments
 (0)