Skip to content

Commit 14381b4

Browse files
authored
Saved Smart Contract IDs (#1392)
* Saved Smart Contract IDs * Adjust item layout * Wrap buttons
1 parent f7445a0 commit 14381b4

File tree

12 files changed

+424
-20
lines changed

12 files changed

+424
-20
lines changed

src/app/(sidebar)/smart-contracts/contract-explorer/page.tsx

Lines changed: 77 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,33 @@ import { useStore } from "@/store/useStore";
88
import { useSEContractInfo } from "@/query/external/useSEContractInfo";
99
import { useWasmGitHubAttestation } from "@/query/useWasmGitHubAttestation";
1010
import { validate } from "@/validate";
11+
1112
import { getNetworkHeaders } from "@/helpers/getNetworkHeaders";
13+
import { localStorageSavedContracts } from "@/helpers/localStorageSavedContracts";
14+
import { buildContractExplorerHref } from "@/helpers/buildContractExplorerHref";
15+
import { delayedAction } from "@/helpers/delayedAction";
1216

1317
import { Box } from "@/components/layout/Box";
1418
import { PageCard } from "@/components/layout/PageCard";
1519
import { MessageField } from "@/components/MessageField";
1620
import { TabView } from "@/components/TabView";
1721
import { SwitchNetworkButtons } from "@/components/SwitchNetworkButtons";
1822
import { PoweredByStellarExpert } from "@/components/PoweredByStellarExpert";
19-
import { ContractInfo } from "./components/ContractInfo";
20-
import { InvokeContract } from "./components/InvokeContract";
23+
import { SaveToLocalStorageModal } from "@/components/SaveToLocalStorageModal";
2124

2225
import { trackEvent, TrackingEvent } from "@/metrics/tracking";
2326

27+
import { ContractInfo } from "./components/ContractInfo";
28+
import { InvokeContract } from "./components/InvokeContract";
29+
2430
export default function ContractExplorer() {
25-
const { network, smartContracts } = useStore();
31+
const { network, smartContracts, savedContractId, clearSavedContractId } =
32+
useStore();
33+
2634
const [contractActiveTab, setContractActiveTab] = useState("contract-info");
2735
const [contractIdInput, setContractIdInput] = useState("");
2836
const [contractIdInputError, setContractIdInputError] = useState("");
37+
const [isSaveModalVisible, setIsSaveModalVisible] = useState(false);
2938

3039
const {
3140
data: contractInfoData,
@@ -62,9 +71,24 @@ export default function ContractExplorer() {
6271
isWasmFetching;
6372

6473
useEffect(() => {
74+
// Pre-fill on page refresh and initial load
6575
if (smartContracts.explorer.contractId) {
6676
setContractIdInput(smartContracts.explorer.contractId);
6777
}
78+
79+
// Pre-fill only on initial load when navigating from the Saved Smart
80+
// Contract IDs view.
81+
if (savedContractId) {
82+
setContractIdInput(savedContractId);
83+
84+
// Remove temporary savedContractId from URL
85+
delayedAction({
86+
action: () => {
87+
clearSavedContractId();
88+
},
89+
delay: 200,
90+
});
91+
}
6892
// On page load only
6993
// eslint-disable-next-line react-hooks/exhaustive-deps
7094
}, []);
@@ -102,7 +126,7 @@ export default function ContractExplorer() {
102126
const renderButtons = () => {
103127
if (isCurrentNetworkSupported) {
104128
return (
105-
<Box gap="sm" direction="row">
129+
<Box gap="sm" direction="row" wrap="wrap">
106130
<Button
107131
size="md"
108132
variant="secondary"
@@ -115,22 +139,37 @@ export default function ContractExplorer() {
115139

116140
<>
117141
{contractIdInput ? (
118-
<Button
119-
size="md"
120-
variant="error"
121-
icon={<Icon.RefreshCw01 />}
122-
onClick={() => {
123-
resetFetchContractInfo();
124-
setContractIdInput("");
125-
126-
trackEvent(
127-
TrackingEvent.SMART_CONTRACTS_EXPLORER_CLEAR_CONTRACT,
128-
);
129-
}}
130-
disabled={isLoading}
131-
>
132-
Clear
133-
</Button>
142+
<>
143+
<Button
144+
disabled={isLoadContractDisabled || isLoading}
145+
size="md"
146+
variant="tertiary"
147+
icon={<Icon.Save01 />}
148+
onClick={(e) => {
149+
e.preventDefault();
150+
setIsSaveModalVisible(true);
151+
}}
152+
>
153+
Save Contract ID
154+
</Button>
155+
156+
<Button
157+
size="md"
158+
variant="error"
159+
icon={<Icon.RefreshCw01 />}
160+
onClick={() => {
161+
resetFetchContractInfo();
162+
setContractIdInput("");
163+
164+
trackEvent(
165+
TrackingEvent.SMART_CONTRACTS_EXPLORER_CLEAR_CONTRACT,
166+
);
167+
}}
168+
disabled={isLoading}
169+
>
170+
Clear
171+
</Button>
172+
</>
134173
) : null}
135174
</>
136175
</Box>
@@ -242,6 +281,24 @@ export default function ContractExplorer() {
242281
<PoweredByStellarExpert />
243282
</>
244283
</>
284+
285+
<SaveToLocalStorageModal
286+
type="save"
287+
itemTitle="Smart Contract ID"
288+
itemProps={{
289+
contractId: contractIdInput,
290+
shareableUrl: `${window.location.origin}${buildContractExplorerHref(contractIdInput)}`,
291+
}}
292+
allSavedItems={localStorageSavedContracts.get()}
293+
isVisible={isSaveModalVisible}
294+
onClose={() => {
295+
setIsSaveModalVisible(false);
296+
}}
297+
onUpdate={(updatedItems) => {
298+
localStorageSavedContracts.set(updatedItems);
299+
trackEvent(TrackingEvent.SMART_CONTRACTS_EXPLORER_SAVE);
300+
}}
301+
/>
245302
</Box>
246303
);
247304
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
"use client";
2+
3+
import { useCallback, useEffect, useState } from "react";
4+
import { Input, Icon, Button } from "@stellar/design-system";
5+
import { useRouter } from "next/navigation";
6+
7+
import { Box } from "@/components/layout/Box";
8+
import { InputSideElement } from "@/components/InputSideElement";
9+
import { SavedItemTimestampAndDelete } from "@/components/SavedItemTimestampAndDelete";
10+
import { PageCard } from "@/components/layout/PageCard";
11+
import { SaveToLocalStorageModal } from "@/components/SaveToLocalStorageModal";
12+
import { ShareUrlButton } from "@/components/ShareUrlButton";
13+
14+
import { localStorageSavedContracts } from "@/helpers/localStorageSavedContracts";
15+
import { arrayItem } from "@/helpers/arrayItem";
16+
import { delayedAction } from "@/helpers/delayedAction";
17+
18+
import { Routes } from "@/constants/routes";
19+
import { useStore } from "@/store/useStore";
20+
import { trackEvent, TrackingEvent } from "@/metrics/tracking";
21+
22+
import { SavedContract } from "@/types/types";
23+
24+
export default function SavedSmartContracts() {
25+
const { network } = useStore();
26+
27+
const [savedContracts, setSavedContracts] = useState<SavedContract[]>([]);
28+
const [currentContractTimestamp, setCurrentContractTimestamp] = useState<
29+
number | undefined
30+
>();
31+
32+
const updateSavedContracts = useCallback(() => {
33+
const contracts = localStorageSavedContracts
34+
.get()
35+
.filter((s) => s.network.id === network.id);
36+
setSavedContracts(contracts);
37+
}, [network.id]);
38+
39+
useEffect(() => {
40+
updateSavedContracts();
41+
}, [updateSavedContracts]);
42+
43+
return (
44+
<Box gap="md">
45+
<PageCard heading="Saved Smart Contract IDs">
46+
<Box gap="md">
47+
<>
48+
{savedContracts.length === 0
49+
? `There are no saved smart contract IDs on ${network.label} network.`
50+
: savedContracts.map((c) => (
51+
<SavedContractItem
52+
key={`saved-contract-${c.timestamp}`}
53+
contract={c}
54+
setCurrentContractTimestamp={setCurrentContractTimestamp}
55+
onDelete={(contract) => {
56+
const savedContracts = localStorageSavedContracts.get();
57+
const indexToUpdate = savedContracts.findIndex(
58+
(c) => c.timestamp === contract.timestamp,
59+
);
60+
61+
if (indexToUpdate >= 0) {
62+
const updatedList = arrayItem.delete(
63+
savedContracts,
64+
indexToUpdate,
65+
);
66+
67+
localStorageSavedContracts.set(updatedList);
68+
updateSavedContracts();
69+
}
70+
}}
71+
/>
72+
))}
73+
</>
74+
</Box>
75+
</PageCard>
76+
77+
<SaveToLocalStorageModal
78+
type="editName"
79+
itemTitle="Smart Contract ID"
80+
itemTimestamp={currentContractTimestamp}
81+
allSavedItems={localStorageSavedContracts.get()}
82+
isVisible={currentContractTimestamp !== undefined}
83+
onClose={(isUpdate?: boolean) => {
84+
setCurrentContractTimestamp(undefined);
85+
86+
if (isUpdate) {
87+
updateSavedContracts();
88+
}
89+
}}
90+
onUpdate={(updatedItems) => {
91+
localStorageSavedContracts.set(updatedItems);
92+
}}
93+
/>
94+
</Box>
95+
);
96+
}
97+
98+
const SavedContractItem = ({
99+
contract,
100+
setCurrentContractTimestamp,
101+
onDelete,
102+
}: {
103+
contract: SavedContract;
104+
setCurrentContractTimestamp: (timestamp: number) => void;
105+
onDelete: (contract: SavedContract) => void;
106+
}) => {
107+
const { setSavedContractId } = useStore();
108+
const router = useRouter();
109+
110+
return (
111+
<Box
112+
gap="sm"
113+
addlClassName="PageBody__content SavedContractItem"
114+
data-testid="saved-contract-item"
115+
>
116+
<Input
117+
id={`saved-contract-${contract.timestamp}-name`}
118+
data-testid="saved-contract-name"
119+
fieldSize="md"
120+
value={contract.name}
121+
readOnly
122+
leftElement="Name"
123+
rightElement={
124+
<InputSideElement
125+
variant="button"
126+
placement="right"
127+
onClick={() => {
128+
setCurrentContractTimestamp(contract.timestamp);
129+
}}
130+
icon={<Icon.Edit05 />}
131+
/>
132+
}
133+
/>
134+
135+
<Input
136+
id={`saved-contract-${contract.timestamp}-id`}
137+
data-testid="saved-contract-id"
138+
fieldSize="md"
139+
value={contract.contractId}
140+
readOnly
141+
leftElement="Contract ID"
142+
copyButton={{ position: "right" }}
143+
/>
144+
145+
<Box
146+
gap="lg"
147+
direction="row"
148+
align="center"
149+
justify="space-between"
150+
addlClassName="Endpoints__urlBar__footer"
151+
>
152+
<Box gap="sm" direction="row">
153+
<Button
154+
size="md"
155+
variant="tertiary"
156+
type="button"
157+
onClick={() => {
158+
// Set a temporary param in URL that we will remove when the route
159+
// loads.
160+
setSavedContractId(contract.contractId);
161+
trackEvent(TrackingEvent.SMART_CONTRACTS_SAVED_VIEW_IN_EXPLORER);
162+
163+
delayedAction({
164+
action: () => {
165+
router.push(Routes.SMART_CONTRACTS_CONTRACT_EXPLORER);
166+
},
167+
delay: 300,
168+
});
169+
}}
170+
>
171+
View in Contract Explorer
172+
</Button>
173+
174+
<ShareUrlButton shareableUrl={contract.shareableUrl} />
175+
</Box>
176+
177+
<SavedItemTimestampAndDelete
178+
timestamp={contract.timestamp}
179+
onDelete={() => onDelete(contract)}
180+
/>
181+
</Box>
182+
</Box>
183+
);
184+
};

src/app/(sidebar)/smart-contracts/styles.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,14 @@
181181
}
182182
}
183183

184+
// Saved Smart Contract IDs
185+
.SavedContractItem {
186+
.Input__side-element--left {
187+
width: pxToRem(76px);
188+
}
189+
}
190+
191+
184192
.NoContractLoaded {
185193
background-color: var(--sds-clr-gray-03);
186194
border-radius: pxToRem(8px);

src/constants/navItems.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,16 @@ export const XDR_NAV_ITEMS = [
119119
];
120120

121121
export const SMART_CONTRACTS_NAV_ITEMS = [
122+
{
123+
navItems: [
124+
{
125+
route: Routes.SMART_CONTRACTS_SAVED,
126+
label: "Saved Smart Contract IDs",
127+
icon: <Icon.Save03 />,
128+
},
129+
],
130+
hasBottomDivider: true,
131+
},
122132
{
123133
instruction: "Smart Contract Tools",
124134
navItems: [

src/constants/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export enum Routes {
8686
SMART_CONTRACTS = "/smart-contracts",
8787
SMART_CONTRACTS_CONTRACT_EXPLORER = "/smart-contracts/contract-explorer",
8888
SMART_CONTRACTS_CONTRACT_LIST = "/smart-contracts/contract-list",
89+
SMART_CONTRACTS_SAVED = "/smart-contracts/saved",
8990
// Blockchain Explorer
9091
BLOCKCHAIN_EXPLORER = "/explorer",
9192
}

src/constants/settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const LOCAL_STORAGE_SAVED_RPC_METHODS =
1212
export const LOCAL_STORAGE_SAVED_TRANSACTIONS =
1313
"stellar_lab_saved_transactions";
1414
export const LOCAL_STORAGE_SAVED_KEYPAIRS = "stellar_lab_saved_keypairs";
15+
export const LOCAL_STORAGE_SAVED_CONTRACTS = "stellar_lab_saved_contract_ids";
1516
export const LOCAL_STORAGE_SAVED_THEME = "stellarTheme:Laboratory";
1617

1718
export const XDR_TYPE_TRANSACTION_ENVELOPE = "TransactionEnvelope";

0 commit comments

Comments
 (0)