From 0095d4eb395d9e89b8b98cbaf53bb8b845a0d4e9 Mon Sep 17 00:00:00 2001 From: Viviane Date: Tue, 25 Feb 2020 17:21:09 -0300 Subject: [PATCH 01/29] chore(packages): Update aragon packages --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f978d98..7479b91 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,13 @@ "version": "1.0.0", "description": "", "dependencies": { + "@aragon/api": "^2.0.0-beta.9", + "@aragon/api-react": "^2.0.0-beta.9", "@aragon/apps-shared-minime": "^1.0.1", "@aragon/apps-token-manager": "2.1.0", "@aragon/apps-vault": "^4.1.0", - "@aragon/os": "^4.2.1" + "@aragon/os": "^4.2.1", + "@aragon/ui": "^1.3.1" }, "devDependencies": { "@aragon/cli": "^6.0.0", From 3fd66de3841929774fac89ca8e8ee22cdf91de96 Mon Sep 17 00:00:00 2001 From: Viviane Date: Tue, 25 Feb 2020 17:21:52 -0300 Subject: [PATCH 02/29] feat(proposals): Migrate proposals data view to another comp Proposals is a component in the screens folder, separated from the App file, and recieves `proposals` and `selectProposal` as props from the App component. --- app/src/screens/Proposals.js | 123 +++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 app/src/screens/Proposals.js diff --git a/app/src/screens/Proposals.js b/app/src/screens/Proposals.js new file mode 100644 index 0000000..103a1c8 --- /dev/null +++ b/app/src/screens/Proposals.js @@ -0,0 +1,123 @@ +import React from 'react' +import { + // Bar, + DataView, + // DropDown, + Link, + // Tag, + // GU, + // textStyle, + // useLayout, + useTheme, + Text, +} from '@aragon/ui' +import { ConvictionBar, ConvictionTrend } from '../components/ConvictionVisuals' +import Balance from '../components/Balance' +import { useAragonApi } from '@aragon/api-react' + +const Proposals = React.memo(function Proposals({ + // proposals, + selectProposal, + // executionTargets, + filteredProposals, + // proposalStatusFilter, + // handleProposalStatusFilterChange, +}) { + // const theme = useTheme() + // const { layoutName } = useLayout() + + return ( + + {/* {layoutName !== 'small' && ( + +
+ + All + + + +
, + 'Open', + 'Closed', + ]} + width="128px" + /> + +
+ )} */} + [ + , + , + , + , + ]} + /> +
+ ) +}) + +const IdAndTitle = ({ id, name, selectProposal }) => { + const theme = useTheme() + return ( + selectProposal(id)}> + #{id}{' '} + {name} + + ) +} + +const Amount = ({ requestedAmount = 0 }) => { + const { + appState: { + requestToken: { symbol, decimals, verified }, + }, + } = useAragonApi() + return ( +
+ +
+ ) +} + +export default Proposals From 88091333ced6be1c6e8c6000cfcd8c6deb0fa2cd Mon Sep 17 00:00:00 2001 From: Viviane Date: Tue, 25 Feb 2020 17:24:20 -0300 Subject: [PATCH 03/29] feat(logic): Separate app login in another file App logic passes function hooks to whoever calls it, such as `selectProposal` or `isSyncing`. --- app/src/app-logic.js | 79 ++++++++++++++++++++++++++++++++++++++++++++ app/src/utils.js | 1 + 2 files changed, 80 insertions(+) create mode 100644 app/src/app-logic.js create mode 100644 app/src/utils.js diff --git a/app/src/app-logic.js b/app/src/app-logic.js new file mode 100644 index 0000000..315f10a --- /dev/null +++ b/app/src/app-logic.js @@ -0,0 +1,79 @@ +import { useCallback, useMemo } from 'react' +import { useApi, useAppState, usePath } from '@aragon/api-react' +// import usePanelState from './hooks/usePanelState' +import { noop } from './utils' + +const PROPOSAL_ID_PATH_RE = /^\/proposal\/([0-9]+)\/?$/ +const NO_PROPOSAL_ID = '-1' + +function idFromPath(path) { + if (!path) { + return NO_PROPOSAL_ID + } + const matches = path.match(PROPOSAL_ID_PATH_RE) + return matches ? matches[1] : NO_PROPOSAL_ID +} + +// Get the proposal currently selected, or null otherwise. +export function useSelectedProposal(proposals) { + const [path, requestPath] = usePath() + const { isSyncing } = useAppState() + + // The memoized proposal currently selected. + const selectedProposal = useMemo(() => { + const id = idFromPath(path) + + // The `isSyncing` check prevents a proposal to be + // selected until the app state is fully ready. + if (isSyncing || id === NO_PROPOSAL_ID) { + return null + } + + return proposals.find(proposal => proposal.id === id) || null + }, [path, isSyncing, proposals]) + + const selectProposal = useCallback( + id => { + requestPath(String(id) === NO_PROPOSAL_ID ? '' : `/proposal/${id}/`) + }, + [requestPath] + ) + + return [selectedProposal, selectProposal] +} + +// Create a new proposal +export function useCreateProposalAction(onDone = noop) { + const api = useApi() + return useCallback( + title => { + if (api) { + // Don't care about response + api['newProposal(string)'](title).toPromise() + onDone() + } + }, + [api, onDone] + ) +} + +// Handles the main logic of the app. +export default function useAppLogic() { + const { isSyncing, proposals = [] } = useAppState() + + const [selectedProposal, selectProposal] = useSelectedProposal(proposals) + // const newProposalPanel = usePanelState() + + // const actions = { + // createProposal: useCreateProposalAction(newProposalPanel.requestClose), + // } + + return { + // actions, + isSyncing, + // newProposalPanel, + selectProposal, + selectedProposal, + proposals, + } +} diff --git a/app/src/utils.js b/app/src/utils.js new file mode 100644 index 0000000..177804c --- /dev/null +++ b/app/src/utils.js @@ -0,0 +1 @@ +export function noop() {} From 8d9b72292f6bba841658914e88b0cce0aec74a8a Mon Sep 17 00:00:00 2001 From: Viviane Date: Tue, 25 Feb 2020 17:26:37 -0300 Subject: [PATCH 04/29] feat: App now uses appLogic and accepts darkmode Major change in the component, that now renders the proposals using the `Proposals` component, and passes functions to its components via the appLogic hook. There were some UI updates such as Header, that is simpler and mobile friendly. --- app/src/App.js | 208 +++++++++++++++++++++---------------------------- 1 file changed, 87 insertions(+), 121 deletions(-) diff --git a/app/src/App.js b/app/src/App.js index c13b867..248e3fe 100644 --- a/app/src/App.js +++ b/app/src/App.js @@ -1,28 +1,39 @@ import React, { useState } from 'react' -import { useAragonApi } from '@aragon/api-react' +import { useAragonApi, useGuiStyle } from '@aragon/api-react' import { Main, Button, SidePanel, Box, - DataView, - useTheme, - Text, Tag, + SyncIndicator, + IconPlus, + Header, + useLayout, } from '@aragon/ui' import styled from 'styled-components' -import AppHeader from './components/AppHeader' -import Balance from './components/Balance' -import ProposalDetail from './components/ProposalDetail' +// import ProposalDetail from './components/ProposalDetail' import AddProposalPanel from './components/AddProposalPanel' -import { ConvictionBar, ConvictionTrend } from './components/ConvictionVisuals' +import Balance from './components/Balance' +import useAppLogic from './app-logic' import { toDecimals } from './lib/math-utils' import { toHex } from 'web3-utils' +import Proposals from './screens/Proposals' + +const App = React.memo(function App() { + const { + isSyncing, + selectProposal, + selectedProposal, + proposals, + } = useAppLogic() + + const { layoutName } = useLayout() + const compactMode = layoutName === 'small' -function App() { const { api, appState, connectedAccount } = useAragonApi() - const { proposals, convictionStakes, requestToken } = appState - const activeProposals = proposals.filter(({ executed }) => !executed) + const { convictionStakes, requestToken } = appState + const filteredProposals = proposals.filter(({ executed }) => !executed) const [proposalPanel, setProposalPanel] = useState(false) const onProposalSubmit = ({ title, link, amount, beneficiary }) => { @@ -40,125 +51,80 @@ function App() { const myLastStake = [...myStakes].pop() || {} return ( -
- <> - + +
setProposalPanel(true)} - > - New proposal - - } - /> - -
- - - - {myLastStake.tokensStaked > 0 && ( - - id === myLastStake.proposal)[0] - } - stake={myLastStake} - /> - - )} -
-
- [ - , - , - , - , - ]} - renderEntryExpansion={proposal => ( - - api.stakeAllToProposal(proposal.id).toPromise() - } - onWithdraw={() => - api.withdrawAllFromProposal(proposal.id).toPromise() - } - onExecute={() => - api.executeProposal(proposal.id, true).toPromise() - } - /> - )} + label="New proposal" + icon={} + display={compactMode ? 'icon' : 'label'} /> -
-
- setProposalPanel(false)} - > - - - -
- ) -} - -const IdAndTitle = ({ id, name }) => { - const theme = useTheme() - return ( -
- #{id}{' '} - {name} -
- ) -} - -const Amount = ({ requestedAmount = 0 }) => { - const { - appState: { - requestToken: { symbol, decimals, verified }, - }, - } = useAragonApi() - return ( -
- -
+ +
+ + + + {/* {myLastStake.tokensStaked > 0 && ( + + id === myLastStake.proposal)[0] + } + stake={myLastStake} + /> + + )} */} +
+
+ +
+
+ setProposalPanel(false)} + > + + + ) -} +}) -const ProposalInfo = ({ proposal, stake }) => { - const { - appState: { - stakeToken: { tokenSymbol }, - }, - } = useAragonApi() - return ( -
- - {`✓ Supported with ${stake.tokensStaked} ${tokenSymbol}`} - -
- ) -} +// const ProposalInfo = ({ proposal, stake }) => { +// const { +// appState: { +// stakeToken: { tokenSymbol }, +// }, +// } = useAragonApi() +// return ( +//
+// +// {`✓ Supported with ${stake.tokensStaked} ${tokenSymbol}`} +// +//
+// ) +// } const Wrapper = styled.div` display: flex; ` -export default App +export default () => { + const { appearance } = useGuiStyle() + return ( +
+ +
+ ) +} From b6839f3b2bf1b04f68e04fc96c84c2a1d8552a57 Mon Sep 17 00:00:00 2001 From: Viviane Date: Tue, 25 Feb 2020 19:55:46 -0300 Subject: [PATCH 05/29] feat(proposals): Add no proposals component If there are no proposals made, then the app will render this. --- app/src/assets/no-proposals.png | Bin 0 -> 39192 bytes app/src/screens/NoProposals.js | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 app/src/assets/no-proposals.png create mode 100644 app/src/screens/NoProposals.js diff --git a/app/src/assets/no-proposals.png b/app/src/assets/no-proposals.png new file mode 100644 index 0000000000000000000000000000000000000000..82f4b89101fbb7ce9bdb78ead173cbff40ec250d GIT binary patch literal 39192 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 + + Syncing… + + ) : ( + 'No proposals here!' + ) + } + action={ + + } + illustration={ + No proposal here + } + /> + ) +}) + +export default NoProposals From f9d43bb105c1e7cf8088806df732daf061cf3a5e Mon Sep 17 00:00:00 2001 From: Viviane Date: Tue, 25 Feb 2020 19:56:52 -0300 Subject: [PATCH 06/29] feat(detail): Add proposal detail page When a proposal is selected, a proposal detail page opens with the details from that proposal. --- app/src/screens/ProposalDetail.js | 112 ++++++++++++++++++++++++++++++ app/src/web3-utils.js | 30 ++++++++ 2 files changed, 142 insertions(+) create mode 100644 app/src/screens/ProposalDetail.js create mode 100644 app/src/web3-utils.js diff --git a/app/src/screens/ProposalDetail.js b/app/src/screens/ProposalDetail.js new file mode 100644 index 0000000..6e221d5 --- /dev/null +++ b/app/src/screens/ProposalDetail.js @@ -0,0 +1,112 @@ +import React from 'react' +import { + BackButton, + Bar, + Box, + GU, + Split, + Text, + textStyle, + useLayout, + useTheme, +} from '@aragon/ui' +import { useConnectedAccount } from '@aragon/api-react' +import LocalIdentityBadge from '../components/LocalIdentityBadge/LocalIdentityBadge' +import { addressesEqual } from '../web3-utils' + +const DEFAULT_DESCRIPTION = + 'No additional description has been provided for this proposal.' + +function ProposalDetail({ proposal, onBack }) { + const theme = useTheme() + const { layoutName } = useLayout() + const connectedAccount = useConnectedAccount() + + const { id, title, description, creator } = proposal + + return ( + + + + + +
+

+ Proposal #{id} - {title} +

+
+
+

+ Description +

+ + {description || DEFAULT_DESCRIPTION} + +
+
+

+ Created By +

+
+ +
+
+
+
+ + } + secondary={Dummy} + /> +
+ ) +} + +export default ProposalDetail diff --git a/app/src/web3-utils.js b/app/src/web3-utils.js new file mode 100644 index 0000000..c9d97c6 --- /dev/null +++ b/app/src/web3-utils.js @@ -0,0 +1,30 @@ +const ETH_ADDRESS_SPLIT_REGEX = /(0x[a-fA-F0-9]{40}(?:\b|\.|,|\?|!|;))/g +const ETH_ADDRESS_TEST_REGEX = /(0x[a-fA-F0-9]{40}(?:\b|\.|,|\?|!|;))/g + +export const EMPTY_ADDRESS = '0x0000000000000000000000000000000000000000' + +// Detect Ethereum addresses in a string and transform each part. +// +// `callback` is called on every part with two params: +// - The string of the current part. +// - A boolean indicating if it is an address. +// +export function transformAddresses(str, callback) { + return str + .split(ETH_ADDRESS_SPLIT_REGEX) + .map((part, index) => + callback(part, ETH_ADDRESS_TEST_REGEX.test(part), index) + ) +} + +/** + * Check address equality without checksums + * @param {string} first First address + * @param {string} second Second address + * @returns {boolean} Address equality + */ +export function addressesEqual(first, second) { + first = first && first.toLowerCase() + second = second && second.toLowerCase() + return first === second +} From 8078c4b3f2ea4867b56c3165f3cbcbc1b77aabfe Mon Sep 17 00:00:00 2001 From: Viviane Date: Tue, 25 Feb 2020 19:58:50 -0300 Subject: [PATCH 07/29] feat(app): Update app logic to render new components App now works in a different logic - according to the hooks state, selectedProposal and proposals. --- app/src/App.js | 141 ++++++++++++++++++++++++++++++------------------- 1 file changed, 88 insertions(+), 53 deletions(-) diff --git a/app/src/App.js b/app/src/App.js index 248e3fe..a95d7e6 100644 --- a/app/src/App.js +++ b/app/src/App.js @@ -1,24 +1,28 @@ -import React, { useState } from 'react' +import React, { useState, useCallback } from 'react' import { useAragonApi, useGuiStyle } from '@aragon/api-react' import { Main, Button, SidePanel, Box, - Tag, + // Tag, SyncIndicator, IconPlus, Header, useLayout, } from '@aragon/ui' import styled from 'styled-components' + +import NoProposals from './screens/NoProposals' +import ProposalDetail from './screens/ProposalDetail' +import Proposals from './screens/Proposals' // import ProposalDetail from './components/ProposalDetail' import AddProposalPanel from './components/AddProposalPanel' import Balance from './components/Balance' + import useAppLogic from './app-logic' import { toDecimals } from './lib/math-utils' import { toHex } from 'web3-utils' -import Proposals from './screens/Proposals' const App = React.memo(function App() { const { @@ -27,12 +31,20 @@ const App = React.memo(function App() { selectedProposal, proposals, } = useAppLogic() + const handleBack = useCallback(() => selectProposal(-1), [selectProposal]) const { layoutName } = useLayout() const compactMode = layoutName === 'small' - const { api, appState, connectedAccount } = useAragonApi() - const { convictionStakes, requestToken } = appState + const { + api, + appState, + // connectedAccount + } = useAragonApi() + const { + // convictionStakes, + requestToken, + } = appState const filteredProposals = proposals.filter(({ executed }) => !executed) const [proposalPanel, setProposalPanel] = useState(false) @@ -43,60 +55,83 @@ const App = React.memo(function App() { setProposalPanel(false) } - const myStakes = - (convictionStakes && - convictionStakes.filter(({ entity }) => entity === connectedAccount)) || - [] + // const myStakes = + // (convictionStakes && + // convictionStakes.filter(({ entity }) => entity === connectedAccount)) || + // [] - const myLastStake = [...myStakes].pop() || {} + // const myLastStake = [...myStakes].pop() || {} return ( - -
setProposalPanel(true)} - label="New proposal" - icon={} - display={compactMode ? 'icon' : 'label'} - /> - ) - } - /> - -
- - - - {/* {myLastStake.tokensStaked > 0 && ( - - id === myLastStake.proposal)[0] - } - stake={myLastStake} - /> - - )} */} -
-
- + setProposalPanel(true)} + isSyncing={isSyncing} />
-
- setProposalPanel(false)} - > - - + )} + {proposals.length > 0 && ( + + +
setProposalPanel(true)} + label="New proposal" + icon={} + display={compactMode ? 'icon' : 'label'} + /> + ) + } + /> + +
+ + + + {/* {myLastStake.tokensStaked > 0 && ( + + id === myLastStake.proposal)[0] + } + stake={myLastStake} + /> + + )} */} +
+ {selectedProposal ? ( + + ) : ( +
+ +
+ )} +
+ setProposalPanel(false)} + > + + + + )} ) }) From ba7c80199d90fd3ffb60d055fe71a916a88a4469 Mon Sep 17 00:00:00 2001 From: Viviane Date: Tue, 25 Feb 2020 19:59:48 -0300 Subject: [PATCH 08/29] feat(selected): selectedProposal func formats id type to number --- app/src/app-logic.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/app-logic.js b/app/src/app-logic.js index 315f10a..ae1e9fc 100644 --- a/app/src/app-logic.js +++ b/app/src/app-logic.js @@ -29,7 +29,9 @@ export function useSelectedProposal(proposals) { return null } - return proposals.find(proposal => proposal.id === id) || null + return ( + proposals.find(proposal => Number(proposal.id) === Number(id)) || null + ) }, [path, isSyncing, proposals]) const selectProposal = useCallback( From 45ecbff0aeb68bea47f8a697c7bc852e33d30eda Mon Sep 17 00:00:00 2001 From: Viviane Date: Wed, 26 Feb 2020 23:15:27 -0300 Subject: [PATCH 09/29] feat(logic): All app logic is separated in another file, app-logic This logic is passed to App and it's child components via props, if necessary. --- app/src/app-logic.js | 110 ++++++++++++++++++------------------------- 1 file changed, 47 insertions(+), 63 deletions(-) diff --git a/app/src/app-logic.js b/app/src/app-logic.js index ae1e9fc..0669acc 100644 --- a/app/src/app-logic.js +++ b/app/src/app-logic.js @@ -1,69 +1,53 @@ -import { useCallback, useMemo } from 'react' -import { useApi, useAppState, usePath } from '@aragon/api-react' +import { useState } from 'react' +import { useAragonApi, useAppState } from '@aragon/api-react' // import usePanelState from './hooks/usePanelState' -import { noop } from './utils' +// import { noop } from './utils' +import { toDecimals } from './lib/math-utils' +import { toHex } from 'web3-utils' -const PROPOSAL_ID_PATH_RE = /^\/proposal\/([0-9]+)\/?$/ -const NO_PROPOSAL_ID = '-1' +// // Create a new proposal +// export function useCreateProposalAction(onDone = noop) { +// const api = useApi() +// return useCallback( +// title => { +// if (api) { +// // Don't care about response +// api['newProposal(string)'](title).toPromise() +// onDone() +// } +// }, +// [api, onDone] +// ) +// } -function idFromPath(path) { - if (!path) { - return NO_PROPOSAL_ID - } - const matches = path.match(PROPOSAL_ID_PATH_RE) - return matches ? matches[1] : NO_PROPOSAL_ID -} - -// Get the proposal currently selected, or null otherwise. -export function useSelectedProposal(proposals) { - const [path, requestPath] = usePath() - const { isSyncing } = useAppState() - - // The memoized proposal currently selected. - const selectedProposal = useMemo(() => { - const id = idFromPath(path) - - // The `isSyncing` check prevents a proposal to be - // selected until the app state is fully ready. - if (isSyncing || id === NO_PROPOSAL_ID) { - return null - } - - return ( - proposals.find(proposal => Number(proposal.id) === Number(id)) || null - ) - }, [path, isSyncing, proposals]) - - const selectProposal = useCallback( - id => { - requestPath(String(id) === NO_PROPOSAL_ID ? '' : `/proposal/${id}/`) - }, - [requestPath] - ) +// Handles the main logic of the app. +export default function useAppLogic() { + const { api, connectedAccount } = useAragonApi() + const { requestToken, convictionStakes } = useAppState() - return [selectedProposal, selectProposal] -} + const [proposalPanel, setProposalPanel] = useState(false) -// Create a new proposal -export function useCreateProposalAction(onDone = noop) { - const api = useApi() - return useCallback( - title => { - if (api) { - // Don't care about response - api['newProposal(string)'](title).toPromise() - onDone() - } - }, - [api, onDone] - ) -} + const onProposalSubmit = ({ + title, + link, + amount, + beneficiary, + description, + }) => { + const decimals = parseInt(requestToken.decimals) + const decimalAmount = toDecimals(amount.trim(), decimals).toString() + api + .addProposal(title, toHex(link), decimalAmount, beneficiary, description) + .toPromise() + setProposalPanel(false) + } -// Handles the main logic of the app. -export default function useAppLogic() { - const { isSyncing, proposals = [] } = useAppState() + const myStakes = + (convictionStakes && + convictionStakes.filter(({ entity }) => entity === connectedAccount)) || + [] - const [selectedProposal, selectProposal] = useSelectedProposal(proposals) + const myLastStake = [...myStakes].pop() || [] // const newProposalPanel = usePanelState() // const actions = { @@ -72,10 +56,10 @@ export default function useAppLogic() { return { // actions, - isSyncing, // newProposalPanel, - selectProposal, - selectedProposal, - proposals, + onProposalSubmit, + proposalPanel, + setProposalPanel, + myLastStake, } } From c9796dc256cf935dc0146dde22cd4027c92248d4 Mon Sep 17 00:00:00 2001 From: Viviane Date: Wed, 26 Feb 2020 23:17:32 -0300 Subject: [PATCH 10/29] feat(addProposal): Add description field and submit validation --- app/src/components/AddProposalPanel.js | 61 ++++++++++++++++++-------- app/src/script.js | 29 +++++++----- 2 files changed, 61 insertions(+), 29 deletions(-) diff --git a/app/src/components/AddProposalPanel.js b/app/src/components/AddProposalPanel.js index 60b8c92..c34aac2 100644 --- a/app/src/components/AddProposalPanel.js +++ b/app/src/components/AddProposalPanel.js @@ -1,56 +1,81 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { Button, Field, TextInput } from '@aragon/ui' import styled from 'styled-components' const AddProposalPanel = ({ onSubmit }) => { - const [title, setTitle] = useState('') - const [link, setLink] = useState('') - const [amount, setAmount] = useState(0) - const [beneficiary, setBeneficiary] = useState('') - const disabled = false // TODO Disable when empty or invalid fields + const [form, setForm] = useState({ + title: '', + link: '', + amount: 0, + beneficiary: '', + description: '', + }) + const [isDisabled, setStatus] = useState(true) + + const isFormValid = form => form.filter(i => i === '' || i === 0).length === 0 + + useEffect(() => { + const values = Object.values(form) + if (isFormValid(values)) return setStatus(false) + return setStatus(true) + }, [form]) + const onFormSubmit = event => { event.preventDefault() - onSubmit({ title, link, amount, beneficiary }) + onSubmit(form) } + return (
setTitle(event.target.value)} - value={title} + onChange={event => setForm({ ...form, title: event.target.value })} + value={form.title} wide required /> + + + setForm({ ...form, description: event.target.value }) + } + value={form.description} + wide + multiline + /> + setAmount(event.target.value)} + value={form.amount} + onChange={event => setForm({ ...form, amount: event.target.value })} min={0} step="any" required wide /> - + setBeneficiary(event.target.value)} - value={beneficiary} + onChange={event => + setForm({ ...form, beneficiary: event.target.value }) + } + value={form.beneficiary} wide required /> setLink(event.target.value)} - value={link} + onChange={event => setForm({ ...form, link: event.target.value })} + value={form.link} wide /> - diff --git a/app/src/script.js b/app/src/script.js index aa9facb..13d8854 100644 --- a/app/src/script.js +++ b/app/src/script.js @@ -112,21 +112,27 @@ async function initialize([ } // Vault event - if (addressesEqual(address, vaultAddress)) { - if (returnValues.token === requestTokenAddress) { - return { - ...nextState, - requestToken: await getRequestTokenSettings( - returnValues.token, - vault - ), - } + if ( + addressesEqual(address, vaultAddress) && + returnValues.token === requestTokenAddress + ) { + return { + ...nextState, + requestToken: await getRequestTokenSettings(returnValues.token, vault), } } switch (event) { case 'ProposalAdded': { - const { entity, id, title, amount, beneficiary, link } = returnValues + const { + entity, // where this comes from? + id, + title, + amount, + beneficiary, + link, + description, + } = returnValues const newProposal = { id: parseInt(id), name: title, @@ -134,8 +140,9 @@ async function initialize([ requestedAmount: parseInt(amount), creator: entity, beneficiary, + description, } - nextState.proposals.push(newProposal) + nextState = [...nextState, newProposal] break } case 'StakeChanged': { From 92641cb470b97f20265c7bfe2c05deb55a2a9293 Mon Sep 17 00:00:00 2001 From: Viviane Date: Wed, 26 Feb 2020 23:18:41 -0300 Subject: [PATCH 11/29] feat(filter): Add filter hooks --- app/src/hooks/useFilterProposals.js | 46 +++++++++++++++++++++++++++++ app/src/utils.js | 12 ++++++++ 2 files changed, 58 insertions(+) create mode 100644 app/src/hooks/useFilterProposals.js diff --git a/app/src/hooks/useFilterProposals.js b/app/src/hooks/useFilterProposals.js new file mode 100644 index 0000000..52ee28b --- /dev/null +++ b/app/src/hooks/useFilterProposals.js @@ -0,0 +1,46 @@ +import { useState, useEffect, useCallback } from 'react' +import { + getProposalStatus, + PROPOSAL_STATUS_OPEN, + PROPOSAL_STATUS_ACCEPTED, +} from '../utils' + +const NULL_FILTER_STATE = -1 +const STATUS_FILTER_OPEN = 1 +const STATUS_FILTER_CLOSED = 2 + +function testStatusFilter(filter, proposalStatus) { + return ( + filter === NULL_FILTER_STATE || + (filter === STATUS_FILTER_OPEN && + proposalStatus === PROPOSAL_STATUS_OPEN) || + (filter === STATUS_FILTER_CLOSED && + proposalStatus === PROPOSAL_STATUS_ACCEPTED) + ) +} + +const useFilterProposals = proposals => { + const [filteredProposals, setFilteredProposals] = useState(proposals) + const [statusFilter, setStatusFilter] = useState(NULL_FILTER_STATE) + + useEffect(() => { + const filtered = proposals.filter(proposal => { + const proposalStatus = getProposalStatus(proposal) + return testStatusFilter(statusFilter, proposalStatus) + }) + setFilteredProposals(filtered) + }, [statusFilter, setFilteredProposals, proposals]) + + return { + filteredProposals, + proposalStatusFilter: statusFilter, + handleProposalStatusFilterChange: useCallback( + index => { + setStatusFilter(index || NULL_FILTER_STATE) + }, + [setStatusFilter] + ), + } +} + +export default useFilterProposals diff --git a/app/src/utils.js b/app/src/utils.js index 177804c..31adc41 100644 --- a/app/src/utils.js +++ b/app/src/utils.js @@ -1 +1,13 @@ export function noop() {} + +export const PROPOSAL_STATUS_OPEN = 1 +export const PROPOSAL_STATUS_ACCEPTED = 2 + +export function getProposalStatus(proposal) { + switch (proposal.state) { + case PROPOSAL_STATUS_ACCEPTED: + return PROPOSAL_STATUS_ACCEPTED + default: + return PROPOSAL_STATUS_OPEN + } +} From 2e1ab2d394bd5bc33e7a0b9e12a9e4dd8e61d91b Mon Sep 17 00:00:00 2001 From: Viviane Date: Wed, 26 Feb 2020 23:20:01 -0300 Subject: [PATCH 12/29] feat: Move web3-utils to a single file --- app/src/lib/web3-utils.js | 24 ++++++++++++++++++++++++ app/src/web3-utils.js | 30 ------------------------------ 2 files changed, 24 insertions(+), 30 deletions(-) delete mode 100644 app/src/web3-utils.js diff --git a/app/src/lib/web3-utils.js b/app/src/lib/web3-utils.js index a4c308f..8d0a93d 100644 --- a/app/src/lib/web3-utils.js +++ b/app/src/lib/web3-utils.js @@ -8,6 +8,30 @@ export function addressesEqual(first, second) { } export const addressPattern = '(0x)?[0-9a-fA-F]{40}' +const ETH_ADDRESS_SPLIT_REGEX = /(0x[a-fA-F0-9]{40}(?:\b|\.|,|\?|!|;))/g +const ETH_ADDRESS_TEST_REGEX = /(0x[a-fA-F0-9]{40}(?:\b|\.|,|\?|!|;))/g + +export const EMPTY_ADDRESS = '0x0000000000000000000000000000000000000000' + +// Detect Ethereum addresses in a string and transform each part. +// +// `callback` is called on every part with two params: +// - The string of the current part. +// - A boolean indicating if it is an address. +// +export function transformAddresses(str, callback) { + return str + .split(ETH_ADDRESS_SPLIT_REGEX) + .map((part, index) => + callback(part, ETH_ADDRESS_TEST_REGEX.test(part), index) + ) +} + +export function addressesEqualNoSum(first, second) { + first = first && first.toLowerCase() + second = second && second.toLowerCase() + return first === second +} // Re-export some web3-utils functions export { isAddress, toChecksumAddress, toUtf8 } from 'web3-utils' diff --git a/app/src/web3-utils.js b/app/src/web3-utils.js deleted file mode 100644 index c9d97c6..0000000 --- a/app/src/web3-utils.js +++ /dev/null @@ -1,30 +0,0 @@ -const ETH_ADDRESS_SPLIT_REGEX = /(0x[a-fA-F0-9]{40}(?:\b|\.|,|\?|!|;))/g -const ETH_ADDRESS_TEST_REGEX = /(0x[a-fA-F0-9]{40}(?:\b|\.|,|\?|!|;))/g - -export const EMPTY_ADDRESS = '0x0000000000000000000000000000000000000000' - -// Detect Ethereum addresses in a string and transform each part. -// -// `callback` is called on every part with two params: -// - The string of the current part. -// - A boolean indicating if it is an address. -// -export function transformAddresses(str, callback) { - return str - .split(ETH_ADDRESS_SPLIT_REGEX) - .map((part, index) => - callback(part, ETH_ADDRESS_TEST_REGEX.test(part), index) - ) -} - -/** - * Check address equality without checksums - * @param {string} first First address - * @param {string} second Second address - * @returns {boolean} Address equality - */ -export function addressesEqual(first, second) { - first = first && first.toLowerCase() - second = second && second.toLowerCase() - return first === second -} From 3ea7ab95d9d3582fd856691af6a906e9219a9688 Mon Sep 17 00:00:00 2001 From: Viviane Date: Wed, 26 Feb 2020 23:20:13 -0300 Subject: [PATCH 13/29] feat(selectedProposal): Move useSelectedProposal to it's own hook file --- app/src/hooks/useSelectedProposal.js | 43 ++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 app/src/hooks/useSelectedProposal.js diff --git a/app/src/hooks/useSelectedProposal.js b/app/src/hooks/useSelectedProposal.js new file mode 100644 index 0000000..65afff3 --- /dev/null +++ b/app/src/hooks/useSelectedProposal.js @@ -0,0 +1,43 @@ +import { useCallback, useMemo } from 'react' +import { useAppState, usePath } from '@aragon/api-react' + +const PROPOSAL_ID_PATH_RE = /^\/proposal\/([0-9]+)\/?$/ +const NO_PROPOSAL_ID = '-1' + +function idFromPath(path) { + if (!path) { + return NO_PROPOSAL_ID + } + const matches = path.match(PROPOSAL_ID_PATH_RE) + return matches ? matches[1] : NO_PROPOSAL_ID +} + +// Get the proposal currently selected, or null otherwise. +export default function useSelectedProposal(proposals) { + const [path, requestPath] = usePath() + const { isSyncing } = useAppState() + + // The memoized proposal currently selected. + const selectedProposal = useMemo(() => { + const id = idFromPath(path) + + // The `isSyncing` check prevents a proposal to be + // selected until the app state is fully ready. + if (isSyncing || id === NO_PROPOSAL_ID) { + return null + } + + return ( + proposals.find(proposal => Number(proposal.id) === Number(id)) || null + ) + }, [path, isSyncing, proposals]) + + const selectProposal = useCallback( + id => { + requestPath(String(id) === NO_PROPOSAL_ID ? '' : `/proposal/${id}/`) + }, + [requestPath] + ) + + return [selectedProposal, selectProposal] +} From 718b7e8a8ba8e489964cb4283481575ba6fce0a1 Mon Sep 17 00:00:00 2001 From: Viviane Date: Wed, 26 Feb 2020 23:21:14 -0300 Subject: [PATCH 14/29] feat(proposals): Add filter, update UI and make all components functional All components are dummy components, only rendering data that is passed to them. --- app/src/screens/Proposals.js | 240 ++++++++++++++++++++--------------- 1 file changed, 139 insertions(+), 101 deletions(-) diff --git a/app/src/screens/Proposals.js b/app/src/screens/Proposals.js index 103a1c8..6c3598d 100644 --- a/app/src/screens/Proposals.js +++ b/app/src/screens/Proposals.js @@ -1,123 +1,161 @@ import React from 'react' import { - // Bar, DataView, - // DropDown, Link, - // Tag, - // GU, - // textStyle, - // useLayout, - useTheme, + GU, Text, + Box, + DropDown, + Tag, + textStyle, + useTheme, } from '@aragon/ui' -import { ConvictionBar, ConvictionTrend } from '../components/ConvictionVisuals' +import styled from 'styled-components' + import Balance from '../components/Balance' -import { useAragonApi } from '@aragon/api-react' +import { ConvictionBar, ConvictionTrend } from '../components/ConvictionVisuals' + +const Wrapper = styled.div` + display: grid; + grid-template-columns: auto; + grid-column-gap: ${2.5 * GU}px; + @media (min-width: 768px) { + grid-template-columns: 160px auto; + } + min-height: 100vh; +` const Proposals = React.memo(function Proposals({ - // proposals, + proposals, selectProposal, // executionTargets, filteredProposals, - // proposalStatusFilter, - // handleProposalStatusFilterChange, + proposalStatusFilter, + handleProposalStatusFilterChange, + myLastStake, + requestToken, }) { - // const theme = useTheme() - // const { layoutName } = useLayout() - return ( - - {/* {layoutName !== 'small' && ( - -
+
+ + + + {myLastStake && myLastStake.tokensStaked > 0 && ( + + id === myLastStake.proposal)[0] + } + stake={myLastStake} + /> + + )} +
+
+ [ + , + , + , + , + ]} + tableRowHeight={14 * GU} + heading={ + + } + /> +
+ + ) +}) + +const ProposalInfo = ({ proposal, stake, tokenSymbol }) => ( +
+ + {`✓ Supported with ${stake.tokensStaked} ${tokenSymbol}`} + +
+) + +const Filters = ({ + proposalStatusFilter, + handleProposalStatusFilterChange, + proposals, +}) => ( +
+ + All + - - All - - - -
, - 'Open', - 'Closed', - ]} - width="128px" - /> -
-
- )} */} - [ - , - , - , - , - ]} - /> -
- ) -}) + + + , + 'Open', + 'Closed', + ]} + width="128px" + /> + +) -const IdAndTitle = ({ id, name, selectProposal }) => { - const theme = useTheme() - return ( - selectProposal(id)}> - #{id}{' '} - {name} - - ) -} +const IdAndTitle = ({ id, name, selectProposal }) => ( + selectProposal(id)}> + #{id}{' '} + {name} + +) -const Amount = ({ requestedAmount = 0 }) => { - const { - appState: { - requestToken: { symbol, decimals, verified }, - }, - } = useAragonApi() - return ( -
- -
- ) -} +const Amount = ({ + requestedAmount = 0, + requestToken: { symbol, decimals, verified }, +}) => ( +
+ +
+) export default Proposals From b42dc975026f076cffbb79dcb8ccb199538fdf09 Mon Sep 17 00:00:00 2001 From: Viviane Date: Wed, 26 Feb 2020 23:23:23 -0300 Subject: [PATCH 15/29] feat(detail): Add more info and fix UI according to figma --- app/src/screens/ProposalDetail.js | 144 +++++++++++++++++++----------- 1 file changed, 93 insertions(+), 51 deletions(-) diff --git a/app/src/screens/ProposalDetail.js b/app/src/screens/ProposalDetail.js index 6e221d5..d25d1f5 100644 --- a/app/src/screens/ProposalDetail.js +++ b/app/src/screens/ProposalDetail.js @@ -4,68 +4,86 @@ import { Bar, Box, GU, - Split, Text, textStyle, useLayout, useTheme, + Link, } from '@aragon/ui' +import styled from 'styled-components' import { useConnectedAccount } from '@aragon/api-react' import LocalIdentityBadge from '../components/LocalIdentityBadge/LocalIdentityBadge' -import { addressesEqual } from '../web3-utils' +import { addressesEqualNoSum as addressesEqual } from '../lib/web3-utils' const DEFAULT_DESCRIPTION = 'No additional description has been provided for this proposal.' +const Wrapper = styled.div` + display: flex; + flex-direction: column; + width: 100%; + min-height: 100vh; +` +const H2 = styled.h2` + ${textStyle('label2')}; + color: ${props => props.color}; + margin-bottom: ${1.5 * GU}px; +` + +const InfoWrapper = styled.div` + display: grid; + grid-template-row: auto; + grid-row-gap: ${3 * GU}px; +` + +const Progress = styled.div` + width: 100%; +` + function ProposalDetail({ proposal, onBack }) { const theme = useTheme() const { layoutName } = useLayout() const connectedAccount = useConnectedAccount() - const { id, title, description, creator } = proposal + const { id, name, description, creator, beneficiary, link } = proposal return ( - + - -
+
+
+

+ #{id} {name} +

+
-

- Proposal #{id} - {title} -

-
+
-

- Description -

+

Description

+ {link && ( +
+

Links

+ + Read more + +
+ )} +
+
-

Status

+
+
+

Created By

+
- Created By - + +
+
+
+

Recipient

-
-
- - } - secondary={Dummy} - /> - + + + + +

Conviction Progress

+
+
+ +
) } From ef5c33eb68ccc171b7657da00927002501167f9c Mon Sep 17 00:00:00 2001 From: Viviane Date: Wed, 26 Feb 2020 23:24:43 -0300 Subject: [PATCH 16/29] feat(app): Comp only passes state/logic as props Components, state and logic are only rendered and passed through here. --- app/src/App.js | 143 +++++++++++++++++++------------------------------ 1 file changed, 54 insertions(+), 89 deletions(-) diff --git a/app/src/App.js b/app/src/App.js index a95d7e6..92b5393 100644 --- a/app/src/App.js +++ b/app/src/App.js @@ -1,14 +1,13 @@ -import React, { useState, useCallback } from 'react' -import { useAragonApi, useGuiStyle } from '@aragon/api-react' +import React, { useCallback } from 'react' +import { useGuiStyle, useAppState } from '@aragon/api-react' import { Main, Button, SidePanel, - Box, - // Tag, SyncIndicator, IconPlus, Header, + GU, useLayout, } from '@aragon/ui' import styled from 'styled-components' @@ -16,54 +15,43 @@ import styled from 'styled-components' import NoProposals from './screens/NoProposals' import ProposalDetail from './screens/ProposalDetail' import Proposals from './screens/Proposals' -// import ProposalDetail from './components/ProposalDetail' import AddProposalPanel from './components/AddProposalPanel' -import Balance from './components/Balance' +// import ProposalDetail from './components/ProposalDetail' import useAppLogic from './app-logic' -import { toDecimals } from './lib/math-utils' -import { toHex } from 'web3-utils' +import useFilterProposals from './hooks/useFilterProposals' +import useSelectedProposal from './hooks/useSelectedProposal' + +const Layout = styled.div` + display: flex; + justify-content: center; + margin-top: ${2.5 * GU}px; +` const App = React.memo(function App() { const { - isSyncing, - selectProposal, - selectedProposal, - proposals, + setProposalPanel, + proposalPanel, + onProposalSubmit, + myLastStake, } = useAppLogic() - const handleBack = useCallback(() => selectProposal(-1), [selectProposal]) + + const { proposals = [], isSyncing, requestToken } = useAppState() const { layoutName } = useLayout() const compactMode = layoutName === 'small' - const { - api, - appState, - // connectedAccount - } = useAragonApi() - const { - // convictionStakes, - requestToken, - } = appState - const filteredProposals = proposals.filter(({ executed }) => !executed) - - const [proposalPanel, setProposalPanel] = useState(false) - const onProposalSubmit = ({ title, link, amount, beneficiary }) => { - const decimals = parseInt(requestToken.decimals) - const decimalAmount = toDecimals(amount.trim(), decimals).toString() - api.addProposal(title, toHex(link), decimalAmount, beneficiary).toPromise() - setProposalPanel(false) - } - - // const myStakes = - // (convictionStakes && - // convictionStakes.filter(({ entity }) => entity === connectedAccount)) || - // [] + const [selectedProposal, selectProposal] = useSelectedProposal(proposals) + const handleBack = useCallback(() => selectProposal(-1), [selectProposal]) - // const myLastStake = [...myStakes].pop() || {} + const { + filteredProposals, + proposalStatusFilter, + handleProposalStatusFilterChange, + } = useFilterProposals(proposals) return ( - + {proposals.length === 0 && (
)} {proposals.length > 0 && ( - +
- -
- - - - {/* {myLastStake.tokensStaked > 0 && ( - - id === myLastStake.proposal)[0] - } - stake={myLastStake} - /> - - )} */} -
- {selectedProposal ? ( - - ) : ( -
- -
- )} -
+ {selectedProposal ? ( + + ) : ( + + )} setProposalPanel(false)} > - +
)} -
+ ) }) -// const ProposalInfo = ({ proposal, stake }) => { -// const { -// appState: { -// stakeToken: { tokenSymbol }, -// }, -// } = useAragonApi() -// return ( -//
-// -// {`✓ Supported with ${stake.tokensStaked} ${tokenSymbol}`} -// -//
-// ) -// } - -const Wrapper = styled.div` - display: flex; -` - export default () => { const { appearance } = useGuiStyle() return ( -
+
) From d9d33cf5dee07d73555db62e8d5952949bffbcb4 Mon Sep 17 00:00:00 2001 From: Viviane Date: Thu, 27 Feb 2020 01:32:20 -0300 Subject: [PATCH 17/29] fix(packages): Update libraries in correct package.json --- app/package.json | 6 +++--- package.json | 5 +---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/package.json b/app/package.json index 7bcb93c..aab67b7 100644 --- a/app/package.json +++ b/app/package.json @@ -3,9 +3,9 @@ "version": "1.0.0", "main": "src/index.js", "dependencies": { - "@aragon/api": "^2.0.0-beta.7", - "@aragon/api-react": "^2.0.0-beta.6", - "@aragon/ui": "^1.0.0-alpha.26", + "@aragon/api": "^2.0.0-beta.9", + "@aragon/api-react": "^2.0.0-beta.9", + "@aragon/ui": "^1.3.1", "core-js": "^3.1.4", "react": "^16.8.6", "react-dom": "^16.8.6", diff --git a/package.json b/package.json index 7479b91..2b1dc83 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,10 @@ "version": "1.0.0", "description": "", "dependencies": { - "@aragon/api": "^2.0.0-beta.9", - "@aragon/api-react": "^2.0.0-beta.9", "@aragon/apps-shared-minime": "^1.0.1", "@aragon/apps-token-manager": "2.1.0", "@aragon/apps-vault": "^4.1.0", - "@aragon/os": "^4.2.1", - "@aragon/ui": "^1.3.1" + "@aragon/os": "^4.3.0" }, "devDependencies": { "@aragon/cli": "^6.0.0", From f8b1e22584b3384a4b997f956baa47de68f19612 Mon Sep 17 00:00:00 2001 From: Viviane Date: Thu, 27 Feb 2020 01:33:16 -0300 Subject: [PATCH 18/29] fix(addProposal): Remove description field so new proposal is added to the list --- app/src/app-logic.js | 6 ++---- app/src/script.js | 11 +++++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/src/app-logic.js b/app/src/app-logic.js index 0669acc..6ae6bda 100644 --- a/app/src/app-logic.js +++ b/app/src/app-logic.js @@ -32,13 +32,11 @@ export default function useAppLogic() { link, amount, beneficiary, - description, + // description, }) => { const decimals = parseInt(requestToken.decimals) const decimalAmount = toDecimals(amount.trim(), decimals).toString() - api - .addProposal(title, toHex(link), decimalAmount, beneficiary, description) - .toPromise() + api.addProposal(title, toHex(link), decimalAmount, beneficiary).toPromise() setProposalPanel(false) } diff --git a/app/src/script.js b/app/src/script.js index 13d8854..3b5129b 100644 --- a/app/src/script.js +++ b/app/src/script.js @@ -125,13 +125,13 @@ async function initialize([ switch (event) { case 'ProposalAdded': { const { - entity, // where this comes from? + entity, id, title, amount, beneficiary, link, - description, + // description, } = returnValues const newProposal = { id: parseInt(id), @@ -140,9 +140,12 @@ async function initialize([ requestedAmount: parseInt(amount), creator: entity, beneficiary, - description, + // description, + } + nextState = { + ...nextState, + proposals: [...nextState.proposals, newProposal], } - nextState = [...nextState, newProposal] break } case 'StakeChanged': { From 44805e9d5bbc9060d886f7b6b2d6cfde0e5b567e Mon Sep 17 00:00:00 2001 From: Viviane Date: Thu, 27 Feb 2020 02:01:29 -0300 Subject: [PATCH 19/29] feat: Change expression support -> vote --- app/src/components/ConvictionVisuals.js | 2 +- app/src/screens/Proposals.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/components/ConvictionVisuals.js b/app/src/components/ConvictionVisuals.js index de96fdd..e2c1dd2 100644 --- a/app/src/components/ConvictionVisuals.js +++ b/app/src/components/ConvictionVisuals.js @@ -140,7 +140,7 @@ export function ConvictionButton({ proposal, onStake, onWithdraw, onExecute }) { ) : ( ) } diff --git a/app/src/screens/Proposals.js b/app/src/screens/Proposals.js index 6c3598d..34d6a0f 100644 --- a/app/src/screens/Proposals.js +++ b/app/src/screens/Proposals.js @@ -89,7 +89,7 @@ const Proposals = React.memo(function Proposals({ const ProposalInfo = ({ proposal, stake, tokenSymbol }) => (
- {`✓ Supported with ${stake.tokensStaked} ${tokenSymbol}`} + {`✓ Voted with ${stake.tokensStaked} ${tokenSymbol}`}
) From 01cb48e7595b5e94089b217d73e0629057d67d92 Mon Sep 17 00:00:00 2001 From: Viviane Date: Thu, 27 Feb 2020 02:02:01 -0300 Subject: [PATCH 20/29] feat(detail): Add 'vote for this proposal' btn in ProposalDetail --- app/src/screens/ProposalDetail.js | 34 ++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/app/src/screens/ProposalDetail.js b/app/src/screens/ProposalDetail.js index d25d1f5..af24b75 100644 --- a/app/src/screens/ProposalDetail.js +++ b/app/src/screens/ProposalDetail.js @@ -11,8 +11,12 @@ import { Link, } from '@aragon/ui' import styled from 'styled-components' -import { useConnectedAccount } from '@aragon/api-react' +import { useAragonApi } from '@aragon/api-react' import LocalIdentityBadge from '../components/LocalIdentityBadge/LocalIdentityBadge' +import { + ConvictionCountdown, + ConvictionButton, +} from '../components/ConvictionVisuals' import { addressesEqualNoSum as addressesEqual } from '../lib/web3-utils' const DEFAULT_DESCRIPTION = @@ -43,7 +47,7 @@ const Progress = styled.div` function ProposalDetail({ proposal, onBack }) { const theme = useTheme() const { layoutName } = useLayout() - const connectedAccount = useConnectedAccount() + const { api, connectedAccount } = useAragonApi() const { id, name, description, creator, beneficiary, link } = proposal @@ -92,18 +96,33 @@ function ProposalDetail({ proposal, onBack }) { {description || DEFAULT_DESCRIPTION}
- {link && ( -
-

Links

+
+

Links

+ {link ? ( Read more -
- )} + ) : ( + + No link provided. + + )} +
+ api.stakeAllToProposal(id).toPromise()} + onWithdraw={() => api.withdrawAllFromProposal(id).toPromise()} + onExecute={() => api.executeProposal(id, true).toPromise()} + />

Status

+

Created By

@@ -144,6 +163,7 @@ function ProposalDetail({ proposal, onBack }) {

Conviction Progress

+ {/* */}
From 7ce295098b88806088c6cf1a70645cb6ec6ceb1c Mon Sep 17 00:00:00 2001 From: Viviane Date: Thu, 27 Feb 2020 02:52:19 -0300 Subject: [PATCH 21/29] feat(proposals): Add description to dataview and fix layout details --- app/src/screens/Proposals.js | 65 ++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/app/src/screens/Proposals.js b/app/src/screens/Proposals.js index 34d6a0f..b2afe21 100644 --- a/app/src/screens/Proposals.js +++ b/app/src/screens/Proposals.js @@ -25,6 +25,9 @@ const Wrapper = styled.div` min-height: 100vh; ` +const DEFAULT_DESCRIPTION = + 'No additional description has been provided for this proposal.' + const Proposals = React.memo(function Proposals({ proposals, selectProposal, @@ -55,19 +58,36 @@ const Proposals = React.memo(function Proposals({
[ - , + renderEntry={({ + id, + name, + description = DEFAULT_DESCRIPTION, + requestedAmount, + ...proposal + }) => [ + , , - , +
+ +
, , ]} tableRowHeight={14 * GU} @@ -109,8 +129,8 @@ const Filters = ({ `} > ) -const IdAndTitle = ({ id, name, selectProposal }) => ( - selectProposal(id)}> - #{id}{' '} - {name} - +const IdAndTitle = ({ id, name, description, selectProposal }) => ( +
+ selectProposal(id)}> + #{id}{' '} + {name} + + + {description.slice(0, 29) + '...'} + +
) const Amount = ({ From 4366822eceb6ae782214279085dff4f3a3d6c58c Mon Sep 17 00:00:00 2001 From: Viviane Date: Thu, 27 Feb 2020 03:09:08 -0300 Subject: [PATCH 22/29] feat(share): Add share btn to proposal detail page --- app/src/screens/ProposalDetail.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/src/screens/ProposalDetail.js b/app/src/screens/ProposalDetail.js index af24b75..b8531f3 100644 --- a/app/src/screens/ProposalDetail.js +++ b/app/src/screens/ProposalDetail.js @@ -9,6 +9,8 @@ import { useLayout, useTheme, Link, + IconShare, + Button, } from '@aragon/ui' import styled from 'styled-components' import { useAragonApi } from '@aragon/api-react' @@ -53,8 +55,23 @@ function ProposalDetail({ proposal, onBack }) { return ( - + + ) : ( ) } diff --git a/app/src/screens/ProposalDetail.js b/app/src/screens/ProposalDetail.js index b8531f3..0791855 100644 --- a/app/src/screens/ProposalDetail.js +++ b/app/src/screens/ProposalDetail.js @@ -9,26 +9,26 @@ import { useLayout, useTheme, Link, - IconShare, - Button, } from '@aragon/ui' import styled from 'styled-components' import { useAragonApi } from '@aragon/api-react' import LocalIdentityBadge from '../components/LocalIdentityBadge/LocalIdentityBadge' +import Balance from '../components/Balance' import { ConvictionCountdown, ConvictionButton, } from '../components/ConvictionVisuals' import { addressesEqualNoSum as addressesEqual } from '../lib/web3-utils' -const DEFAULT_DESCRIPTION = - 'No additional description has been provided for this proposal.' - const Wrapper = styled.div` - display: flex; - flex-direction: column; - width: 100%; + display: grid; + grid-template-columns: auto; + grid-column-gap: ${2.5 * GU}px; + @media (min-width: 768px) { + grid-template-columns: auto ${25 * GU}px; + } min-height: 100vh; + width: 100%; ` const H2 = styled.h2` ${textStyle('label2')}; @@ -36,59 +36,36 @@ const H2 = styled.h2` margin-bottom: ${1.5 * GU}px; ` -const InfoWrapper = styled.div` - display: grid; - grid-template-row: auto; - grid-row-gap: ${3 * GU}px; -` - const Progress = styled.div` width: 100%; ` -function ProposalDetail({ proposal, onBack }) { +function ProposalDetail({ proposal, onBack, requestToken }) { const theme = useTheme() const { layoutName } = useLayout() const { api, connectedAccount } = useAragonApi() - const { id, name, description, creator, beneficiary, link } = proposal + const { id, name, creator, beneficiary, link } = proposal return ( - - -
+
+ + + + + + +
) } From cfb975f73f5bd0d117afd529890778fcd232de9b Mon Sep 17 00:00:00 2001 From: Viviane Date: Thu, 27 Feb 2020 11:10:49 -0300 Subject: [PATCH 24/29] fix(description): Remove description field from everywhere --- app/src/App.js | 2 +- app/src/app-logic.js | 8 +---- app/src/components/AddProposalPanel.js | 13 +------- app/src/screens/Proposals.js | 45 ++++---------------------- app/src/script.js | 11 +------ 5 files changed, 11 insertions(+), 68 deletions(-) diff --git a/app/src/App.js b/app/src/App.js index 9942fcb..2903c21 100644 --- a/app/src/App.js +++ b/app/src/App.js @@ -107,7 +107,7 @@ const App = React.memo(function App() { /> )} setProposalPanel(false)} > diff --git a/app/src/app-logic.js b/app/src/app-logic.js index 6ae6bda..aef01a1 100644 --- a/app/src/app-logic.js +++ b/app/src/app-logic.js @@ -27,13 +27,7 @@ export default function useAppLogic() { const [proposalPanel, setProposalPanel] = useState(false) - const onProposalSubmit = ({ - title, - link, - amount, - beneficiary, - // description, - }) => { + const onProposalSubmit = ({ title, link, amount, beneficiary }) => { const decimals = parseInt(requestToken.decimals) const decimalAmount = toDecimals(amount.trim(), decimals).toString() api.addProposal(title, toHex(link), decimalAmount, beneficiary).toPromise() diff --git a/app/src/components/AddProposalPanel.js b/app/src/components/AddProposalPanel.js index c34aac2..9f4ea75 100644 --- a/app/src/components/AddProposalPanel.js +++ b/app/src/components/AddProposalPanel.js @@ -8,7 +8,6 @@ const AddProposalPanel = ({ onSubmit }) => { link: '', amount: 0, beneficiary: '', - description: '', }) const [isDisabled, setStatus] = useState(true) @@ -35,16 +34,6 @@ const AddProposalPanel = ({ onSubmit }) => { required />
- - - setForm({ ...form, description: event.target.value }) - } - value={form.description} - wide - multiline - /> - { diff --git a/app/src/screens/Proposals.js b/app/src/screens/Proposals.js index b2afe21..0f09d7d 100644 --- a/app/src/screens/Proposals.js +++ b/app/src/screens/Proposals.js @@ -25,9 +25,6 @@ const Wrapper = styled.div` min-height: 100vh; ` -const DEFAULT_DESCRIPTION = - 'No additional description has been provided for this proposal.' - const Proposals = React.memo(function Proposals({ proposals, selectProposal, @@ -64,19 +61,8 @@ const Proposals = React.memo(function Proposals({ { label: 'Trend', priority: 5, align: 'start' }, ]} entries={filteredProposals} - renderEntry={({ - id, - name, - description = DEFAULT_DESCRIPTION, - requestedAmount, - ...proposal - }) => [ - , + renderEntry={({ id, name, requestedAmount, ...proposal }) => [ + , ) -const IdAndTitle = ({ id, name, description, selectProposal }) => ( -
- selectProposal(id)}> - #{id}{' '} - {name} - - - {description.slice(0, 29) + '...'} - -
+const IdAndTitle = ({ id, name, selectProposal }) => ( + selectProposal(id)}> + #{id}{' '} + {name} + ) const Amount = ({ diff --git a/app/src/script.js b/app/src/script.js index 3b5129b..9cc791a 100644 --- a/app/src/script.js +++ b/app/src/script.js @@ -124,15 +124,7 @@ async function initialize([ switch (event) { case 'ProposalAdded': { - const { - entity, - id, - title, - amount, - beneficiary, - link, - // description, - } = returnValues + const { entity, id, title, amount, beneficiary, link } = returnValues const newProposal = { id: parseInt(id), name: title, @@ -140,7 +132,6 @@ async function initialize([ requestedAmount: parseInt(amount), creator: entity, beneficiary, - // description, } nextState = { ...nextState, From 2388b02ae8143711aed3e917aca3164efe594ffc Mon Sep 17 00:00:00 2001 From: Viviane Date: Thu, 27 Feb 2020 11:48:39 -0300 Subject: [PATCH 25/29] feat(data view): Add proposals title and fix filter name --- app/src/screens/Proposals.js | 75 +++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/app/src/screens/Proposals.js b/app/src/screens/Proposals.js index 0f09d7d..053df8d 100644 --- a/app/src/screens/Proposals.js +++ b/app/src/screens/Proposals.js @@ -107,39 +107,54 @@ const Filters = ({ }) => (
- - All - - - -
, - 'Open', - 'Closed', - ]} - width="128px" - /> +

+ Proposals +

+
+ Filter by + + All + + + +
, + 'Open', + 'Closed', + ]} + width="128px" + /> + ) From 5023a4c61f1c17e12c5ada571eba45bf4be73a8a Mon Sep 17 00:00:00 2001 From: Viviane Date: Thu, 27 Feb 2020 11:49:21 -0300 Subject: [PATCH 26/29] feat(detail): Add conviction bar to proposal detail page --- app/src/screens/ProposalDetail.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/app/src/screens/ProposalDetail.js b/app/src/screens/ProposalDetail.js index 0791855..9eecad6 100644 --- a/app/src/screens/ProposalDetail.js +++ b/app/src/screens/ProposalDetail.js @@ -17,6 +17,7 @@ import Balance from '../components/Balance' import { ConvictionCountdown, ConvictionButton, + ConvictionBar, } from '../components/ConvictionVisuals' import { addressesEqualNoSum as addressesEqual } from '../lib/web3-utils' @@ -45,7 +46,7 @@ function ProposalDetail({ proposal, onBack, requestToken }) { const { layoutName } = useLayout() const { api, connectedAccount } = useAragonApi() - const { id, name, creator, beneficiary, link } = proposal + const { id, name, creator, beneficiary, link, requestedAmount } = proposal return ( @@ -79,6 +80,10 @@ function ProposalDetail({ proposal, onBack, requestToken }) { grid-gap: ${layoutName !== 'small' ? 5 * GU : 2.5 * GU}px; `} > +

Links

{link ? ( @@ -129,7 +134,7 @@ function ProposalDetail({ proposal, onBack, requestToken }) {

Conviction Progress

- {/* */} +
( +
+

Amount

+ +
+) + export default ProposalDetail From d71e184f30d2dcf7199d3ddbecc14291d948a264 Mon Sep 17 00:00:00 2001 From: Viviane Date: Thu, 27 Feb 2020 11:49:36 -0300 Subject: [PATCH 27/29] feat(balance): Update balance token layout --- app/src/components/BalanceToken.js | 59 ++++++++---------------------- 1 file changed, 16 insertions(+), 43 deletions(-) diff --git a/app/src/components/BalanceToken.js b/app/src/components/BalanceToken.js index ee1eae2..f7a1287 100644 --- a/app/src/components/BalanceToken.js +++ b/app/src/components/BalanceToken.js @@ -1,6 +1,6 @@ import React from 'react' import styled from 'styled-components' -import { theme, breakpoint } from '@aragon/ui' +import { theme, useTheme, GU, Text } from '@aragon/ui' import { formatTokenAmount } from '../lib/utils' const splitAmount = amount => { @@ -14,53 +14,26 @@ const splitAmount = amount => { } const BalanceToken = ({ amount, symbol, verified, convertedAmount = -1 }) => ( - - {symbol || '?'} - - {splitAmount(amount.toFixed(3))} - - {convertedAmount >= 0 - ? `$${formatTokenAmount(convertedAmount.toFixed(2))}` - : '−'} - - - + +
+ + {splitAmount(amount.toFixed(3))}{' '} + + {symbol || '?'} +
+ + {convertedAmount >= 0 + ? `(${formatTokenAmount(convertedAmount.toFixed(2))})` + : '(−)'} + +
) const Wrap = styled.div` - text-align: right; - - ${breakpoint( - 'medium', - ` - text-align: left; - ` - )}; -` - -const Token = styled.div` display: flex; align-items: center; - text-transform: uppercase; - font-size: 28px; - color: ${theme.textSecondary}; - img { - margin-right: 10px; - } - - ${breakpoint( - 'medium', - ` - font-size: 14px; - ` - )} -` - -const Amount = styled.div` - font-size: 26px; - .fractional { - font-size: 14px; - } + justify-content: center; + flex-direction: column; ` const ConvertedAmount = styled.div` From 2d0098c963b89eba7c3ee57ef78bd055d8b8677d Mon Sep 17 00:00:00 2001 From: Viviane Date: Thu, 27 Feb 2020 12:10:26 -0300 Subject: [PATCH 28/29] feat(new proposal): Side panel appears even without proposals --- app/src/App.js | 21 ++++++++------------- app/src/utils.js | 2 -- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/app/src/App.js b/app/src/App.js index 2903c21..0de4e2f 100644 --- a/app/src/App.js +++ b/app/src/App.js @@ -29,12 +29,7 @@ const Layout = styled.div` ` const App = React.memo(function App() { - const { - setProposalPanel, - proposalPanel, - onProposalSubmit, - myLastStake, - } = useAppLogic() + const { setProposalPanel, proposalPanel, onProposalSubmit } = useAppLogic() const { proposals = [], isSyncing, requestToken } = useAppState() @@ -106,15 +101,15 @@ const App = React.memo(function App() { requestToken={requestToken} /> )} - setProposalPanel(false)} - > - - )} + setProposalPanel(false)} + > + + ) }) diff --git a/app/src/utils.js b/app/src/utils.js index 31adc41..a93cf8c 100644 --- a/app/src/utils.js +++ b/app/src/utils.js @@ -1,5 +1,3 @@ -export function noop() {} - export const PROPOSAL_STATUS_OPEN = 1 export const PROPOSAL_STATUS_ACCEPTED = 2 From 7504b8df07626b0f7b9a0212da7686259b3e0bb8 Mon Sep 17 00:00:00 2001 From: Viviane Date: Thu, 27 Feb 2020 12:10:59 -0300 Subject: [PATCH 29/29] feat(no proposal): Remove NoProposal screen and stay with default from DataView --- app/src/App.js | 90 +++++++++++++--------------------- app/src/screens/NoProposals.js | 48 ------------------ app/src/screens/Proposals.js | 10 ++++ 3 files changed, 45 insertions(+), 103 deletions(-) delete mode 100644 app/src/screens/NoProposals.js diff --git a/app/src/App.js b/app/src/App.js index 0de4e2f..8f9af75 100644 --- a/app/src/App.js +++ b/app/src/App.js @@ -12,7 +12,6 @@ import { } from '@aragon/ui' import styled from 'styled-components' -import NoProposals from './screens/NoProposals' import ProposalDetail from './screens/ProposalDetail' import Proposals from './screens/Proposals' import AddProposalPanel from './components/AddProposalPanel' @@ -47,62 +46,43 @@ const App = React.memo(function App() { return ( - {proposals.length === 0 && ( -
- setProposalPanel(true)} - isSyncing={isSyncing} +
+ +
setProposalPanel(true)} + label="New proposal" + icon={} + display={compactMode ? 'icon' : 'label'} + /> + ) + } + /> + {selectedProposal ? ( + -
- )} - {proposals.length > 0 && ( -
- -
setProposalPanel(true)} - label="New proposal" - icon={} - display={compactMode ? 'icon' : 'label'} - /> - ) - } + ) : ( + - {selectedProposal ? ( - - ) : ( - - )} -
- )} + )} +
- - Syncing… - - ) : ( - 'No proposals here!' - ) - } - action={ - - } - illustration={ - No proposal here - } - /> - ) -}) - -export default NoProposals diff --git a/app/src/screens/Proposals.js b/app/src/screens/Proposals.js index 053df8d..0ba2654 100644 --- a/app/src/screens/Proposals.js +++ b/app/src/screens/Proposals.js @@ -60,6 +60,16 @@ const Proposals = React.memo(function Proposals({ { label: 'Conviction progress', priority: 2, align: 'start' }, { label: 'Trend', priority: 5, align: 'start' }, ]} + statusEmpty={ +

+ No proposals yet! +

+ } entries={filteredProposals} renderEntry={({ id, name, requestedAmount, ...proposal }) => [ ,