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

chore: clear transactions on special route #392

Merged
merged 4 commits into from
Dec 18, 2024
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
4 changes: 3 additions & 1 deletion extension/src/components/InlineForm.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { ComponentPropsWithRef } from 'react'
import { Form } from 'react-router'

export type InlineFormContext = Record<string, string | number | undefined>

type InlineFormProps = ComponentPropsWithRef<typeof Form> & {
context?: Record<string, string | number | undefined>
context?: InlineFormContext
intent?: string
}

Expand Down
1 change: 1 addition & 0 deletions extension/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export { ConfirmationModal, useConfirmationModal } from './ConfirmationModal'
export { CopyToClipboard } from './CopyToClipboard'
export { Divider } from './Divider'
export { InlineForm } from './InlineForm'
export type { InlineFormContext } from './InlineForm'
export * from './inputs'
export * from './logos'
export { Modal } from './Modal'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { expectRouteToBe, render } from '@/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { action, ClearTransactions } from './ClearTransactions'

const { mockClearTransactions } = vi.hoisted(() => ({
mockClearTransactions: vi.fn(),
}))

vi.mock('./useClearTransactions', () => ({
useClearTransactions: () => mockClearTransactions,
}))

describe('Clear transactions', () => {
beforeEach(() => {
mockClearTransactions.mockResolvedValue(undefined)
})

it('clears all transactions', async () => {
await render('/test-route/clear-transactions/new-route', [
{
path: '/:activeRouteId/clear-transactions/:newActiveRouteId',
Component: ClearTransactions,
action,
},
])

expect(mockClearTransactions).toHaveBeenCalled()
})

it('redirects to the new active route', async () => {
await render(
'/test-route/clear-transactions/new-route',
[
{
path: '/:activeRouteId/clear-transactions/:newActiveRouteId',
Component: ClearTransactions,
action,
},
],
{ inspectRoutes: ['/:activeRouteId'] },
)

await expectRouteToBe('/new-route')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useEffect } from 'react'
import { redirect, useSubmit, type ActionFunctionArgs } from 'react-router'
import { useClearTransactions } from './useClearTransactions'

export const action = async ({ params }: ActionFunctionArgs) => {
const { newActiveRouteId } = params

return redirect(`/${newActiveRouteId}`)
}

export const ClearTransactions = () => {
const clearTransactions = useClearTransactions()
const submit = useSubmit()

useEffect(() => {
clearTransactions().then(() => submit(null, { method: 'post' }))
}, [clearTransactions, submit])

return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { action, ClearTransactions as Component } from './ClearTransactions'

export const ClearTransactions = {
path: 'clear-transactions/:newActiveRouteId',
element: <Component />,
action,
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ export const useClearTransactions = () => {
const provider = useProvider()
const dispatch = useDispatch()

const hasTransactions = transactions.length > 0
const clearTransactions = useCallback(async () => {
return useCallback(async () => {
if (transactions.length === 0) {
return
}
Expand All @@ -22,7 +21,5 @@ export const useClearTransactions = () => {
if (provider instanceof ForkProvider) {
await provider.deleteFork()
}
}, [provider, transactions, dispatch])

return { hasTransactions, clearTransactions }
}, [dispatch, transactions, provider])
}
4 changes: 3 additions & 1 deletion extension/src/panel/pages/$activeRouteId/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { redirect, type RouteObject } from 'react-router'
import { ActiveRoute as Component, loader } from './ActiveRoute'
import { ClearTransactions } from './clear-transactions.$newActiveRouteId'
import { Transactions } from './transactions'

export const ActiveRoute: RouteObject = {
path: '/:activeRouteId',
element: <Component />,
loader,
children: [
{ path: '', loader: () => redirect('transactions') },
{ index: true, loader: () => redirect('transactions') },
Transactions,
ClearTransactions,
],
}
34 changes: 18 additions & 16 deletions extension/src/panel/pages/ClearTransactionsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import { GhostButton, Modal, PrimaryButton } from '@/components'
import { useClearTransactions } from './useClearTransactions'
import {
GhostButton,
InlineForm,
Modal,
PrimaryButton,
type InlineFormContext,
} from '@/components'

type ClearTransactionsModalProps = {
open: boolean
newActiveRouteId: string
additionalContext?: InlineFormContext
intent: string
onClose: () => void
onConfirm: () => void
}

export const ClearTransactionsModal = ({
open,
newActiveRouteId,
intent,
additionalContext,
onClose,
onConfirm,
}: ClearTransactionsModalProps) => {
const { clearTransactions } = useClearTransactions()

return (
<Modal
open={open}
Expand All @@ -27,16 +34,11 @@ export const ClearTransactionsModal = ({
Cancel
</GhostButton>

<PrimaryButton
style="contrast"
onClick={() => {
clearTransactions()
onClose()
onConfirm()
}}
>
Clear transactions
</PrimaryButton>
<InlineForm context={{ newActiveRouteId, ...additionalContext }}>
<PrimaryButton submit intent={intent} style="contrast">
Clear transactions
</PrimaryButton>
</InlineForm>
</Modal.Actions>
</Modal>
)
Expand Down
51 changes: 51 additions & 0 deletions extension/src/panel/pages/routes/edit.$routeId/EditRoute.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,57 @@ describe('Edit Zodiac route', () => {
screen.queryByRole('dialog', { name: 'Clear transactions' }),
).not.toBeInTheDocument()
})

it('is possible to launch a new route and clear transactions', async () => {
const selectedRoute = createMockRoute({
id: 'firstRoute',
label: 'First route',
avatar: randomPrefixedAddress(),
})

await mockRoutes(selectedRoute, {
id: 'another-route',
avatar: randomPrefixedAddress(),
})
await saveLastUsedRouteId('another-route')

await render(
'/routes/edit/firstRoute',
[
{
path: '/routes/edit/:routeId',
Component: EditRoute,
loader,
action,
},
],
{
initialState: [createTransaction()],
inspectRoutes: [
'/:activeRouteId/clear-transactions/:newActiveRouteId',
],
},
)

await userEvent.click(
screen.getByRole('button', { name: 'Clear piloted Safe' }),
)

await userEvent.type(
screen.getByRole('textbox', { name: 'Piloted Safe' }),
randomAddress(),
)

await userEvent.click(
screen.getByRole('button', { name: 'Save & Launch' }),
)

await userEvent.click(
screen.getByRole('button', { name: 'Clear transactions' }),
)

await expectRouteToBe('/another-route/clear-transactions/firstRoute')
})
})
})
})
29 changes: 28 additions & 1 deletion extension/src/panel/pages/routes/edit.$routeId/EditRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,31 @@ export const action = async ({ params, request }: ActionFunctionArgs) => {

return redirect(`/${routeId}`)
}

case Intent.clearTransactions: {
const lastUsedRouteId = await getLastUsedRouteId()

await saveRoute(
fromLegacyConnection({
id: routeId,
label: getString(data, 'label'),
chainId: getInt(data, 'chainId') as ChainId,
avatarAddress: getString(data, 'avatarAddress'),
moduleAddress: getString(data, 'moduleAddress'),
pilotAddress: getString(data, 'pilotAddress'),
providerType: getInt(data, 'providerType'),
moduleType: getOptionalString(
data,
'moduleType',
) as SupportedModuleType,
multisend: getOptionalString(data, 'multisend'),
multisendCallOnly: getOptionalString(data, 'multisendCallOnly'),
roleId: getOptionalString(data, 'roleId'),
}),
)

return redirect(`/${lastUsedRouteId}/clear-transactions/${routeId}`)
}
}
}

