Skip to content

Commit

Permalink
Feat(sidebar): sort safes by name and recently used [SW-307] (#4458)
Browse files Browse the repository at this point in the history
* Feat: add option to sort by recently visited and by name

* tests: add new properties to unit tests

* fix: make order by preference persist

* feat: sort sub account items by name and recently visited

* refactor: fix incorrect variable names and remove console log
  • Loading branch information
jmealy authored Nov 5, 2024
1 parent 3a88459 commit cfb0e9f
Show file tree
Hide file tree
Showing 14 changed files with 504 additions and 93 deletions.
22 changes: 11 additions & 11 deletions src/components/welcome/MyAccounts/MultiAccountItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { OVERVIEW_EVENTS, OVERVIEW_LABELS, trackEvent } from '@/services/analyti
import { AppRoutes } from '@/config/routes'
import { useAppDispatch, useAppSelector } from '@/store'
import css from './styles.module.css'
import { selectAllAddressBooks } from '@/store/addressBookSlice'
import useSafeAddress from '@/hooks/useSafeAddress'
import { sameAddress } from '@/utils/addresses'
import classnames from 'classnames'
Expand All @@ -43,6 +42,8 @@ import BookmarkIcon from '@/public/images/apps/bookmark.svg'
import BookmarkedIcon from '@/public/images/apps/bookmarked.svg'
import { addOrUpdateSafe, pinSafe, selectAllAddedSafes, unpinSafe } from '@/store/addedSafesSlice'
import { defaultSafeInfo } from '@/store/safeInfoSlice'
import { getComparator } from './utils'
import { selectOrderByPreference } from '@/store/orderByPreferenceSlice'

type MultiAccountItemProps = {
multiSafeAccountItem: MultiChainSafeItem
Expand Down Expand Up @@ -81,6 +82,10 @@ const MultiAccountItem = ({ onLinkClick, multiSafeAccountItem }: MultiAccountIte
const isWelcomePage = router.pathname === AppRoutes.welcome.accounts
const [expanded, setExpanded] = useState(isCurrentSafe)
const chains = useAppSelector(selectChains)
const { orderBy } = useAppSelector(selectOrderByPreference)

const sortComparator = getComparator(orderBy)
const sortedSafes = useMemo(() => safes.sort(sortComparator), [safes, sortComparator])

const allAddedSafes = useAppSelector((state) => selectAllAddedSafes(state))
const dispatch = useAppDispatch()
Expand All @@ -99,11 +104,6 @@ const MultiAccountItem = ({ onLinkClick, multiSafeAccountItem }: MultiAccountIte
setExpanded((prev) => !prev)
}

const allAddressBooks = useAppSelector(selectAllAddressBooks)
const name = useMemo(() => {
return Object.values(allAddressBooks).find((ab) => ab[address] !== undefined)?.[address]
}, [address, allAddressBooks])

const currency = useAppSelector(selectCurrency)
const { address: walletAddress } = useWallet() ?? {}
const deployedSafes = useMemo(
Expand Down Expand Up @@ -197,9 +197,9 @@ const MultiAccountItem = ({ onLinkClick, multiSafeAccountItem }: MultiAccountIte
<SafeIcon address={address} owners={sharedSetup?.owners.length} threshold={sharedSetup?.threshold} />
</Box>
<Typography variant="body2" component="div" className={css.safeAddress}>
{name && (
{multiSafeAccountItem.name && (
<Typography variant="subtitle2" component="p" fontWeight="bold" className={css.safeName}>
{name}
{multiSafeAccountItem.name}
</Typography>
)}
<Typography color="var(--color-primary-light)" fontSize="inherit" component="span">
Expand Down Expand Up @@ -232,15 +232,15 @@ const MultiAccountItem = ({ onLinkClick, multiSafeAccountItem }: MultiAccountIte
/>
</IconButton>
<MultiAccountContextMenu
name={name ?? ''}
name={multiSafeAccountItem.name ?? ''}
address={address}
chainIds={deployedChainIds}
addNetwork={hasReplayableSafe}
/>
</AccordionSummary>
<AccordionDetails sx={{ padding: '0px 12px' }}>
<Box>
{safes.map((safeItem) => (
{sortedSafes.map((safeItem) => (
<SubAccountItem
onLinkClick={onLinkClick}
safeItem={safeItem}
Expand All @@ -254,7 +254,7 @@ const MultiAccountItem = ({ onLinkClick, multiSafeAccountItem }: MultiAccountIte
<Divider sx={{ ml: '-12px', mr: '-12px' }} />
<Box display="flex" alignItems="center" justifyContent="center" sx={{ ml: '-12px', mr: '-12px' }}>
<AddNetworkButton
currentName={name}
currentName={multiSafeAccountItem.name ?? ''}
safeAddress={address}
deployedChains={safes.map((safe) => safe.chainId)}
/>
Expand Down
71 changes: 71 additions & 0 deletions src/components/welcome/MyAccounts/OrderByButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useState } from 'react'
import { Button, ListItemText, MenuItem, SvgIcon } from '@mui/material'
import ContextMenu from '@/components/common/ContextMenu'
import TransactionsIcon from '@/public/images/transactions/transactions.svg'
import CheckIcon from '@/public/images/common/check.svg'
import { OrderByOption } from '@/store/orderByPreferenceSlice'

type OrderByButtonProps = {
orderBy: OrderByOption
onOrderByChange: (orderBy: OrderByOption) => void
}

const OrderByButton = ({ orderBy: orderBy, onOrderByChange: onOrderByChange }: OrderByButtonProps) => {
const [anchorEl, setAnchorEl] = useState<HTMLElement | undefined>()

const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget)
}

const handleClose = () => {
setAnchorEl(undefined)
}

const handleOrderByChange = (newOrderBy: OrderByOption) => {
onOrderByChange(newOrderBy)
handleClose()
}

return (
<>
<Button
onClick={handleClick}
startIcon={<SvgIcon component={TransactionsIcon} inheritViewBox />}
sx={{ color: 'text.secondary' }}
size="small"
>
Sort
</Button>

<ContextMenu
anchorEl={anchorEl}
open={!!anchorEl}
onClose={handleClose}
sx={{
'& .MuiPaper-root': { minWidth: '250px' },
'& .Mui-selected, & .Mui-selected:hover': {
backgroundColor: `background.paper`,
},
}}
>
<MenuItem disabled>
<ListItemText>Sort by</ListItemText>
</MenuItem>
<MenuItem
sx={{ borderRadius: 0 }}
onClick={() => handleOrderByChange(OrderByOption.LAST_VISITED)}
selected={orderBy === OrderByOption.LAST_VISITED}
>
<ListItemText sx={{ mr: 2 }}>Most recent</ListItemText>
{orderBy === OrderByOption.LAST_VISITED && <CheckIcon sx={{ ml: 1 }} />}
</MenuItem>
<MenuItem onClick={() => handleOrderByChange(OrderByOption.NAME)} selected={orderBy === OrderByOption.NAME}>
<ListItemText>Name</ListItemText>
{orderBy === OrderByOption.NAME && <CheckIcon sx={{ ml: 1 }} />}
</MenuItem>
</ContextMenu>
</>
)
}

export default OrderByButton
160 changes: 103 additions & 57 deletions src/components/welcome/MyAccounts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import {
AccordionSummary,
Box,
Button,
Divider,
InputAdornment,
Link,
Paper,
SvgIcon,
TextField,
Typography,
} from '@mui/material'
import madProps from '@/utils/mad-props'
Expand All @@ -27,16 +30,30 @@ import { type SafeItem } from './useAllSafes'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import BookmarkIcon from '@/public/images/apps/bookmark.svg'
import classNames from 'classnames'
import { getComparator } from './utils'
import OrderByButton from './OrderByButton'
import SearchIcon from '@/public/images/common/search.svg'
import type { OrderByOption } from '@/store/orderByPreferenceSlice'
import { selectOrderByPreference, setOrderByPreference } from '@/store/orderByPreferenceSlice'
import { useAppDispatch, useAppSelector } from '@/store'

type AccountsListProps = {
safes: AllSafesGrouped
isSidebar?: boolean
onLinkClick?: () => void
}

const AccountsList = ({ safes, onLinkClick, isSidebar = false }: AccountsListProps) => {
const wallet = useWallet()
const router = useRouter()
const allSafes = [...(safes.allMultiChainSafes ?? []), ...(safes.allSingleSafes ?? [])]
const { orderBy } = useAppSelector(selectOrderByPreference)
const dispatch = useAppDispatch()
const sortComparator = getComparator(orderBy)

const allSafes = useMemo(
() => [...(safes.allMultiChainSafes ?? []), ...(safes.allSingleSafes ?? [])].sort(sortComparator),
[safes.allMultiChainSafes, safes.allSingleSafes, sortComparator],
)

// We consider a multiChain account owned if at least one of the multiChain accounts is not on the watchlist
const ownedMultiChainSafes = useMemo(
Expand All @@ -62,13 +79,14 @@ const AccountsList = ({ safes, onLinkClick, isSidebar = false }: AccountsListPro
[safes, watchlistMultiChainSafes],
)
const pinnedSafes = useMemo<(MultiChainSafeItem | SafeItem)[]>(
() => [
...(safes.allSingleSafes?.filter(({ isPinned }) => isPinned) ?? []),
...(safes.allMultiChainSafes?.filter(({ isPinned }) => isPinned) ?? []),
],
[safes],
() => [...(allSafes?.filter(({ isPinned }) => isPinned) ?? [])],
[allSafes],
)

const handleOrderByChange = (orderBy: OrderByOption) => {
dispatch(setOrderByPreference({ orderBy }))
}

useTrackSafesCount(ownedSafes, watchlistSafes, wallet)

const isLoginPage = router.pathname === AppRoutes.welcome.accounts
Expand Down Expand Up @@ -102,62 +120,90 @@ const AccountsList = ({ safes, onLinkClick, isSidebar = false }: AccountsListPro
</Box>
</Box>

<Paper className={css.safeList}>
{/* Pinned Accounts */}
<Box mb={2} minHeight="170px">
<div className={css.listHeader}>
<SvgIcon
component={BookmarkIcon}
inheritViewBox
fontSize="small"
sx={{ mt: '2px', mr: 1, strokeWidth: 2 }}
<Paper sx={{ padding: 0 }}>
<Paper sx={{ px: 2, py: 1 }}>
<Box display="flex" justifyContent="space-between" width="100%" gap={1}>
<TextField
id="search-by-name"
placeholder="Search"
aria-label="Search Safe list by name"
variant="filled"
hiddenLabel
onChange={() => {}}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SvgIcon component={SearchIcon} inheritViewBox color="border" fontSize="small" />
</InputAdornment>
),
disableUnderline: true,
}}
fullWidth
size="small"
/>
<Typography variant="h5" fontWeight={700} mb={2}>
Pinned
</Typography>
</div>
{pinnedSafes.length > 0 ? (
<SafesList safes={pinnedSafes} onLinkClick={onLinkClick} />
) : (
<Box className={css.noPinnedSafesMessage}>
<Typography color="text.secondary" variant="body2" maxWidth="350px" textAlign="center">
Personalize your account list by clicking the
<SvgIcon
component={BookmarkIcon}
inheritViewBox
fontSize="small"
sx={{ mx: '4px', color: 'text.secondary', position: 'relative', top: '2px' }}
/>
icon on the accounts most important to you.
</Typography>
</Box>
)}
</Box>
<OrderByButton orderBy={orderBy} onOrderByChange={handleOrderByChange} />
</Box>
</Paper>

{/* All Accounts */}
<Accordion defaultExpanded={pinnedSafes.length === 0} sx={{ border: 'none' }}>
<AccordionSummary
expandIcon={<ExpandMoreIcon sx={{ '& path': { fill: 'var(--color-text-secondary)' } }} />}
sx={{ padding: 0, '& .MuiAccordionSummary-content': { margin: '0 !important', mb: 1, flexGrow: 0 } }}
>
{isSidebar && <Divider />}

<Paper className={css.safeList}>
{/* Pinned Accounts */}
<Box mb={2} minHeight="170px">
<div className={css.listHeader}>
<Typography variant="h5" fontWeight={700}>
Accounts
{allSafes && allSafes.length > 0 && (
<Typography component="span" color="text.secondary" fontSize="inherit" fontWeight="normal" mr={1}>
{' '}
({allSafes.length})
</Typography>
)}
<SvgIcon
component={BookmarkIcon}
inheritViewBox
fontSize="small"
sx={{ mt: '2px', mr: 1, strokeWidth: 2 }}
/>
<Typography variant="h5" fontWeight={700} mb={2}>
Pinned
</Typography>
</div>
</AccordionSummary>
<AccordionDetails sx={{ padding: 0 }}>
<Box mt={1}>
<SafesList safes={allSafes} onLinkClick={onLinkClick} />
</Box>
</AccordionDetails>
</Accordion>
{pinnedSafes.length > 0 ? (
<SafesList safes={pinnedSafes} onLinkClick={onLinkClick} />
) : (
<Box className={css.noPinnedSafesMessage}>
<Typography color="text.secondary" variant="body2" maxWidth="350px" textAlign="center">
Personalize your account list by clicking the
<SvgIcon
component={BookmarkIcon}
inheritViewBox
fontSize="small"
sx={{ mx: '4px', color: 'text.secondary', position: 'relative', top: '2px' }}
/>
icon on the accounts most important to you.
</Typography>
</Box>
)}
</Box>

{/* All Accounts */}
<Accordion defaultExpanded={pinnedSafes.length === 0} sx={{ border: 'none' }}>
<AccordionSummary
expandIcon={<ExpandMoreIcon sx={{ '& path': { fill: 'var(--color-text-secondary)' } }} />}
sx={{ padding: 0, '& .MuiAccordionSummary-content': { margin: '0 !important', mb: 1, flexGrow: 0 } }}
>
<div className={css.listHeader}>
<Typography variant="h5" fontWeight={700}>
Accounts
{allSafes && allSafes.length > 0 && (
<Typography component="span" color="text.secondary" fontSize="inherit" fontWeight="normal" mr={1}>
{' '}
({allSafes.length})
</Typography>
)}
</Typography>
</div>
</AccordionSummary>
<AccordionDetails sx={{ padding: 0 }}>
<Box mt={1}>
<SafesList safes={allSafes} onLinkClick={onLinkClick} />
</Box>
</AccordionDetails>
</Accordion>
</Paper>
</Paper>
<DataWidget />
</Box>
Expand Down
Loading

0 comments on commit cfb0e9f

Please sign in to comment.