diff --git a/packages/round-manager/.env.sample b/packages/round-manager/.env.sample index 56c0355416..cb6e96433a 100644 --- a/packages/round-manager/.env.sample +++ b/packages/round-manager/.env.sample @@ -13,4 +13,7 @@ REACT_APP_INFURA_ID=###################### REACT_APP_DATADOG_APPLICATION_ID=###################### REACT_APP_DATADOG_CLIENT_TOKEN=###################### REACT_APP_DATADOG_SERVICE=###################### -REACT_APP_DATADOG_SITE=###################### \ No newline at end of file +REACT_APP_DATADOG_SITE=###################### + +# Grant Explorer +REACT_APP_GRANT_EXPLORER=https://gegitcoin.on.fleek.co/ \ No newline at end of file diff --git a/packages/round-manager/src/assets/grantexplorer-icon.svg b/packages/round-manager/src/assets/grantexplorer-icon.svg new file mode 100644 index 0000000000..3cab477b00 --- /dev/null +++ b/packages/round-manager/src/assets/grantexplorer-icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/round-manager/src/context/application/BulkUpdateGrantApplicationContext.tsx b/packages/round-manager/src/context/application/BulkUpdateGrantApplicationContext.tsx index 95d8f334fe..4162c0c69d 100644 --- a/packages/round-manager/src/context/application/BulkUpdateGrantApplicationContext.tsx +++ b/packages/round-manager/src/context/application/BulkUpdateGrantApplicationContext.tsx @@ -3,7 +3,13 @@ import { ProgressStatus, Web3Instance, } from "../../features/api/types"; -import React, { createContext, useContext, useReducer } from "react"; +import React, { + createContext, + ReactNode, + SetStateAction, + useContext, + useState, +} from "react"; import { updateApplicationList, updateRoundContract, @@ -14,54 +20,85 @@ import { Signer } from "@ethersproject/abstract-signer"; import { useWallet } from "../../features/common/Auth"; export interface BulkUpdateGrantApplicationState { + roundId: string; + setRoundId: React.Dispatch>; + applications: GrantApplication[]; + setApplications: React.Dispatch>; IPFSCurrentStatus: ProgressStatus; + setIPFSCurrentStatus: React.Dispatch>; contractUpdatingStatus: ProgressStatus; + setContractUpdatingStatus: React.Dispatch>; indexingStatus: ProgressStatus; + setIndexingStatus: React.Dispatch>; } -export type BulkUpdateGrantApplicationParams = { - roundId: string; - applications: GrantApplication[]; -}; - export const initialBulkUpdateGrantApplicationState: BulkUpdateGrantApplicationState = { + roundId: "", + setRoundId: () => { + /**/ + }, + applications: [], + setApplications: () => { + /**/ + }, IPFSCurrentStatus: ProgressStatus.NOT_STARTED, + setIPFSCurrentStatus: () => { + /**/ + }, contractUpdatingStatus: ProgressStatus.NOT_STARTED, + setContractUpdatingStatus: () => { + /**/ + }, indexingStatus: ProgressStatus.NOT_STARTED, + setIndexingStatus: () => { + /**/ + }, }; -type Dispatch = (action: Action) => void; - -enum ActionType { - SET_STORING_STATUS = "SET_STORING_STATUS", - SET_CONTRACT_UPDATING_STATUS = "SET_CONTRACT_UPDATING_STATUS", - SET_INDEXING_STATUS = "SET_INDEXING_STATUS", - RESET_TO_INITIAL_STATE = "RESET_TO_INITIAL_STATE", -} - -interface Action { - type: ActionType; - payload?: any; -} +export type BulkUpdateGrantApplicationParams = { + roundId: string; + applications: GrantApplication[]; +}; -export const BulkUpdateGrantApplicationContext = createContext< - { state: BulkUpdateGrantApplicationState; dispatch: Dispatch } | undefined ->(undefined); +export const BulkUpdateGrantApplicationContext = + createContext( + initialBulkUpdateGrantApplicationState + ); export const BulkUpdateGrantApplicationProvider = ({ children, }: { - children: React.ReactNode; + children: ReactNode; }) => { - const [state, dispatch] = useReducer( - bulkUpdateGrantApplicationReducer, - initialBulkUpdateGrantApplicationState + const [roundId, setRoundId] = useState( + initialBulkUpdateGrantApplicationState.roundId + ); + const [applications, setApplications] = useState( + initialBulkUpdateGrantApplicationState.applications + ); + const [IPFSCurrentStatus, setIPFSCurrentStatus] = useState( + initialBulkUpdateGrantApplicationState.IPFSCurrentStatus + ); + const [contractUpdatingStatus, setContractUpdatingStatus] = useState( + initialBulkUpdateGrantApplicationState.contractUpdatingStatus + ); + + const [indexingStatus, setIndexingStatus] = useState( + initialBulkUpdateGrantApplicationState.indexingStatus ); - const providerProps = { - state, - dispatch, + const providerProps: BulkUpdateGrantApplicationState = { + roundId, + setRoundId, + applications, + setApplications, + IPFSCurrentStatus, + setIPFSCurrentStatus, + contractUpdatingStatus, + setContractUpdatingStatus, + indexingStatus, + setIndexingStatus, }; return ( @@ -71,59 +108,49 @@ export const BulkUpdateGrantApplicationProvider = ({ ); }; -const bulkUpdateGrantApplicationReducer = ( - state: BulkUpdateGrantApplicationState, - action: Action -) => { - switch (action.type) { - case ActionType.SET_STORING_STATUS: - return { ...state, IPFSCurrentStatus: action.payload }; - case ActionType.SET_CONTRACT_UPDATING_STATUS: - return { - ...state, - contractUpdatingStatus: action.payload, - }; - case ActionType.SET_INDEXING_STATUS: - return { - ...state, - indexingStatus: action.payload, - }; - case ActionType.RESET_TO_INITIAL_STATE: { - return initialBulkUpdateGrantApplicationState; - } - } - return state; -}; - -interface _bulkUpdateGrantApplicationParams { - dispatch: Dispatch; +interface bulkUpdateGrantApplicationParams { signer: Signer; - params: BulkUpdateGrantApplicationParams; + context: any; + roundId: string; + applications: GrantApplication[]; +} + +function resetToInitialState(context: any) { + const { setIPFSCurrentStatus, setContractUpdatingStatus, setIndexingStatus } = + context; + + setIPFSCurrentStatus( + initialBulkUpdateGrantApplicationState.IPFSCurrentStatus + ); + setContractUpdatingStatus( + initialBulkUpdateGrantApplicationState.contractUpdatingStatus + ); + setIndexingStatus(initialBulkUpdateGrantApplicationState.indexingStatus); } async function _bulkUpdateGrantApplication({ - dispatch, signer, - params, -}: _bulkUpdateGrantApplicationParams) { - const { roundId, applications } = params; - dispatch({ - type: ActionType.RESET_TO_INITIAL_STATE, - }); + context, + roundId, + applications, +}: bulkUpdateGrantApplicationParams) { + resetToInitialState(context); + try { const newProjectsMetaPtr = await storeDocument({ - dispatch, signer, roundId, applications, + context, }); const transactionBlockNumber = await updateContract({ - dispatch, signer, roundId, newProjectsMetaPtr, + context, }); - await waitForSubgraphToUpdate(dispatch, signer, transactionBlockNumber); + + await waitForSubgraphToUpdate(signer, transactionBlockNumber, context); } catch (error) { datadogLogs.logger.error(`error: _bulkUpdateGrantApplication - ${error}`); console.error("Error while bulk updating applications: ", error); @@ -132,7 +159,6 @@ async function _bulkUpdateGrantApplication({ export const useBulkUpdateGrantApplications = () => { const context = useContext(BulkUpdateGrantApplicationContext); - if (context === undefined) { throw new Error( "useBulkUpdateGrantApplication must be used within a BulkUpdateGrantApplicationProvider" @@ -141,82 +167,75 @@ export const useBulkUpdateGrantApplications = () => { const { signer } = useWallet(); - const bulkUpdateGrantApplications = async ( + const handleBulkUpdateGrantApplications = async ( params: BulkUpdateGrantApplicationParams ) => { return _bulkUpdateGrantApplication({ - dispatch: context.dispatch, + ...params, signer, - params, + context, }); }; return { - bulkUpdateGrantApplications, - IPFSCurrentStatus: context.state.IPFSCurrentStatus, - contractUpdatingStatus: context.state.contractUpdatingStatus, - indexingStatus: context.state.indexingStatus, + bulkUpdateGrantApplications: handleBulkUpdateGrantApplications, + IPFSCurrentStatus: context.IPFSCurrentStatus, + contractUpdatingStatus: context.contractUpdatingStatus, + indexingStatus: context.indexingStatus, }; }; interface StoreDocumentParams { - dispatch: Dispatch; signer: Signer; roundId: string; applications: GrantApplication[]; + context: any; } const storeDocument = async ({ - dispatch, signer, roundId, applications, + context, }: StoreDocumentParams): Promise => { + const { setIPFSCurrentStatus } = context; try { - dispatch({ - type: ActionType.SET_STORING_STATUS, - payload: ProgressStatus.IN_PROGRESS, - }); + setIPFSCurrentStatus(ProgressStatus.IN_PROGRESS); + const chainId = await signer.getChainId(); const ipfsHash = await updateApplicationList( applications, roundId, chainId ); - dispatch({ - type: ActionType.SET_STORING_STATUS, - payload: ProgressStatus.IS_SUCCESS, - }); + + setIPFSCurrentStatus(ProgressStatus.IS_SUCCESS); return ipfsHash; } catch (error) { datadogLogs.logger.error(`error: storeDocument - ${error}`); - dispatch({ - type: ActionType.SET_STORING_STATUS, - payload: ProgressStatus.IS_ERROR, - }); + setIPFSCurrentStatus(ProgressStatus.IS_ERROR); throw error; } }; interface UpdateContractParams { - dispatch: Dispatch; signer: Signer; roundId: string; newProjectsMetaPtr: string; + context: any; } const updateContract = async ({ - dispatch, signer, roundId, newProjectsMetaPtr, + context, }: UpdateContractParams): Promise => { + const { setContractUpdatingStatus } = context; + try { - dispatch({ - type: ActionType.SET_CONTRACT_UPDATING_STATUS, - payload: ProgressStatus.IN_PROGRESS, - }); + setContractUpdatingStatus(ProgressStatus.IN_PROGRESS); const { transactionBlockNumber } = await updateRoundContract( roundId, @@ -224,53 +243,40 @@ const updateContract = async ({ newProjectsMetaPtr ); - dispatch({ - type: ActionType.SET_CONTRACT_UPDATING_STATUS, - payload: ProgressStatus.IS_SUCCESS, - }); + setContractUpdatingStatus(ProgressStatus.IS_SUCCESS); return transactionBlockNumber; } catch (error) { datadogLogs.logger.error(`error: updateContract - ${error}`); - dispatch({ - type: ActionType.SET_CONTRACT_UPDATING_STATUS, - payload: ProgressStatus.IS_ERROR, - }); + setContractUpdatingStatus(ProgressStatus.IS_ERROR); throw error; } }; async function waitForSubgraphToUpdate( - dispatch: (action: Action) => void, signerOrProvider: Web3Instance["provider"], - transactionBlockNumber: number + transactionBlockNumber: number, + context: any ) { + const { setIndexingStatus } = context; + try { datadogLogs.logger.error( `waitForSubgraphToUpdate: txnBlockNumber - ${transactionBlockNumber}` ); - dispatch({ - type: ActionType.SET_INDEXING_STATUS, - payload: ProgressStatus.IN_PROGRESS, - }); + setIndexingStatus(ProgressStatus.IN_PROGRESS); const chainId = await signerOrProvider.getChainId(); await waitForSubgraphSyncTo(chainId, transactionBlockNumber); - dispatch({ - type: ActionType.SET_INDEXING_STATUS, - payload: ProgressStatus.IS_SUCCESS, - }); + setIndexingStatus(ProgressStatus.IS_SUCCESS); } catch (error) { datadogLogs.logger.error( `error: waitForSubgraphToUpdate - ${error}. Data - ${transactionBlockNumber}` ); - dispatch({ - type: ActionType.SET_INDEXING_STATUS, - payload: ProgressStatus.IS_ERROR, - }); + setIndexingStatus(ProgressStatus.IS_ERROR); throw error; } } diff --git a/packages/round-manager/src/context/application/__tests__/BulkUpdateGrantApplicationContext.test.tsx b/packages/round-manager/src/context/application/__tests__/BulkUpdateGrantApplicationContext.test.tsx index ef6c30cf4c..2978ac24da 100644 --- a/packages/round-manager/src/context/application/__tests__/BulkUpdateGrantApplicationContext.test.tsx +++ b/packages/round-manager/src/context/application/__tests__/BulkUpdateGrantApplicationContext.test.tsx @@ -10,6 +10,7 @@ import { updateApplicationList, } from "../../../features/api/application"; import { waitForSubgraphSyncTo } from "../../../features/api/subgraph"; +import { faker } from "@faker-js/faker"; jest.mock("../../../features/api/application"); jest.mock("../../../features/api/subgraph"); @@ -126,7 +127,7 @@ describe("", () => { }); it("sets indexing status to completed when subgraph is finished indexing", async () => { - const transactionBlockNumber = 10; + const transactionBlockNumber = faker.datatype.number(); (updateApplicationList as jest.Mock).mockResolvedValue("bafabcdef"); (updateRoundContract as jest.Mock).mockResolvedValue({ transactionBlockNumber, diff --git a/packages/round-manager/src/features/api/application.ts b/packages/round-manager/src/features/api/application.ts index 123f026ff9..0163940028 100644 --- a/packages/round-manager/src/features/api/application.ts +++ b/packages/round-manager/src/features/api/application.ts @@ -305,7 +305,6 @@ export const updateApplicationList = async ( let reviewedApplications: any[] = []; let foundEntry = false; - // fetch latest ipfs pointer to the list of application for the round const res = await graphql_fetch( ` query GetApplicationListPointer($roundId: String!) { diff --git a/packages/round-manager/src/features/round/ViewRoundPage.tsx b/packages/round-manager/src/features/round/ViewRoundPage.tsx index 424b9650eb..d6b1b599e5 100644 --- a/packages/round-manager/src/features/round/ViewRoundPage.tsx +++ b/packages/round-manager/src/features/round/ViewRoundPage.tsx @@ -21,12 +21,13 @@ import CopyToClipboardButton from "../common/CopyToClipboardButton"; import { useRoundById } from "../../context/round/RoundContext"; import { Spinner } from "../common/Spinner"; import { useApplicationByRoundId } from "../../context/application/ApplicationContext"; -import { ApplicationStatus, ProgressStatus } from "../api/types"; +import { ApplicationStatus, ProgressStatus, Round } from "../api/types"; +import { Button } from "../common/styles"; +import { ReactComponent as GrantExplorerLogo } from "../../assets/grantexplorer-icon.svg"; export default function ViewRoundPage() { datadogLogs.logger.info("====> Route: /round/:id"); datadogLogs.logger.info(`====> URL: ${window.location.href}`); - const { id } = useParams(); const { address, chain } = useWallet(); @@ -106,30 +107,26 @@ export default function ViewRoundPage() { {"Round Details"} -

