diff --git a/src/routes/safe/components/Settings/Advanced/dataFetcher.ts b/src/routes/safe/components/Settings/Advanced/dataFetcher.ts index bbf8cf3c..8ef9b4f5 100644 --- a/src/routes/safe/components/Settings/Advanced/dataFetcher.ts +++ b/src/routes/safe/components/Settings/Advanced/dataFetcher.ts @@ -1,11 +1,13 @@ -import { List, Set } from 'immutable' +import { List } from 'immutable' export const MODULES_TABLE_ADDRESS_ID = 'address' export const MODULES_TABLE_ACTIONS_ID = 'actions' -export const getModuleData = (modules: Set): List<{ [MODULES_TABLE_ADDRESS_ID]: string }> => { - return modules.toList().map((module) => ({ - [MODULES_TABLE_ADDRESS_ID]: module, +export const getModuleData = ( + modulesList: List<[string, string]>, +): List<{ [MODULES_TABLE_ADDRESS_ID]: [string, string] }> => { + return modulesList.map((modules) => ({ + [MODULES_TABLE_ADDRESS_ID]: modules, })) } diff --git a/src/routes/safe/components/Settings/Advanced/index.tsx b/src/routes/safe/components/Settings/Advanced/index.tsx index e0ee038a..aeb1fdb8 100644 --- a/src/routes/safe/components/Settings/Advanced/index.tsx +++ b/src/routes/safe/components/Settings/Advanced/index.tsx @@ -1,8 +1,8 @@ -import { Button, GenericModal, Text, Title, theme } from '@gnosis.pm/safe-react-components' +import { Button, Text, Title, theme } from '@gnosis.pm/safe-react-components' import { makeStyles } from '@material-ui/core/styles' import TableContainer from '@material-ui/core/TableContainer' import cn from 'classnames' -import { Set } from 'immutable' +import { List } from 'immutable' import { useSnackbar } from 'notistack' import React from 'react' import { useDispatch, useSelector } from 'react-redux' @@ -26,6 +26,16 @@ import { safeModulesSelector, } from 'src/routes/safe/store/selectors' import styled from 'styled-components' +import Modal from '../../../../../components/Modal' +import Paragraph from '../../../../../components/layout/Paragraph' +import IconButton from '@material-ui/core/IconButton' +import Close from '@material-ui/icons/Close' +import Hairline from '../../../../../components/layout/Hairline' +import Col from '../../../../../components/layout/Col' +import Link from '../../../../../components/layout/Link' +import OpenInNew from '@material-ui/icons/OpenInNew' +import { getEtherScanLink } from '../../../../../logic/wallets/getWeb3' +import { md, secondary } from '../../../../../theme/variables' export const REMOVE_MODULE_BTN_TEST_ID = 'remove-module-btn' export const MODULES_ROW_TEST_ID = 'owners-row' @@ -51,11 +61,17 @@ const TableActionButton = styled(Button)` background-color: transparent; } ` + const FooterWrapper = styled.div` display: flex; justify-content: space-around; ` +const openIconStyle = { + height: md, + color: secondary, +} + const Advanced: React.FC = () => { const classes = useStyles() @@ -66,14 +82,14 @@ const Advanced: React.FC = () => { const nonce = useSelector(safeNonceSelector) const granted = useSelector(grantedSelector) const modules = useSelector(safeModulesSelector) - const moduleData = getModuleData(Set(modules)) + const moduleData = modules !== null ? getModuleData(List(modules)) : modules const [viewRemoveModuleModal, setViewRemoveModuleModal] = React.useState(false) const hideRemoveModuleModal = () => setViewRemoveModuleModal(false) const [selectedModule, setSelectedModule] = React.useState(null) - const triggerRemoveSelectedModule = (moduleAddress: string): void => { - setSelectedModule(moduleAddress) + const triggerRemoveSelectedModule = (module: [string, string]): void => { + setSelectedModule(module) setViewRemoveModuleModal(true) } @@ -81,20 +97,25 @@ const Advanced: React.FC = () => { const dispatch = useDispatch() const removeSelectedModule = async (): Promise => { - const safeInstance = await getGnosisSafeInstanceAt(safeAddress) - const txData = safeInstance.contract.methods.disableModule(selectedModule).encodeABI() + try { + const safeInstance = await getGnosisSafeInstanceAt(safeAddress) + const [module, prevModule] = selectedModule + const txData = safeInstance.contract.methods.disableModule(module, prevModule).encodeABI() - dispatch( - createTransaction({ - safeAddress, - to: safeAddress, - valueInWei: '0', - txData, - notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, - enqueueSnackbar, - closeSnackbar, - }), - ) + dispatch( + createTransaction({ + safeAddress, + to: safeAddress, + valueInWei: '0', + txData, + notifiedTransaction: TX_NOTIFICATION_TYPES.SETTINGS_CHANGE_TX, + enqueueSnackbar, + closeSnackbar, + }), + ) + } catch (e) { + console.error(`failed to remove the module ${selectedModule}`, e.message) + } } return ( @@ -130,7 +151,7 @@ const Advanced: React.FC = () => { . - {moduleData.size === 0 ? ( + {moduleData === null ? ( No modules enabled @@ -163,8 +184,8 @@ const Advanced: React.FC = () => { {columnId === MODULES_TABLE_ADDRESS_ID ? ( - - {rowElement} + + {rowElement[0]} ) : ( rowElement @@ -178,7 +199,7 @@ const Advanced: React.FC = () => { iconType="delete" color="error" variant="outlined" - onClick={() => triggerRemoveSelectedModule(columnId)} + onClick={() => triggerRemoveSelectedModule(rowElement)} data-testid={REMOVE_MODULE_BTN_TEST_ID} > {null} @@ -197,11 +218,57 @@ const Advanced: React.FC = () => { )} {viewRemoveModuleModal && ( - This is the body} - footer={ + + + + Remove Modal + + + + + + + + + + + + + + + {selectedModule[0]} + + + + {selectedModule[0]} + + + + + + + + + + + + By removing a Module you will sign a new Safe Transaction. The Safe will not be able to interact with it + any longer until it is enabled again through a new Safe Transaction. + + + + + - } - /> + + )} ) diff --git a/src/routes/safe/components/Settings/Advanced/style.ts b/src/routes/safe/components/Settings/Advanced/style.ts index 887ff65b..54f6243a 100644 --- a/src/routes/safe/components/Settings/Advanced/style.ts +++ b/src/routes/safe/components/Settings/Advanced/style.ts @@ -1,5 +1,5 @@ import { createStyles } from '@material-ui/core' -import { border, fontColor, lg, secondaryText, smallFontSize, xl } from 'src/theme/variables' +import { background, border, error, fontColor, lg, md, secondaryText, sm, smallFontSize, xl } from 'src/theme/variables' export const styles = createStyles({ title: { @@ -54,4 +54,59 @@ export const styles = createStyles({ marginLeft: lg, cursor: 'pointer', }, + modalHeading: { + boxSizing: 'border-box', + justifyContent: 'space-between', + maxHeight: '75px', + padding: `${sm} ${lg}`, + }, + modalContainer: { + minHeight: '369px', + }, + modalManage: { + fontSize: lg, + }, + modalClose: { + height: '35px', + width: '35px', + }, + modalButtonRow: { + height: '84px', + justifyContent: 'center', + }, + modalButtonRemove: { + color: '#fff', + backgroundColor: error, + height: '42px', + }, + modalName: { + textOverflow: 'ellipsis', + overflow: 'hidden', + }, + modalUserName: { + whiteSpace: 'nowrap', + }, + modalOwner: { + backgroundColor: background, + padding: md, + alignItems: 'center', + }, + modalUser: { + justifyContent: 'left', + }, + modalDescription: { + padding: md, + }, + modalOpen: { + paddingLeft: sm, + width: 'auto', + '&:hover': { + cursor: 'pointer', + }, + }, + modal: { + height: 'auto', + maxWidth: 'calc(100% - 30px)', + overflow: 'hidden', + }, }) diff --git a/src/routes/safe/store/actions/fetchSafe.ts b/src/routes/safe/store/actions/fetchSafe.ts index ea1b18d0..b9d695bf 100644 --- a/src/routes/safe/store/actions/fetchSafe.ts +++ b/src/routes/safe/store/actions/fetchSafe.ts @@ -15,6 +15,7 @@ import { makeOwner } from 'src/routes/safe/store/models/owner' import { checksumAddress } from 'src/utils/checksumAddress' import { SafeOwner } from '../models/safe' import addSafeModules from './addSafeModules' +import { SENTINEL_ADDRESS } from '../../../../logic/contracts/safeContracts' const buildOwnersFrom = ( safeOwners, @@ -37,6 +38,18 @@ const buildOwnersFrom = ( }) }) +const buildModulesLinkedList = (modulesPaginated: [Array, string] | null): Array<[string, string]> | null => { + if (modulesPaginated.length) { + const [remoteModules, nextModule] = modulesPaginated + + return remoteModules.map((moduleAddress, index, modules) => { + const prevModule = modules[index + 1] + return [moduleAddress, prevModule !== undefined ? prevModule : nextModule] + }) + } + return null +} + export const buildSafe = async (safeAdd, safeName, latestMasterContractVersion?: any) => { const safeAddress = checksumAddress(safeAdd) @@ -75,8 +88,14 @@ export const buildSafe = async (safeAdd, safeName, latestMasterContractVersion?: export const checkAndUpdateSafe = (safeAdd) => async (dispatch) => { const safeAddress = checksumAddress(safeAdd) // Check if the owner's safe did change and update them - const safeParams = ['getThreshold', 'nonce', 'getOwners', 'getModules'] - const [[remoteThreshold, remoteNonce, remoteOwners, remoteModules], localSafe] = await Promise.all([ + const safeParams = [ + 'getThreshold', + 'nonce', + 'getOwners', + // TODO: 100 is an arbitrary large number, to avoid the need for pagination. But pagination must be properly handled + { method: 'getModulesPaginated', args: [SENTINEL_ADDRESS, 100] }, + ] + const [[remoteThreshold, remoteNonce, remoteOwners, remoteModulesPaginated], localSafe] = await Promise.all([ generateBatchRequests({ abi: GnosisSafeSol.abi, address: safeAddress, @@ -93,7 +112,7 @@ export const checkAndUpdateSafe = (safeAdd) => async (dispatch) => { dispatch( addSafeModules({ safeAddress, - modulesAddresses: remoteModules, + modulesAddresses: buildModulesLinkedList(remoteModulesPaginated), }), ) diff --git a/src/routes/safe/store/models/safe.ts b/src/routes/safe/store/models/safe.ts index e69d3cd6..8699994b 100644 --- a/src/routes/safe/store/models/safe.ts +++ b/src/routes/safe/store/models/safe.ts @@ -11,7 +11,7 @@ export type SafeRecordProps = { threshold: number ethBalance: number | string owners: List<{ name: string; address: string }> - modules: Set + modules: List<[string, string]> | null activeTokens: Set activeAssets: Set blacklistedTokens: Set @@ -31,7 +31,7 @@ const makeSafe = Record({ threshold: 0, ethBalance: '0', owners: List([]), - modules: Set(), + modules: List(), activeTokens: Set(), activeAssets: Set(), blacklistedTokens: Set(),