Expand Down Expand Up @@ -442,9 +467,11 @@ export const EditRoute = () => {
</Page>

<ClearTransactionsModal
newActiveRouteId={initialRouteState.id}
additionalContext={{ ...legacyConnection }}
open={confirmClearTransactions}
intent={Intent.clearTransactions}
onClose={() => setConfirmClearTransactions(false)}
onConfirm={() => submit(formRef.current, { method: 'post' })}
/>
</>
)
Expand Down
1 change: 1 addition & 0 deletions extension/src/panel/pages/routes/edit.$routeId/intents.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export enum Intent {
saveRoute = 'saveRoute',
removeRoute = 'removeRoute',
clearTransactions = 'clearTransactions',
}
10 changes: 7 additions & 3 deletions extension/src/panel/pages/routes/list/ListRoutes.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ETH_ZERO_ADDRESS, ZERO_ADDRESS } from '@/chains'
import { getRoutes } from '@/execution-routes'
import { getRoutes, saveLastUsedRouteId } from '@/execution-routes'
import {
connectMockWallet,
createMockRoute,
Expand Down Expand Up @@ -63,6 +63,8 @@ describe('List routes', () => {
label: 'First route',
})

await saveLastUsedRouteId('firstRoute')

mockRoutes(selectedRoute, { id: 'secondRoute', label: 'Second route' })

await render(
Expand All @@ -71,7 +73,9 @@ describe('List routes', () => {
{
initialSelectedRoute: selectedRoute,
initialState: [createTransaction()],
inspectRoutes: ['/:activeRouteId'],
inspectRoutes: [
'/:activeRouteId/clear-transactions/:newActiveRouteId',
],
},
)

Expand All @@ -84,7 +88,7 @@ describe('List routes', () => {
screen.getByRole('button', { name: 'Clear transactions' }),
)

await expectRouteToBe('/secondRoute')
await expectRouteToBe('/firstRoute/clear-transactions/secondRoute')
})
})

