diff --git a/package.json b/package.json index c7467ee18..ea305f8e7 100644 --- a/package.json +++ b/package.json @@ -134,6 +134,7 @@ "@storybook/testing-library": "^0.0.11", "@synthetixio/synpress": "^1.2.0", "@testing-library/cypress": "^8.0.2", + "@testing-library/react-hooks": "^8.0.0", "@testing-library/jest-dom": "^5.16.2", "@testing-library/react-hooks": "^8.0.0", "@types/jest": "^27.4.0", diff --git a/scripts/dev.ts b/scripts/dev.ts index 31f4df10b..eae78c19a 100644 --- a/scripts/dev.ts +++ b/scripts/dev.ts @@ -303,7 +303,8 @@ async function main() { }, { type: 'transfer', - timestamp: moment().subtract(26, 'minutes').unix(), + // We use this to reset to local time, in future we should ensure the automatic process works + timestamp: moment().subtract(46, 'minutes').unix(), from: accounts[0], data: { asset: ZERO_ADDRESS, diff --git a/src/Components/ExecuteButton/index.test.tsx b/src/Components/ExecuteButton/index.test.tsx new file mode 100644 index 000000000..58ff68d16 --- /dev/null +++ b/src/Components/ExecuteButton/index.test.tsx @@ -0,0 +1,32 @@ +import { fireEvent, screen, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import ExecuteButton from '.'; +import { render } from 'utils/tests'; + +let mockedIsExecutable = true; +let mockedExecuteProposal = jest.fn(); +jest.mock('hooks/Guilds/useExecutable', () => ({ + __esModule: true, + default: () => ({ + data: { + isExecutable: mockedIsExecutable, + executeProposal: mockedExecuteProposal, + }, + loading: false, + error: null, + }), +})); + +describe('Execute Button', () => { + beforeAll(() => { + render(); + }); + + it('User is able to click button to execute', async () => { + const button = screen.queryByTestId('execute-btn'); + fireEvent.click(button); + await waitFor(() => { + expect(mockedExecuteProposal).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/Components/ExecuteButton/index.tsx b/src/Components/ExecuteButton/index.tsx new file mode 100644 index 000000000..fcd733c5d --- /dev/null +++ b/src/Components/ExecuteButton/index.tsx @@ -0,0 +1,19 @@ +import { Button } from 'old-components/Guilds/common/Button'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface ExecuteButtonProps { + executeProposal: () => void; +} + +const ExecuteButton: React.FC = ({ executeProposal }) => { + const { t } = useTranslation(); + + return ( + + ); +}; + +export default ExecuteButton; diff --git a/src/Components/Fixtures/index.ts b/src/Components/Fixtures/index.ts index d2f9a763e..e4074bf6f 100644 --- a/src/Components/Fixtures/index.ts +++ b/src/Components/Fixtures/index.ts @@ -1,6 +1,7 @@ import { BigNumber } from 'ethers'; import moment from 'moment'; -import { Proposal, ENSAvatar, ProposalState } from '../Types'; +import { Proposal, ContractState } from 'types/types.guilds.d'; +import { ENSAvatar } from '../Types'; export const proposalMock: Proposal = { id: '0x1234567890123456789012345678901234567890', @@ -12,7 +13,8 @@ export const proposalMock: Proposal = { value: [], startTime: moment(), endTime: moment(), - state: ProposalState.Active, + timeDetail: '', + contractState: ContractState.Active, totalActions: BigNumber.from(0), totalVotes: [], }; @@ -24,6 +26,6 @@ export const ensAvatarMock: ENSAvatar = { export const proposalStatusPropsMock = { timeDetail: 'Time', - status: ProposalState.Active, + status: ContractState.Active, endTime: moment('2022-05-09T08:00:00'), }; diff --git a/src/Components/ProposalCard/__snapshots__/ProposalCard.test.tsx.snap b/src/Components/ProposalCard/__snapshots__/ProposalCard.test.tsx.snap index de770badc..35848e246 100644 --- a/src/Components/ProposalCard/__snapshots__/ProposalCard.test.tsx.snap +++ b/src/Components/ProposalCard/__snapshots__/ProposalCard.test.tsx.snap @@ -83,7 +83,7 @@ exports[`ProposalCard ProposalCard Renders properly with data 1`] = ` class="sc-hOqruk cHwGYV" >
` `; const DetailText = styled(Box)` -padding: 0 0.2rem; + padding: 0 0.2rem; -@media only screen and (min-width: 768px) { - padding - right: 0.5rem; -} + @media only screen and (min-width: 768px) { + padding-right: 0.5rem; + } `; const ProposalStatus: React.FC = ({ diff --git a/src/Components/ProposalStatus/__snapshots__/ProposalStatus.test.tsx.snap b/src/Components/ProposalStatus/__snapshots__/ProposalStatus.test.tsx.snap index eeac15f29..330d476b0 100644 --- a/src/Components/ProposalStatus/__snapshots__/ProposalStatus.test.tsx.snap +++ b/src/Components/ProposalStatus/__snapshots__/ProposalStatus.test.tsx.snap @@ -10,7 +10,7 @@ exports[`ProposalStatus loading 1`] = ` class="sc-iBPTik gOtsle" >
= ({ const ensAvatar = useENSAvatar(proposal?.creator, MAINNET_ID); - // Make into hooks - const timeDetail = useMemo(() => { - if (!proposal?.endTime) return null; - - const currentTime = moment(); - if (proposal.endTime?.isBefore(currentTime)) { - return proposal.endTime.fromNow(); - } else { - return proposal.endTime.toNow(); - } - }, [proposal]); - - // Make into singular guild state hook - const status = useMemo(() => { - if (!proposal?.endTime) return null; - switch (proposal.state) { - case ProposalState.Active: - const currentTime = moment(); - if (currentTime.isSameOrAfter(proposal.endTime)) { - return ProposalState.Failed; - } else { - return ProposalState.Active; - } - case ProposalState.Executed: - return ProposalState.Executed; - case ProposalState.Passed: - return ProposalState.Passed; - case ProposalState.Failed: - return ProposalState.Failed; - default: - return proposal.state; - } - }, [proposal]); + const status = useProposalState(proposal); return ( = ({ ensAvatar={ensAvatar} votes={votes} href={`/${chainName}/${guildId}/proposal/${proposalId}`} - statusProps={{ timeDetail, status, endTime: proposal?.endTime }} + statusProps={{ + timeDetail: proposal?.timeDetail, + status, + endTime: proposal?.endTime, + }} summaryActions={summaryActions} /> ); diff --git a/src/hooks/Guilds/ether-swr/guild/useProposal.ts b/src/hooks/Guilds/ether-swr/guild/useProposal.ts index 7f8534d85..a3a138f36 100644 --- a/src/hooks/Guilds/ether-swr/guild/useProposal.ts +++ b/src/hooks/Guilds/ether-swr/guild/useProposal.ts @@ -1,16 +1,35 @@ -import { unix } from 'moment'; +import moment, { unix } from 'moment'; import { Middleware, SWRHook } from 'swr'; -import { Proposal } from '../../../../types/types.guilds'; import useEtherSWR from '../useEtherSWR'; import ERC20GuildContract from 'contracts/ERC20Guild.json'; +import { Proposal, ContractState } from 'types/types.guilds.d'; const formatterMiddleware: Middleware = (useSWRNext: SWRHook) => (key, fetcher, config) => { const swr = useSWRNext(key, fetcher, config); if (swr.data) { const original = swr.data as any; - const clone: any = Object.assign({}, swr.data); + + //rename state to contractState + clone.contractState = clone.state; + delete clone.state; + + switch (clone.contractState) { + case 1: + clone.contractState = ContractState.Active; + break; + case 2: + clone.contractState = ContractState.Rejected; + break; + case 3: + clone.contractState = ContractState.Executed; + break; + case 4: + clone.contractState = ContractState.Failed; + break; + } + clone.startTime = original.startTime ? unix(original.startTime.toNumber()) : null; @@ -18,17 +37,28 @@ const formatterMiddleware: Middleware = ? unix(original.endTime.toNumber()) : null; + // Add timeDetail + const currentTime = moment(); + let differenceInMilliseconds = currentTime.diff(clone.endTime); + let timeDifference = moment.duration(differenceInMilliseconds).humanize(); + if (clone.endTime.isBefore(currentTime)) { + clone.timeDetail = `ended ${timeDifference} ago`; + } else { + clone.timeDetail = `${timeDifference} left`; + } + return { ...swr, data: clone }; } return swr; }; export const useProposal = (guildId: string, proposalId: string) => { - return useEtherSWR( + let result = useEtherSWR( guildId && proposalId ? [guildId, 'getProposal', proposalId] : [], { use: [formatterMiddleware], ABIs: new Map([[guildId, ERC20GuildContract.abi]]), } ); + return result; }; diff --git a/src/hooks/Guilds/guild/useProposalCalls.ts b/src/hooks/Guilds/guild/useProposalCalls.ts index 279fee59f..f60bf3d6c 100644 --- a/src/hooks/Guilds/guild/useProposalCalls.ts +++ b/src/hooks/Guilds/guild/useProposalCalls.ts @@ -2,12 +2,12 @@ import { useState, useEffect, useMemo } from 'react'; import { useTheme } from 'styled-components'; import { useWeb3React } from '@web3-react/core'; import { bulkDecodeCallsFromOptions } from '../contracts/useDecodedCall'; -import useProposalMetadata from 'hooks/Guilds/ether-swr/guild/useProposalMetadata'; import { decodeCall } from 'hooks/Guilds/contracts/useDecodedCall'; import { useProposal } from '../ether-swr/guild/useProposal'; import { useVotingResults } from 'hooks/Guilds/ether-swr/guild/useVotingResults'; import { Call, Option } from 'old-components/Guilds/ActionsBuilder/types'; import { ZERO_HASH } from 'utils'; +import useProposalMetadata from '../useProposalMetadata'; import { useRichContractRegistry } from '../contracts/useRichContractRegistry'; import { ERC20_APPROVE_SIGNATURE } from 'utils'; import useGuildImplementationTypeConfig from './useGuildImplementationType'; diff --git a/src/hooks/Guilds/useExecutable/index.test.ts b/src/hooks/Guilds/useExecutable/index.test.ts new file mode 100644 index 000000000..3d45bb634 --- /dev/null +++ b/src/hooks/Guilds/useExecutable/index.test.ts @@ -0,0 +1,92 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { BigNumber } from 'ethers'; +import { ContractState } from 'types/types.guilds.d'; +import useExecutable from '.'; +import * as hooks from '../ether-swr/guild/useProposal'; + +jest.mock('moment', () => { + return () => + jest.requireActual('moment')('01.01.2022 10:10', 'DD.MM.YYYY HH:mm'); +}); + +let mockedData = { + id: '0x0', + creator: '0x0', + startTime: jest.requireActual('moment')( + '01.01.2022 10:10', + 'DD.MM.YYYY HH:mm' + ), + endTime: jest.requireActual('moment')('01.01.2022 11:10', 'DD.MM.YYYY HH:mm'), + timeDetail: '', + to: ['0x0', '0x0'], + data: ['0x0', '0x0'], + value: [BigNumber.from(0), BigNumber.from(0)], + totalActions: BigNumber.from(0), + title: 'Proposal Title', + contentHash: '0x0', + contractState: ContractState.Active, + totalVotes: [BigNumber.from(0)], +}; + +jest.mock('react-router-dom', () => ({ + _esModule: true, + useParams: () => ({ + guildId: 'guild_id', + proposalId: 'proposal_id', + }), + useRouteMatch: () => ({ url: '/guild_id/proposal_id/' }), +})); + +jest.mock('contexts/Guilds/transactions', () => ({ + useTransactions: () => ({ + transactions: { + hash: '0x0', + from: '0x0', + addedTime: 0, + }, + pendingTransaction: { + summary: 'filler transaction', + transactionHash: '0x0', + cancelled: false, + showModal: true, + }, + createTransaction: jest.fn(() => true), + clearAllTransactions: jest.fn(), + }), +})); + +jest.mock('hooks/Guilds/contracts/useContract', () => ({ + useERC20Guild: () => ({ + contractId: '0x0', + abi: 'anything', + provider: jest.fn(), + account: '0x0', + chainId: 0, + walletChainId: 0, + withSignerIfPossible: false, + }), +})); + +describe('useExecutable', () => { + it(`executeProposal function is valid if there is proposal data`, async () => { + jest.spyOn(hooks, 'useProposal').mockImplementation(() => ({ + data: mockedData, + isValidating: false, + mutate: null, + })); + const { result } = renderHook(() => useExecutable()); + expect(result.current.loading).toBeFalsy(); + expect(result.current.data.executeProposal).toBeTruthy(); + }); + + it(`executeProposal function is null if there isn't proposal data`, async () => { + jest.spyOn(hooks, 'useProposal').mockImplementation(() => ({ + data: null, + isValidating: false, + mutate: null, + })); + const { result } = renderHook(() => useExecutable()); + expect(result.current.loading).toBeTruthy(); + expect(result.current.data.executeProposal).toBeNull(); + }); +}); diff --git a/src/hooks/Guilds/useExecutable/index.ts b/src/hooks/Guilds/useExecutable/index.ts new file mode 100644 index 000000000..d8b298071 --- /dev/null +++ b/src/hooks/Guilds/useExecutable/index.ts @@ -0,0 +1,47 @@ +import { useTransactions } from 'contexts/Guilds/transactions'; +import { useERC20Guild } from 'hooks/Guilds/contracts/useContract'; +import { useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import { useProposal } from '../ether-swr/guild/useProposal'; + +interface useExecutableReturns { + data: { + executeProposal: () => void; + }; + error: Error; + loading: boolean; +} + +function useExecutable(): useExecutableReturns { + const { guildId, proposalId } = + useParams<{ guildId?: string; proposalId?: string }>(); + const { data: proposal, error } = useProposal(guildId, proposalId); + const { createTransaction } = useTransactions(); + const guildContract = useERC20Guild(guildId); + + const { data, loading } = useMemo(() => { + const executeProposal = () => + createTransaction('Execute Proposal', async () => { + return guildContract.endProposal(proposalId); + }); + + if (!proposal) + return { + data: { executeProposal: null }, + loading: true, + error: error, + }; + + let result = { + data: { executeProposal }, + loading: false, + error: error, + }; + + return result; + }, [createTransaction, error, guildContract, proposal, proposalId]); + + return { data, error, loading }; +} + +export default useExecutable; diff --git a/src/hooks/Guilds/ether-swr/guild/useProposalMetadata.ts b/src/hooks/Guilds/useProposalMetadata.ts similarity index 91% rename from src/hooks/Guilds/ether-swr/guild/useProposalMetadata.ts rename to src/hooks/Guilds/useProposalMetadata.ts index bcf186997..a78934ab3 100644 --- a/src/hooks/Guilds/ether-swr/guild/useProposalMetadata.ts +++ b/src/hooks/Guilds/useProposalMetadata.ts @@ -2,7 +2,7 @@ import useIPFSFile from 'hooks/Guilds/ipfs/useIPFSFile'; import { useMemo } from 'react'; import { ProposalMetadata } from 'types/types.guilds'; import contentHash from 'content-hash'; -import { useProposal } from './useProposal'; +import { useProposal } from './ether-swr/guild/useProposal'; function useProposalMetadata(guildId: string, proposalId: string) { const { data: proposal, error } = useProposal(guildId, proposalId); diff --git a/src/hooks/Guilds/useProposalState/index.test.ts b/src/hooks/Guilds/useProposalState/index.test.ts new file mode 100644 index 000000000..a3a68745e --- /dev/null +++ b/src/hooks/Guilds/useProposalState/index.test.ts @@ -0,0 +1,82 @@ +import useProposalState from '.'; +import { Proposal, ContractState } from 'types/types.guilds.d'; +import { BigNumber } from 'ethers'; +import { renderHook } from '@testing-library/react-hooks'; + +jest.mock('moment', () => { + return () => + jest.requireActual('moment')('01.01.2022 11:10', 'DD.MM.YYYY HH:mm'); +}); + +const proposal: Proposal = { + id: '0x0', + creator: '0x0', + startTime: jest.requireActual('moment')( + '01.01.2022 10:10', + 'DD.MM.YYYY HH:mm' + ), + endTime: jest.requireActual('moment')('01.01.2022 12:10', 'DD.MM.YYYY HH:mm'), + timeDetail: '', + to: ['0x0', '0x0'], + data: ['0x0', '0x0'], + value: [BigNumber.from(0), BigNumber.from(0)], + totalActions: BigNumber.from(0), + title: 'Proposal Title', + contentHash: '0x0', + contractState: ContractState.Active, + totalVotes: [BigNumber.from(0)], +}; + +describe(`useProposalState`, () => { + it(`Should return null if there's no endTime property`, () => { + const tempProposal = { ...proposal }; + tempProposal.endTime = null; + + const { result } = renderHook(() => useProposalState(tempProposal)); + expect(result.current).toBeNull(); + }); + + it(`Should return 'Active' if the state is 'Active' and the current time is before endTime`, () => { + const tempProposal = { ...proposal }; + tempProposal.contractState = ContractState.Active; + + const { result } = renderHook(() => useProposalState(tempProposal)); + expect(result.current).toBe('Active'); + }); + + it(`Should return 'Executable' if the state is 'Active' and the current time is after the endTime`, () => { + const tempProposal = { ...proposal }; + tempProposal.contractState = ContractState.Active; + tempProposal.endTime = jest.requireActual('moment')( + '01.01.2022 11:00', + 'DD.MM.YYYY HH:mm' + ); + + const { result } = renderHook(() => useProposalState(tempProposal)); + expect(result.current).toBe('Executable'); + }); + + it(`Should return 'Executed' if the state is 'Executed'`, () => { + const tempProposal = { ...proposal }; + tempProposal.contractState = ContractState.Executed; + + const { result } = renderHook(() => useProposalState(tempProposal)); + expect(result.current).toBe('Executed'); + }); + + it(`Should return 'Rejected' if the state is 'Rejected'`, () => { + const tempProposal = { ...proposal }; + tempProposal.contractState = ContractState.Rejected; + + const { result } = renderHook(() => useProposalState(tempProposal)); + expect(result.current).toBe('Rejected'); + }); + + it(`Should return 'Failed' if the state is 'Failed'`, () => { + const tempProposal = { ...proposal }; + tempProposal.contractState = ContractState.Failed; + + const { result } = renderHook(() => useProposalState(tempProposal)); + expect(result.current).toBe('Failed'); + }); +}); diff --git a/src/hooks/Guilds/useProposalState/index.tsx b/src/hooks/Guilds/useProposalState/index.tsx new file mode 100644 index 000000000..7341842c9 --- /dev/null +++ b/src/hooks/Guilds/useProposalState/index.tsx @@ -0,0 +1,28 @@ +import moment from 'moment'; +import { Proposal, ContractState, ProposalState } from 'types/types.guilds.d'; + +// Contract state is direct contract storage state but we want to show more accurate up to date data to the user so we process into proposalState + +export const useProposalState = (proposal: Proposal): ProposalState => { + if (!proposal?.endTime) return null; + switch (proposal.contractState) { + case ContractState.Active: + const currentTime = moment(); + + if (currentTime.isSameOrAfter(proposal.endTime)) { + return ProposalState.Executable; + } else { + return ProposalState.Active; + } + case ContractState.Executed: + return ProposalState.Executed; + case ContractState.Rejected: + return ProposalState.Rejected; + case ContractState.Failed: + return ProposalState.Failed; + default: + return null; + } +}; + +export default useProposalState; diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 833cdaa13..51db0c813 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -9,6 +9,7 @@ "actions_one": "Action", "actions_other": "Actions", "save": "Save", + "execute": "Execute", "core": "Core", "switch": "Switch", "wallet": "Wallet", diff --git a/src/old-components/Guilds/ProposalPage/ProposalDescription/index.tsx b/src/old-components/Guilds/ProposalPage/ProposalDescription/index.tsx index 84bbb2ded..bdb6a0bc3 100644 --- a/src/old-components/Guilds/ProposalPage/ProposalDescription/index.tsx +++ b/src/old-components/Guilds/ProposalPage/ProposalDescription/index.tsx @@ -1,8 +1,8 @@ import { useTypedParams } from 'Modules/Guilds/Hooks/useTypedParams'; -import useProposalMetadata from 'hooks/Guilds/ether-swr/guild/useProposalMetadata'; import Markdown from 'markdown-to-jsx'; import { Loading } from 'Components/Primitives/Loading'; import styled from 'styled-components'; +import useProposalMetadata from 'hooks/Guilds/useProposalMetadata'; const ProposalDescriptionWrapper = styled.div` margin: 1.5rem 0; diff --git a/src/old-components/Guilds/ProposalPage/ProposalInfoCard/index.tsx b/src/old-components/Guilds/ProposalPage/ProposalInfoCard/index.tsx index 5046ff97f..7b2d469ef 100644 --- a/src/old-components/Guilds/ProposalPage/ProposalInfoCard/index.tsx +++ b/src/old-components/Guilds/ProposalPage/ProposalInfoCard/index.tsx @@ -74,11 +74,8 @@ const ProposalInfoCard: React.FC = () => { if (!proposal || !proposal.endTime) return null; const currentTime = moment(); - if (proposal.endTime.isBefore(currentTime)) { - return `Ended ${proposal.endTime.fromNow()}`; - } else { - return `Ends ${proposal.endTime.toNow()}`; - } + let prefix = proposal.endTime.isBefore(currentTime) ? 'Ended' : 'Ends'; + return `${prefix} ${proposal.endTime.fromNow()}`; }, [proposal]); if (error) return
Error
; diff --git a/src/old-components/Guilds/ProposalPage/ProposalVoteCard/VoteResults.tsx b/src/old-components/Guilds/ProposalPage/ProposalVoteCard/VoteResults.tsx index 7eb77b1a1..aa26af52a 100644 --- a/src/old-components/Guilds/ProposalPage/ProposalVoteCard/VoteResults.tsx +++ b/src/old-components/Guilds/ProposalPage/ProposalVoteCard/VoteResults.tsx @@ -1,11 +1,11 @@ import { useTypedParams } from 'Modules/Guilds/Hooks/useTypedParams'; import { formatUnits } from 'ethers/lib/utils'; -import useProposalMetadata from 'hooks/Guilds/ether-swr/guild/useProposalMetadata'; import { useVotingResults } from 'hooks/Guilds/ether-swr/guild/useVotingResults'; import useVotingPowerPercent from 'hooks/Guilds/guild/useVotingPowerPercent'; import Bullet from 'old-components/Guilds/common/Bullet'; import { Loading } from 'Components/Primitives/Loading'; import styled, { useTheme } from 'styled-components'; +import useProposalMetadata from 'hooks/Guilds/useProposalMetadata'; const VotesRowWrapper = styled.div` display: flex; diff --git a/src/old-components/Guilds/ProposalPage/ProposalVoteCard/index.tsx b/src/old-components/Guilds/ProposalPage/ProposalVoteCard/index.tsx index b7533aae1..4914fc7aa 100644 --- a/src/old-components/Guilds/ProposalPage/ProposalVoteCard/index.tsx +++ b/src/old-components/Guilds/ProposalPage/ProposalVoteCard/index.tsx @@ -12,7 +12,6 @@ import { useTransactions } from 'contexts/Guilds'; import { BigNumber } from 'ethers'; import { useERC20Guild } from 'hooks/Guilds/contracts/useContract'; import { useProposal } from 'hooks/Guilds/ether-swr/guild/useProposal'; -import useProposalMetadata from 'hooks/Guilds/ether-swr/guild/useProposalMetadata'; import useSnapshotId from 'hooks/Guilds/ether-swr/guild/useSnapshotId'; import { useVotingPowerOf } from 'hooks/Guilds/ether-swr/guild/useVotingPowerOf'; import { useVotingResults } from 'hooks/Guilds/ether-swr/guild/useVotingResults'; @@ -23,6 +22,7 @@ import { Loading } from 'Components/Primitives/Loading'; import { useState, useMemo } from 'react'; import { toast } from 'react-toastify'; import styled, { css, useTheme } from 'styled-components'; +import useProposalMetadata from 'hooks/Guilds/useProposalMetadata'; const ButtonsContainer = styled.div` flex-direction: column; diff --git a/src/pages/Guilds/Proposal.tsx b/src/pages/Guilds/Proposal.tsx index ebac71415..0c7087b32 100644 --- a/src/pages/Guilds/Proposal.tsx +++ b/src/pages/Guilds/Proposal.tsx @@ -14,12 +14,15 @@ import useProposalCalls from 'hooks/Guilds/guild/useProposalCalls'; import { ActionsBuilder } from 'old-components/Guilds/CreateProposalPage'; import { Loading } from 'Components/Primitives/Loading'; import Result, { ResultState } from 'old-components/Guilds/common/Result'; -import React, { useContext, useMemo } from 'react'; +import React, { useContext } from 'react'; import { FaChevronLeft } from 'react-icons/fa'; import { FiArrowLeft } from 'react-icons/fi'; import styled from 'styled-components'; -import moment from 'moment'; -import { ProposalState } from 'Components/Types'; +import ExecuteButton from 'Components/ExecuteButton'; +import { useProposalState } from 'hooks/Guilds/useProposalState'; +import useExecutable from 'hooks/Guilds/useExecutable'; +import { useGuildConfig } from 'hooks/Guilds/ether-swr/guild/useGuildConfig'; +import { ProposalState } from 'types/types.guilds.d'; const PageContainer = styled(Box)` display: grid; @@ -85,40 +88,13 @@ const ProposalPage: React.FC = () => { const { data: proposalIds } = useGuildProposalIds(guildId); const { data: proposal, error } = useProposal(guildId, proposalId); const { options } = useProposalCalls(guildId, proposalId); + const { data: guildConfig } = useGuildConfig(guildId); - // TODO These are copied from ProposalCardWrapper and to be replaced - const timeDetail = useMemo(() => { - if (!proposal?.endTime) return null; + const status = useProposalState(proposal); - const currentTime = moment(); - if (proposal.endTime?.isBefore(currentTime)) { - return proposal.endTime.fromNow(); - } else { - return proposal.endTime.toNow(); - } - }, [proposal]); - - // TODO These are copied from ProposalCardWrapper and to be replaced - const status = useMemo(() => { - if (!proposal?.endTime) return null; - switch (proposal.state) { - case ProposalState.Active: - const currentTime = moment(); - if (currentTime.isSameOrAfter(proposal.endTime)) { - return ProposalState.Failed; - } else { - return ProposalState.Active; - } - case ProposalState.Executed: - return ProposalState.Executed; - case ProposalState.Passed: - return ProposalState.Passed; - case ProposalState.Failed: - return ProposalState.Failed; - default: - return proposal.state; - } - }, [proposal]); + const { + data: { executeProposal }, + } = useExecutable(); if (!isGuildAvailabilityLoading) { if (!proposalIds?.includes(proposalId)) { @@ -154,15 +130,19 @@ const ProposalPage: React.FC = () => { - DXdao + {' '} + {guildConfig?.name} + {status === ProposalState.Executable && ( + + )} {proposal?.title || ( diff --git a/src/types/types.guilds.d.ts b/src/types/types.guilds.d.ts index f982d1605..8cce96325 100644 --- a/src/types/types.guilds.d.ts +++ b/src/types/types.guilds.d.ts @@ -1,28 +1,39 @@ import { Moment } from 'moment'; import { - BigNumber, + BigNumber } from 'ethers'; export interface Proposal { id: string; creator: string; startTime: Moment; endTime: Moment; + timeDetail: string | null; to: string[]; data: string[]; value: BigNumber[]; totalActions: BigNumber; title: string; contentHash: string; - state: ProposalState; + contractState: ContractState; totalVotes: BigNumber[]; } + export enum ProposalState { - Active = "Active", - Passed = "Passed", - Executed = "Executed", - Failed = "Failed", + Active = 'Active', + Executable = 'Executable', + Executed = 'Executed', + Rejected = 'Rejected', + Failed = 'Failed', +} + +export enum ContractState { + Active = 'Active', + Rejected = 'Rejected', + Executed = 'Executed', + Failed = 'Failed', } + export interface ProposalMetadata { description: string; voteOptions: string[];