Skip to content

Commit

Permalink
Merge branch 'dev' into nect
Browse files Browse the repository at this point in the history
  • Loading branch information
iamacook committed Oct 18, 2023
2 parents aa6b049 + 8189cd7 commit 2453d54
Show file tree
Hide file tree
Showing 11 changed files with 144 additions and 125 deletions.
17 changes: 7 additions & 10 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,11 @@ jobs:

- uses: ./.github/workflows/yarn

- name: Run tests
run: 'yarn test:ci'

- name: Comment in failing tests
uses: mattallty/[email protected]
if: failure()
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Annotations and coverage report
uses: ArtiomTr/jest-coverage-report-action@v2
with:
test-command: 'echo unit tests already executed'
coverage-comment: false
skip-step: install
annotations: failed-tests
package-manager: yarn
test-script: yarn test:ci
github-token: ${{ secrets.GITHUB_TOKEN }}
6 changes: 1 addition & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"prettier": "prettier -w \"{src,cypress,mocks,scripts}/**/*.{ts,tsx,css,js}\"",
"fix": "yarn lint:fix && ts-prune && yarn prettier",
"test": "cross-env TZ=CET DEBUG_PRINT_LIMIT=30000 jest",
"test:ci": "yarn test --ci --silent --coverage --json --watchAll=false --testLocationInResults --outputFile=jest.results.json",
"test:ci": "yarn test --ci --silent --coverage --json --watchAll=false --testLocationInResults --outputFile=report.json",
"test:coverage": "yarn test --coverage --watchAll=false",
"cmp": "./scripts/cmp.sh",
"routes": "node scripts/generate-routes.js > src/config/routes.ts && prettier -w src/config/routes.ts && cat src/config/routes.ts",
Expand All @@ -32,9 +32,6 @@
"engines": {
"node": ">=16"
},
"pre-commit": [
"lint"
],
"resolutions": {
"@web3-onboard/trezor/**/protobufjs": "^7.2.4"
},
Expand Down Expand Up @@ -127,7 +124,6 @@
"fake-indexeddb": "^4.0.2",
"jest": "^29.6.2",
"jest-environment-jsdom": "^29.6.2",
"pre-commit": "^1.2.2",
"prettier": "^2.7.0",
"ts-node": "^10.8.2",
"ts-prune": "^0.10.3",
Expand Down
18 changes: 14 additions & 4 deletions src/components/address-book/AddressBookHeader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import { ADDRESS_BOOK_EVENTS } from '@/services/analytics/events/addressBook'
import PageHeader from '@/components/common/PageHeader'
import { ModalType } from '../AddressBookTable'
import { useAppSelector } from '@/store'
import { selectAllAddressBooks } from '@/store/addressBookSlice'
import { type AddressBookState, selectAllAddressBooks } from '@/store/addressBookSlice'
import ImportIcon from '@/public/images/common/import.svg'
import ExportIcon from '@/public/images/common/export.svg'
import AddCircleIcon from '@/public/images/common/add-outlined.svg'
import mapProps from '@/utils/mad-props'

