Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: pinned apps on top of all apps + rm list view switch #2699

Merged
merged 7 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions cypress/e2e/pages/safeapps.pages.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,22 +78,28 @@ export function verifyNoAppsTextPresent() {
}

export function pinApp(app, pin = true) {
let str = 'Unpin'
if (!pin) str = 'Pin'
cy.findByLabelText(app)
.click()
.should(($el) => {
const ariaLabel = $el.attr('aria-label')
expect(ariaLabel).to.include(str)
})
cy.findByLabelText(app).click()
cy.wait(200)
cy.findByLabelText(app).should(($el) => {
const ariaLabel = $el.attr('aria-label')
expect(ariaLabel).to.include(pin ? 'Unpin' : 'Pin')
})
}

export function clickOnBookmarkedAppsTab() {
cy.findByText(bookmarkedAppsStr).click()
}

export function verifyAppCount(count) {
cy.findByText(`ALL (${count})`).should('be.visible')
cy.findByText(`All apps (${count})`).should('be.visible')
}

export function verifyCustomAppCount(count) {
cy.findByText(`Custom apps (${count})`).should('be.visible')
}

export function verifyPinnedAppCount(count) {
cy.findByText(`My pinned apps (${count})`).should(count ? 'be.visible' : 'not.exist')
}