- {round?.roundMetadata?.name || "Round Details"} -

-
-
- -

Applications:

-

- {formatDate(round?.applicationsStartTime) || "..."} - - - {formatDate(round?.applicationsEndTime) || "..."} -

-
- -
- -

Round:

-

- {formatDate(round?.roundStartTime) || "..."} - - - {formatDate(round?.roundEndTime) || "..."} -

+
+ +
+
+
+
+ +

@@ -243,3 +240,71 @@ export default function ViewRoundPage() { ); } + +type ViewGrantsExplorerButtonType = { + styles?: string; + iconStyle?: string; + chainId: string; + roundId: string | undefined; +}; + +function RoundName(props: { round?: Round }) { + return ( +

+ {props.round?.roundMetadata?.name || "Round Details"} +

+ ); +} + +export function ViewGrantsExplorerButton(props: ViewGrantsExplorerButtonType) { + const { chainId, roundId } = props; + + return ( + + ); +} + +function redirectToGrantExplorer(chainId: string, roundId: string | undefined) { + const url = `${process.env.REACT_APP_GRANT_EXPLORER}/#/round/${chainId}/${roundId}`; + setTimeout(() => { + window.open(url, "_blank", "noopener,noreferrer"); + }, 1000); +} + +function ApplicationOpenDateRange(props: { startTime: any; endTime: any }) { + return ( +
+ +

Applications:

+

+ {props.startTime || "..."} + - + {props.endTime || "..."} +

+
+ ); +} + +function RoundOpenDateRange(props: { startTime: any; endTime: any }) { + return ( +
+ +

Round:

+

+ {props.startTime || "..."} + - + {props.endTime || "..."} +

+
+ ); +} diff --git a/packages/round-manager/src/features/round/__tests__/ApplicationsApproved.test.tsx b/packages/round-manager/src/features/round/__tests__/ApplicationsApproved.test.tsx index 964faeecd9..93fb2530b5 100644 --- a/packages/round-manager/src/features/round/__tests__/ApplicationsApproved.test.tsx +++ b/packages/round-manager/src/features/round/__tests__/ApplicationsApproved.test.tsx @@ -393,11 +393,8 @@ export const renderWithContext = ( { applicationIdOverride, projectTwitterOverride: expectedTwitterHandle, }); - grantApplicationWithNoVc.project.credentials = {}; + + grantApplicationWithNoVc.project!.credentials = {}; (getApplicationById as any).mockResolvedValue(grantApplicationWithNoVc); renderWithContext(, { @@ -260,7 +261,7 @@ describe("ViewApplicationPage verification badges", () => { applicationIdOverride, projectGithubOverride: expectedGithubOrganizationName, }); - grantApplicationWithNoVc.project.credentials = {}; + grantApplicationWithNoVc.project!.credentials = {}; (getApplicationById as any).mockResolvedValue(grantApplicationWithNoVc); renderWithContext(, { @@ -311,7 +312,7 @@ describe("ViewApplicationPage verification badges", () => { applicationIdOverride, projectTwitterOverride: handle.toLowerCase(), }); - grantApplication.project.projectTwitter = handle.toUpperCase(); + grantApplication.project!.projectTwitter = handle.toUpperCase(); (getApplicationById as any).mockResolvedValue(grantApplication); renderWithContext(, { @@ -336,7 +337,7 @@ describe("ViewApplicationPage verification badges", () => { applicationIdOverride, projectGithubOverride: handle.toLowerCase(), }); - grantApplication.project.projectGithub = handle.toUpperCase(); + grantApplication.project!.projectGithub = handle.toUpperCase(); (getApplicationById as any).mockResolvedValue(grantApplication); renderWithContext(, { @@ -411,7 +412,7 @@ describe("ViewApplicationPage verification badges", () => { applicationIdOverride, projectGithubOverride: "whatever", }); - grantApplication.project.credentials["github"].issuer = fakeIssuer; + grantApplication.project!.credentials["github"].issuer = fakeIssuer; (getApplicationById as any).mockResolvedValue(grantApplication); renderWithContext(, { @@ -436,7 +437,7 @@ describe("ViewApplicationPage verification badges", () => { applicationIdOverride, projectTwitterOverride: handle, }); - grantApplication.project.projectTwitter = "not some handle"; + grantApplication.project!.projectTwitter = "not some handle"; (getApplicationById as any).mockResolvedValue(grantApplication); renderWithContext(, { @@ -461,7 +462,7 @@ describe("ViewApplicationPage verification badges", () => { applicationIdOverride, projectGithubOverride: handle, }); - grantApplication.project.projectGithub = "not some handle"; + grantApplication.project!.projectGithub = "not some handle"; (getApplicationById as any).mockResolvedValue(grantApplication); renderWithContext(, { @@ -489,7 +490,7 @@ describe("ViewApplicationPage verification badges", () => { applicationIdOverride, ...overrides, }); - grantApplicationData.project.owners.forEach((it) => { + grantApplicationData.project!.owners.forEach((it) => { it.address = "bad"; }); (getApplicationById as any).mockResolvedValue(grantApplicationData); @@ -514,10 +515,7 @@ export const renderWithContext = ( render( ({ }), })); -describe("the view round page", () => { +describe("View Round", () => { beforeEach(() => { (useParams as jest.Mock).mockImplementation(() => { return { @@ -65,7 +56,7 @@ describe("the view round page", () => { (useDisconnect as jest.Mock).mockReturnValue({}); }); - it("should display 404 when there no round is found", () => { + it("displays a 404 when there no round is found", () => { (useParams as jest.Mock).mockReturnValueOnce({ id: undefined, }); @@ -91,7 +82,7 @@ describe("the view round page", () => { expect(screen.getByText("404 ERROR")).toBeInTheDocument(); }); - it("should display access denied when wallet accessing is not round operator", () => { + it("displays access denied when wallet accessing is not round operator", () => { render( wrapWithBulkUpdateGrantApplicationContext( wrapWithApplicationContext( @@ -111,7 +102,7 @@ describe("the view round page", () => { expect(screen.getByText("Access Denied!")).toBeInTheDocument(); }); - it("should display Copy to Clipboard", () => { + it("displays Copy to Clipboard", () => { render( wrapWithBulkUpdateGrantApplicationContext( wrapWithApplicationContext( @@ -128,7 +119,7 @@ describe("the view round page", () => { expect(screen.getByText("Copy to clipboard")).toBeInTheDocument(); }); - it("should display copy when there are no applicants for a given round", () => { + it("displays copy when there are no applicants for a given round", () => { render( wrapWithBulkUpdateGrantApplicationContext( wrapWithApplicationContext( @@ -149,7 +140,7 @@ describe("the view round page", () => { expect(screen.getByText("No Applications")).toBeInTheDocument(); }); - it("should indicate how many of each kind of application there are", () => { + it("indicates how many of each kind of application there are", () => { const mockApplicationData: GrantApplication[] = [ makeGrantApplicationData(), makeGrantApplicationData(), @@ -191,7 +182,7 @@ describe("the view round page", () => { ).toBe(1); }); - it("should display loading spinner when round is loading", () => { + it("displays loading spinner when round is loading", () => { render( wrapWithApplicationContext( wrapWithReadProgramContext( @@ -206,33 +197,19 @@ describe("the view round page", () => { expect(screen.getByTestId("loading-spinner")).toBeInTheDocument(); }); -}); -export const renderWithContext = ( - ui: JSX.Element, - grantApplicationStateOverrides: Partial = {}, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - dispatch: any = jest.fn() -) => - render( - - - - {ui} - - - - ); + it("displays option to view round's explorer", () => { + render( + wrapWithApplicationContext( + wrapWithReadProgramContext( + wrapWithRoundContext(, { + data: [mockRoundData], + }) + ) + ) + ); + const roundExplorer = screen.getByTestId("round-explorer"); + + expect(roundExplorer).toBeInTheDocument(); + }); +}); diff --git a/packages/round-manager/src/test-utils.tsx b/packages/round-manager/src/test-utils.tsx index eaf69cddc6..21c974f30f 100644 --- a/packages/round-manager/src/test-utils.tsx +++ b/packages/round-manager/src/test-utils.tsx @@ -293,17 +293,13 @@ export const wrapWithRoundContext = ( export const wrapWithBulkUpdateGrantApplicationContext = ( ui: JSX.Element, - bulkUpdateOverrides: Partial = {}, + bulkUpdateOverrides: Partial = {} // eslint-disable-next-line @typescript-eslint/no-explicit-any - dispatch: any = jest.fn() ) => ( {ui}