Expand Down
12 changes: 11 additions & 1 deletion extension/src/panel/pages/routes/list/ListRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Breadcrumbs, InlineForm, Page, PrimaryButton } from '@/components'
import { createRoute, getRoutes } from '@/execution-routes'
import { createRoute, getLastUsedRouteId, getRoutes } from '@/execution-routes'
import { getString } from '@/utils'
import { Plus } from 'lucide-react'
import { redirect, useLoaderData, type ActionFunctionArgs } from 'react-router'
Expand Down Expand Up @@ -27,6 +27,16 @@ export const action = async ({ request }: ActionFunctionArgs) => {

return redirect(`/${routeId}`)
}

case Intent.clearTransactions: {
const currentlyActiveRouteId = await getLastUsedRouteId()

const newActiveRouteId = getString(data, 'newActiveRouteId')

return redirect(
`/${currentlyActiveRouteId}/clear-transactions/${newActiveRouteId}`,
)
}
}
}

Expand Down
5 changes: 2 additions & 3 deletions extension/src/panel/pages/routes/list/Route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import type { ExecutionRoute } from '@/types'
import { formatDistanceToNow } from 'date-fns'
import { Cable, PlugZap, Unplug } from 'lucide-react'
import { useRef, useState } from 'react'
import { useSubmit } from 'react-router'
import { ClearTransactionsModal } from '../../ClearTransactionsModal'
import { ConnectionStack } from '../../ConnectionStack'
import { asLegacyConnection } from '../../legacyConnectionMigrations'
Expand All @@ -26,7 +25,6 @@ export const Route = ({ route }: RouteProps) => {
const [confirmClearTransactions, setConfirmClearTransactions] =
useState(false)
const transactions = useTransactions()
const submit = useSubmit()
const formRef = useRef(null)

return (
Expand Down Expand Up @@ -102,9 +100,10 @@ export const Route = ({ route }: RouteProps) => {
</section>

<ClearTransactionsModal
newActiveRouteId={route.id}
intent={Intent.clearTransactions}
open={confirmClearTransactions}
onClose={() => setConfirmClearTransactions(false)}
onConfirm={() => submit(formRef.current, { method: 'post' })}
/>
</>
)
Expand Down
1 change: 1 addition & 0 deletions extension/src/panel/pages/routes/list/intents.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export enum Intent {
addRoute = 'addRoute',
launchRoute = 'launchRoute',
clearTransactions = 'clearTransactions',
}
Loading