export function clickOnCustomAppsTab() {
Expand Down
8 changes: 3 additions & 5 deletions cypress/e2e/safe-apps/apps_list.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,15 @@ describe('Safe Apps tests', () => {
safeapps.clearSearchAppInput()
safeapps.pinApp(safeapps.pinWalletConnectStr)
safeapps.pinApp(safeapps.transactionBuilderStr)
safeapps.clickOnBookmarkedAppsTab()
safeapps.verifyAppCount(2)
safeapps.verifyPinnedAppCount(2)
})

it('Verify apps can be unpinned [C56134]', () => {
safeapps.pinApp(safeapps.pinWalletConnectStr)
safeapps.pinApp(safeapps.transactionBuilderStr)
safeapps.pinApp(safeapps.pinWalletConnectStr, false)
safeapps.pinApp(safeapps.transactionBuilderStr, false)
safeapps.clickOnBookmarkedAppsTab()
safeapps.verifyAppCount(0)
safeapps.verifyPinnedAppCount(0)
})

it('Verify there is an error when the app manifest is invalid [C56135]', () => {
Expand All @@ -70,7 +68,7 @@ describe('Safe Apps tests', () => {
safeapps.verifyAppTitle(myCustomAppTitle)
safeapps.acceptTC()
safeapps.clickOnAddBtn()
safeapps.verifyAppCount(1)
safeapps.verifyCustomAppCount(1)
safeapps.verifyAppDescription(myCustomAppDescrAdded)
})
})
70 changes: 0 additions & 70 deletions src/components/safe-apps/SafeAppCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import Card from '@mui/material/Card'
import CardHeader from '@mui/material/CardHeader'
import CardContent from '@mui/material/CardContent'
import Typography from '@mui/material/Typography'
import CardActions from '@mui/material/CardActions'
import Box from '@mui/material/Box'
import { resolveHref } from 'next/dist/client/resolve-href'
import classNames from 'classnames'
import type { ReactNode, SyntheticEvent } from 'react'
Expand All @@ -21,15 +19,9 @@ import { AppRoutes } from '@/config/routes'
import BatchIcon from '@/public/images/apps/batch-icon.svg'
import css from './styles.module.css'

export type SafeAppsViewMode = 'list-view' | 'grid-view'

export const GRID_VIEW_MODE: SafeAppsViewMode = 'grid-view' // default view
export const LIST_VIEW_MODE: SafeAppsViewMode = 'list-view'

type SafeAppCardProps = {
safeApp: SafeAppData
onClickSafeApp?: () => void
viewMode?: SafeAppsViewMode
isBookmarked?: boolean
onBookmarkSafeApp?: (safeAppId: number) => void
removeCustomApp?: (safeApp: SafeAppData) => void
Expand All @@ -39,7 +31,6 @@ type SafeAppCardProps = {
const SafeAppCard = ({
safeApp,
onClickSafeApp,
viewMode,
isBookmarked,
onBookmarkSafeApp,
removeCustomApp,
Expand All @@ -49,23 +40,6 @@ const SafeAppCard = ({

const safeAppUrl = getSafeAppUrl(router, safeApp.url)

const isListViewMode = viewMode === LIST_VIEW_MODE

if (isListViewMode) {
return (
<SafeAppCardListView
safeApp={safeApp}
safeAppUrl={safeAppUrl}
isBookmarked={isBookmarked}
onBookmarkSafeApp={onBookmarkSafeApp}
removeCustomApp={removeCustomApp}
onClickSafeApp={onClickSafeApp}
openPreviewDrawer={openPreviewDrawer}
/>
)
}

// Grid view as fallback
return (
<SafeAppCardGridView
safeApp={safeApp}
Expand Down Expand Up @@ -157,50 +131,6 @@ const SafeAppCardGridView = ({
)
}

const SafeAppCardListView = ({
safeApp,
onClickSafeApp,
safeAppUrl,
isBookmarked,
onBookmarkSafeApp,
removeCustomApp,
openPreviewDrawer,
}: SafeAppCardViewProps) => {
return (
<SafeAppCardContainer safeAppUrl={safeAppUrl} onClickSafeApp={onClickSafeApp}>
<CardContent sx={{ pb: '16px !important' }}>
<Box display="flex" flexDirection="row" alignItems="center" gap={2}>
<div className={css.safeAppIconContainer}>
{/* Batch transactions Icon */}
{isOptimizedForBatchTransactions(safeApp) && (
<BatchIcon className={css.safeAppBatchIcon} alt="batch transactions icon" />
)}

{/* Safe App Icon */}
<SafeAppIconCard src={safeApp.iconUrl} alt={`${safeApp.name} logo`} />
</div>

{/* Safe App Title */}
<Typography className={css.safeAppTitle} gutterBottom variant="h5">
{safeApp.name}
</Typography>

{/* Safe App Action Buttons */}
<CardActions>
<SafeAppActionButtons
safeApp={safeApp}
isBookmarked={isBookmarked}
onBookmarkSafeApp={onBookmarkSafeApp}
removeCustomApp={removeCustomApp}
openPreviewDrawer={openPreviewDrawer}
/>
</CardActions>
</Box>
</CardContent>
</SafeAppCardContainer>
)
}

type SafeAppCardContainerProps = {
onClickSafeApp?: () => void
safeAppUrl: string
Expand Down
81 changes: 30 additions & 51 deletions src/components/safe-apps/SafeAppList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,42 @@
import { useCallback } from 'react'
import type { SafeAppData } from '@safe-global/safe-gateway-typescript-sdk'
import classnames from 'classnames'
import { motion, AnimatePresence } from 'framer-motion'

import SafeAppsFilters from '@/components/safe-apps/SafeAppsFilters'
import SafeAppCard, { GRID_VIEW_MODE } from '@/components/safe-apps/SafeAppCard'
import type { SafeAppsViewMode } from '@/components/safe-apps/SafeAppCard'
import SafeAppCard from '@/components/safe-apps/SafeAppCard'
import AddCustomSafeAppCard from '@/components/safe-apps/AddCustomSafeAppCard'
import SafeAppPreviewDrawer from '@/components/safe-apps/SafeAppPreviewDrawer'
import SafeAppsListHeader from '@/components/safe-apps/SafeAppsListHeader'
import SafeAppsZeroResultsPlaceholder from '@/components/safe-apps/SafeAppsZeroResultsPlaceholder'
import useSafeAppsFilters from '@/hooks/safe-apps/useSafeAppsFilters'
import useSafeAppPreviewDrawer from '@/hooks/safe-apps/useSafeAppPreviewDrawer'
import css from './styles.module.css'
import { Skeleton } from '@mui/material'
import useLocalStorage from '@/services/local-storage/useLocalStorage'
import { useOpenedSafeApps } from '@/hooks/safe-apps/useOpenedSafeApps'

type SafeAppListProps = {
safeAppsList: SafeAppData[]
safeAppsListLoading?: boolean
bookmarkedSafeAppsId?: Set<number>
onBookmarkSafeApp?: (safeAppId: number) => void
showFilters?: boolean
addCustomApp?: (safeApp: SafeAppData) => void
removeCustomApp?: (safeApp: SafeAppData) => void
title: string
query?: string
}

const VIEW_MODE_KEY = 'SafeApps_viewMode'

const SafeAppList = ({
safeAppsList,
safeAppsListLoading,
bookmarkedSafeAppsId,
onBookmarkSafeApp,
showFilters,
addCustomApp,
removeCustomApp,
title,
query,
}: SafeAppListProps) => {
const [safeAppsViewMode = GRID_VIEW_MODE, setSafeAppsViewMode] = useLocalStorage<SafeAppsViewMode>(VIEW_MODE_KEY)
const { isPreviewDrawerOpen, previewDrawerApp, openPreviewDrawer, closePreviewDrawer } = useSafeAppPreviewDrawer()
const { openedSafeAppIds } = useOpenedSafeApps()

const { filteredApps, query, setQuery, setSelectedCategories, setOptimizedWithBatchFilter, selectedCategories } =
useSafeAppsFilters(safeAppsList)

const showZeroResultsPlaceholder = query && filteredApps.length === 0
const showZeroResultsPlaceholder = query && safeAppsList.length === 0

const handleSafeAppClick = useCallback(
(safeApp: SafeAppData) => {
Expand All @@ -59,31 +51,11 @@ const SafeAppList = ({

return (
<>
{/* Safe Apps Filters */}
{showFilters && (
<SafeAppsFilters
onChangeQuery={setQuery}
onChangeFilterCategory={setSelectedCategories}
onChangeOptimizedWithBatch={setOptimizedWithBatchFilter}
selectedCategories={selectedCategories}
safeAppsList={safeAppsList}
/>
)}

{/* Safe Apps List Header */}
<SafeAppsListHeader
amount={filteredApps.length}
safeAppsViewMode={safeAppsViewMode}
setSafeAppsViewMode={setSafeAppsViewMode}
/>
<SafeAppsListHeader title={title} amount={safeAppsList.length} />

{/* Safe Apps List */}
<ul
className={classnames(
css.safeAppsContainer,
safeAppsViewMode === GRID_VIEW_MODE ? css.safeAppsGridViewContainer : css.safeAppsListViewContainer,
)}
>
<ul className={css.safeAppsContainer}>
{/* Add Custom Safe App Card */}
{addCustomApp && (
<li>
Expand All @@ -98,20 +70,27 @@ const SafeAppList = ({
</li>
))}

{/* Flat list filtered by search query */}
{filteredApps.map((safeApp) => (
<li key={safeApp.id}>
<SafeAppCard
safeApp={safeApp}
viewMode={safeAppsViewMode}
isBookmarked={bookmarkedSafeAppsId?.has(safeApp.id)}
onBookmarkSafeApp={onBookmarkSafeApp}
removeCustomApp={removeCustomApp}
onClickSafeApp={handleSafeAppClick(safeApp)}
openPreviewDrawer={openPreviewDrawer}
/>
</li>
))}
<AnimatePresence>
{/* Flat list filtered by search query */}
{safeAppsList.map((safeApp) => (
<motion.li
key={safeApp.id}
layout
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.8, opacity: 0 }}
transition={{ duration: 0.2 }}
>
<SafeAppCard
safeApp={safeApp}
isBookmarked={bookmarkedSafeAppsId?.has(safeApp.id)}
onBookmarkSafeApp={onBookmarkSafeApp}
removeCustomApp={removeCustomApp}
onClickSafeApp={handleSafeAppClick(safeApp)}
openPreviewDrawer={openPreviewDrawer}
/>
</motion.li>
))}
</AnimatePresence>
</ul>

{/* Zero results placeholder */}
Expand Down
9 changes: 1 addition & 8 deletions src/components/safe-apps/SafeAppList/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,6 @@
display: grid;
grid-gap: var(--space-3);
list-style-type: none;
padding: 0;
}

.safeAppsGridViewContainer {
padding: 0 0 var(--space-1);
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}

.safeAppsListViewContainer {
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
64 changes: 5 additions & 59 deletions src/components/safe-apps/SafeAppsListHeader/index.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,15 @@
import Stack from '@mui/material/Stack'
import Typography from '@mui/material/Typography'
import RadioGroup from '@mui/material/RadioGroup'
import Radio from '@mui/material/Radio'
import FormControl from '@mui/material/FormControl'

import GridViewIcon from '@/public/images/apps/grid-view-icon.svg'
import ListViewIcon from '@/public/images/apps/list-view-icon.svg'
import { GRID_VIEW_MODE, LIST_VIEW_MODE } from '@/components/safe-apps/SafeAppCard'
import type { SafeAppsViewMode } from '@/components/safe-apps/SafeAppCard'
import css from './styles.module.css'
import { SAFE_APPS_EVENTS, trackEvent } from '@/services/analytics'

type SafeAppsListHeaderProps = {
title: string
amount?: number
safeAppsViewMode: SafeAppsViewMode
setSafeAppsViewMode: (viewMode: SafeAppsViewMode) => void
}

const SafeAppsListHeader = ({ amount, safeAppsViewMode, setSafeAppsViewMode }: SafeAppsListHeaderProps) => {
const SafeAppsListHeader = ({ title, amount }: SafeAppsListHeaderProps) => {
return (
<Stack display="flex" flexDirection="row" justifyContent="space-between">
{/* Safe Apps count */}
<Typography variant="body2" color="primary.light" mb={2} mt={1.5} fontSize="12px" letterSpacing="0.4px">
ALL ({amount || 0})
</Typography>

{/* switch Safe Apps view mode radio buttons */}
<FormControl>
<RadioGroup
value={safeAppsViewMode}
aria-label="safe apps view mode selector"
name="safe-apps-view-mode"
sx={{ flexDirection: 'row' }}
onChange={(_, viewMode) => {
trackEvent({ ...SAFE_APPS_EVENTS.SWITCH_LIST_VIEW, label: viewMode })
setSafeAppsViewMode(viewMode as SafeAppsViewMode)
}}
>
{/* Grid view radio button */}
<Radio
value={GRID_VIEW_MODE}
disableRipple
color="default"
checkedIcon={<GridViewIcon />}
icon={<GridViewIcon className={css.gridView} />}
sx={{ padding: '4px' }}
inputProps={{
'aria-label': 'Grid view mode',
}}
/>
{/* Grid view radio button */}
<Radio
value={LIST_VIEW_MODE}
disableRipple
color="default"
checkedIcon={<ListViewIcon />}
icon={<ListViewIcon className={css.listView} />}
sx={{ padding: '4px' }}
inputProps={{
'aria-label': 'List view mode',
}}
/>
</RadioGroup>
</FormControl>
</Stack>
<Typography variant="body2" color="primary.light" fontWeight="bold" mt={3}>
{title} ({amount || 0})
</Typography>
)
}

Expand Down
Loading
Loading