const HeaderButton = ({
icon,
Expand All @@ -35,13 +36,18 @@ const HeaderButton = ({
}

type Props = {
allAddressBooks: AddressBookState
handleOpenModal: (type: ModalType) => () => void
searchQuery: string
onSearchQueryChange: (searchQuery: string) => void
}

const AddressBookHeader = ({ handleOpenModal, searchQuery, onSearchQueryChange }: Props): ReactElement => {
const allAddressBooks = useAppSelector(selectAllAddressBooks)
function AddressBookHeader({
allAddressBooks,
handleOpenModal,
searchQuery,
onSearchQueryChange,
}: Props): ReactElement {
const canExport = Object.values(allAddressBooks).some((addressBook) => Object.keys(addressBook || {}).length > 0)

return (
Expand Down Expand Up @@ -96,4 +102,8 @@ const AddressBookHeader = ({ handleOpenModal, searchQuery, onSearchQueryChange }
)
}

export default AddressBookHeader
const useAllAddressBooks = () => useAppSelector(selectAllAddressBooks)

export default mapProps(AddressBookHeader, {
allAddressBooks: useAllAddressBooks,
})
20 changes: 15 additions & 5 deletions src/components/address-book/AddressBookTable/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useContext, useMemo, useState } from 'react'
import { Box } from '@mui/material'
import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk'

import EnhancedTable from '@/components/common/EnhancedTable'
import type { AddressEntry } from '@/components/address-book/EntryDialog'
import EntryDialog from '@/components/address-book/EntryDialog'
Expand All @@ -21,9 +23,10 @@ import PagePlaceholder from '@/components/common/PagePlaceholder'
import NoEntriesIcon from '@/public/images/address-book/no-entries.svg'
import { useCurrentChain } from '@/hooks/useChains'
import tableCss from '@/components/common/EnhancedTable/styles.module.css'
import { TxModalContext } from '@/components/tx-flow'
import { TxModalContext, type TxModalContextType } from '@/components/tx-flow'
import TokenTransferFlow from '@/components/tx-flow/flows/TokenTransfer'
import CheckWallet from '@/components/common/CheckWallet'
import madProps from '@/utils/mad-props'

const headCells = [
{ id: 'name', label: 'Name' },
Expand All @@ -45,10 +48,12 @@ const defaultOpen = {
[ModalType.REMOVE]: false,
}

const AddressBookTable = () => {
const chain = useCurrentChain()
const { setTxFlow } = useContext(TxModalContext)
type AddressBookTableProps = {
chain?: ChainInfo
setTxFlow: TxModalContextType['setTxFlow']
}

function AddressBookTable({ chain, setTxFlow }: AddressBookTableProps) {
const [open, setOpen] = useState<typeof defaultOpen>(defaultOpen)
const [searchQuery, setSearchQuery] = useState('')
const [defaultValues, setDefaultValues] = useState<AddressEntry | undefined>(undefined)
Expand Down Expand Up @@ -170,4 +175,9 @@ const AddressBookTable = () => {
)
}

export default AddressBookTable
const useSetTxFlow = () => useContext(TxModalContext).setTxFlow

export default madProps(AddressBookTable, {
chain: useCurrentChain,
setTxFlow: useSetTxFlow,
})
12 changes: 8 additions & 4 deletions src/components/address-book/EntryDialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,30 @@ import NameInput from '@/components/common/NameInput'
import useChainId from '@/hooks/useChainId'
import { useAppDispatch } from '@/store'
import { upsertAddressBookEntry } from '@/store/addressBookSlice'
import madProps from '@/utils/mad-props'

export type AddressEntry = {
name: string
address: string
}

const EntryDialog = ({
function EntryDialog({
handleClose,
defaultValues = {
name: '',
address: '',
},
disableAddressInput = false,
chainId,
currentChainId,
}: {
handleClose: () => void
defaultValues?: AddressEntry
disableAddressInput?: boolean
chainId?: string
}): ReactElement => {
currentChainId: string
}): ReactElement {
const dispatch = useAppDispatch()
const currentChainId = useChainId()

const methods = useForm<AddressEntry>({
defaultValues,
Expand Down Expand Up @@ -81,4 +83,6 @@ const EntryDialog = ({
)
}

export default EntryDialog
export default madProps(EntryDialog, {
currentChainId: useChainId,
})
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { _getCsvData } from '.'
import ExportDialog from '.'
import { render, screen } from '@/tests/test-utils'

describe('ExportDialog', () => {
describe('getCsvData', () => {
Expand All @@ -22,4 +24,10 @@ describe('ExportDialog', () => {
])
})
})

it('should render the export dialog', () => {
const onClose = jest.fn()
render(<ExportDialog handleClose={onClose} />)
expect(screen.getByText('Export address book')).toBeInTheDocument()
})
})
20 changes: 15 additions & 5 deletions src/components/address-book/ExportDialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useAppSelector } from '@/store'
import { trackEvent, ADDRESS_BOOK_EVENTS } from '@/services/analytics'
import ExternalLink from '@/components/common/ExternalLink'
import { HelpCenterArticle } from '@/config/constants'
import madProps from '@/utils/mad-props'

const COL_1 = 'address'
const COL_2 = 'name'
Expand All @@ -35,14 +36,19 @@ export const _getCsvData = (addressBooks: AddressBookState): CsvData => {
return csvData
}

const ExportDialog = ({ handleClose }: { handleClose: () => void }): ReactElement => {
const addressBooks: AddressBookState = useAppSelector(selectAllAddressBooks)
const length = Object.values(addressBooks).reduce<number>((acc, entries) => acc + Object.keys(entries).length, 0)
function ExportDialog({
allAddressBooks,
handleClose,
}: {
allAddressBooks: AddressBookState
handleClose: () => void
}): ReactElement {
const length = Object.values(allAddressBooks).reduce<number>((acc, entries) => acc + Object.keys(entries).length, 0)
const { CSVDownloader } = useCSVDownloader()
// safe-address-book-1970-01-01
const filename = `safe-address-book-${new Date().toISOString().slice(0, 10)}`

const csvData = useMemo(() => _getCsvData(addressBooks), [addressBooks])
const csvData = useMemo(() => _getCsvData(allAddressBooks), [allAddressBooks])

const onSubmit = (e: SyntheticEvent) => {
e.preventDefault()
Expand Down Expand Up @@ -87,4 +93,8 @@ const ExportDialog = ({ handleClose }: { handleClose: () => void }): ReactElemen
)
}

export default ExportDialog
const useAllAddressBooks = () => useAppSelector(selectAllAddressBooks)

export default madProps(ExportDialog, {
allAddressBooks: useAllAddressBooks,
})
2 changes: 1 addition & 1 deletion src/components/tx-flow/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { SuccessScreen } from './flows/SuccessScreen'

const noop = () => {}

type TxModalContextType = {
export type TxModalContextType = {
txFlow: JSX.Element | undefined
setTxFlow: (txFlow: TxModalContextType['txFlow'], onClose?: () => void, shouldWarn?: boolean) => void
setFullWidth: (fullWidth: boolean) => void
Expand Down
34 changes: 34 additions & 0 deletions src/utils/__tests__/mad-props.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { render, waitFor, screen } from '@/tests/test-utils'
import madProps from '../mad-props'

describe('madProps', () => {
it('should map a set of props to hooks', async () => {
const Component = ({ foo, bar }: { foo: string; bar: string }) => <>{foo + bar}</>

const MappedComponent = madProps(Component, {
foo: () => 'foo',
bar: () => 'bar',
})

render(<MappedComponent />)

await waitFor(() => {
expect(screen.getByText('foobar')).toBeInTheDocument()
})
})

it('should accept additional props', async () => {
const Component = ({ foo, bar, baz }: { foo: string; bar: string; baz: string }) => <>{foo + bar + baz}</>

const MappedComponent = madProps(Component, {
foo: () => 'foo',
bar: () => 'bar',
})

render(<MappedComponent baz="baz" />)

await waitFor(() => {
expect(screen.getByText('foobarbaz')).toBeInTheDocument()
})
})
})
40 changes: 40 additions & 0 deletions src/utils/mad-props.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { ComponentType, FC } from 'react'
import React, { memo } from 'react'

type HookMap<P> = {
[K in keyof P]?: () => P[K]
}

/**
* Injects props into a component using hooks.
* This allows to keep the original component pure and testable.
*
* @param Component The component to wrap
* @param hookMap A map of hooks to use, keys are the props to inject, values are the hooks
* @returns A new component with the props injected
*/
const madProps = <P extends Record<string, unknown>, H extends keyof P>(
Component: ComponentType<P>,
hookMap: HookMap<Pick<P, H>>,
): FC<Omit<P, H>> => {
const MadComponent = (externalProps: Omit<P, H>) => {
let newProps: P = { ...externalProps } as P

for (const key in hookMap) {
const hook = hookMap[key]
if (hook !== undefined) {
newProps[key as H] = hook()
}
}

return <Component {...newProps} />
}

MadComponent.displayName = Component.displayName || Component.name

// Wrapping MadComponent with React.memo and casting to FC<Omit<P, H>>
// The casting is only needed because of memo, the component itself satisfies the type
return memo(MadComponent) as unknown as FC<Omit<P, H>>
}

export default madProps
Loading

0 comments on commit 2453d54

Please sign in to comment.