From 93bfb6fe3115c6cbc096773519de2717963d85f8 Mon Sep 17 00:00:00 2001 From: Philipp Giese <187786+frontendphil@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:40:51 +0100 Subject: [PATCH] chore: clear transactions on special route (#392) * list routes redirects to clear-transactions route instead of doing the work * add failing test * redirect from edit route to new clear transactions route * implement clear transactions route --- extension/src/components/InlineForm.tsx | 4 +- extension/src/components/index.ts | 1 + .../ClearTransactions.spec.ts | 45 ++++++++++++++++ .../ClearTransactions.tsx | 20 ++++++++ .../index.tsx | 7 +++ .../useClearTransactions.ts | 7 +-- .../src/panel/pages/$activeRouteId/index.tsx | 4 +- .../panel/pages/ClearTransactionsModal.tsx | 34 +++++++------ .../routes/edit.$routeId/EditRoute.spec.tsx | 51 +++++++++++++++++++ .../pages/routes/edit.$routeId/EditRoute.tsx | 29 ++++++++++- .../pages/routes/edit.$routeId/intents.ts | 1 + .../pages/routes/list/ListRoutes.spec.ts | 10 ++-- .../panel/pages/routes/list/ListRoutes.tsx | 12 ++++- .../src/panel/pages/routes/list/Route.tsx | 5 +- .../src/panel/pages/routes/list/intents.ts | 1 + 15 files changed, 200 insertions(+), 31 deletions(-) create mode 100644 extension/src/panel/pages/$activeRouteId/clear-transactions.$newActiveRouteId/ClearTransactions.spec.ts create mode 100644 extension/src/panel/pages/$activeRouteId/clear-transactions.$newActiveRouteId/ClearTransactions.tsx create mode 100644 extension/src/panel/pages/$activeRouteId/clear-transactions.$newActiveRouteId/index.tsx rename extension/src/panel/pages/{ => $activeRouteId/clear-transactions.$newActiveRouteId}/useClearTransactions.ts (74%) diff --git a/extension/src/components/InlineForm.tsx b/extension/src/components/InlineForm.tsx index 56303b29f..91dafcc87 100644 --- a/extension/src/components/InlineForm.tsx +++ b/extension/src/components/InlineForm.tsx @@ -1,8 +1,10 @@ import type { ComponentPropsWithRef } from 'react' import { Form } from 'react-router' +export type InlineFormContext = Record + type InlineFormProps = ComponentPropsWithRef & { - context?: Record + context?: InlineFormContext intent?: string } diff --git a/extension/src/components/index.ts b/extension/src/components/index.ts index 7547015fe..26f4a8f6a 100644 --- a/extension/src/components/index.ts +++ b/extension/src/components/index.ts @@ -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' diff --git a/extension/src/panel/pages/$activeRouteId/clear-transactions.$newActiveRouteId/ClearTransactions.spec.ts b/extension/src/panel/pages/$activeRouteId/clear-transactions.$newActiveRouteId/ClearTransactions.spec.ts new file mode 100644 index 000000000..4c46e7883 --- /dev/null +++ b/extension/src/panel/pages/$activeRouteId/clear-transactions.$newActiveRouteId/ClearTransactions.spec.ts @@ -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') + }) +}) diff --git a/extension/src/panel/pages/$activeRouteId/clear-transactions.$newActiveRouteId/ClearTransactions.tsx b/extension/src/panel/pages/$activeRouteId/clear-transactions.$newActiveRouteId/ClearTransactions.tsx new file mode 100644 index 000000000..159200a54 --- /dev/null +++ b/extension/src/panel/pages/$activeRouteId/clear-transactions.$newActiveRouteId/ClearTransactions.tsx @@ -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 +} diff --git a/extension/src/panel/pages/$activeRouteId/clear-transactions.$newActiveRouteId/index.tsx b/extension/src/panel/pages/$activeRouteId/clear-transactions.$newActiveRouteId/index.tsx new file mode 100644 index 000000000..d63a02767 --- /dev/null +++ b/extension/src/panel/pages/$activeRouteId/clear-transactions.$newActiveRouteId/index.tsx @@ -0,0 +1,7 @@ +import { action, ClearTransactions as Component } from './ClearTransactions' + +export const ClearTransactions = { + path: 'clear-transactions/:newActiveRouteId', + element: , + action, +} diff --git a/extension/src/panel/pages/useClearTransactions.ts b/extension/src/panel/pages/$activeRouteId/clear-transactions.$newActiveRouteId/useClearTransactions.ts similarity index 74% rename from extension/src/panel/pages/useClearTransactions.ts rename to extension/src/panel/pages/$activeRouteId/clear-transactions.$newActiveRouteId/useClearTransactions.ts index 99511471b..e1bf2e23a 100644 --- a/extension/src/panel/pages/useClearTransactions.ts +++ b/extension/src/panel/pages/$activeRouteId/clear-transactions.$newActiveRouteId/useClearTransactions.ts @@ -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 } @@ -22,7 +21,5 @@ export const useClearTransactions = () => { if (provider instanceof ForkProvider) { await provider.deleteFork() } - }, [provider, transactions, dispatch]) - - return { hasTransactions, clearTransactions } + }, [dispatch, transactions, provider]) } diff --git a/extension/src/panel/pages/$activeRouteId/index.tsx b/extension/src/panel/pages/$activeRouteId/index.tsx index 8e5f591d4..b29babf00 100644 --- a/extension/src/panel/pages/$activeRouteId/index.tsx +++ b/extension/src/panel/pages/$activeRouteId/index.tsx @@ -1,5 +1,6 @@ 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 = { @@ -7,7 +8,8 @@ export const ActiveRoute: RouteObject = { element: , loader, children: [ - { path: '', loader: () => redirect('transactions') }, + { index: true, loader: () => redirect('transactions') }, Transactions, + ClearTransactions, ], } diff --git a/extension/src/panel/pages/ClearTransactionsModal.tsx b/extension/src/panel/pages/ClearTransactionsModal.tsx index 83f7d1328..56d79ffd5 100644 --- a/extension/src/panel/pages/ClearTransactionsModal.tsx +++ b/extension/src/panel/pages/ClearTransactionsModal.tsx @@ -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 ( - { - clearTransactions() - onClose() - onConfirm() - }} - > - Clear transactions - + + + Clear transactions + + ) diff --git a/extension/src/panel/pages/routes/edit.$routeId/EditRoute.spec.tsx b/extension/src/panel/pages/routes/edit.$routeId/EditRoute.spec.tsx index 16613663e..51c6e47fc 100644 --- a/extension/src/panel/pages/routes/edit.$routeId/EditRoute.spec.tsx +++ b/extension/src/panel/pages/routes/edit.$routeId/EditRoute.spec.tsx @@ -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') + }) }) }) }) diff --git a/extension/src/panel/pages/routes/edit.$routeId/EditRoute.tsx b/extension/src/panel/pages/routes/edit.$routeId/EditRoute.tsx index 363b17271..3144b3c44 100644 --- a/extension/src/panel/pages/routes/edit.$routeId/EditRoute.tsx +++ b/extension/src/panel/pages/routes/edit.$routeId/EditRoute.tsx @@ -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}`) + } } } @@ -442,9 +467,11 @@ export const EditRoute = () => { setConfirmClearTransactions(false)} - onConfirm={() => submit(formRef.current, { method: 'post' })} /> ) diff --git a/extension/src/panel/pages/routes/edit.$routeId/intents.ts b/extension/src/panel/pages/routes/edit.$routeId/intents.ts index 55dcbe788..827a4cd37 100644 --- a/extension/src/panel/pages/routes/edit.$routeId/intents.ts +++ b/extension/src/panel/pages/routes/edit.$routeId/intents.ts @@ -1,4 +1,5 @@ export enum Intent { saveRoute = 'saveRoute', removeRoute = 'removeRoute', + clearTransactions = 'clearTransactions', } diff --git a/extension/src/panel/pages/routes/list/ListRoutes.spec.ts b/extension/src/panel/pages/routes/list/ListRoutes.spec.ts index 0420fba7c..e640a6ea1 100644 --- a/extension/src/panel/pages/routes/list/ListRoutes.spec.ts +++ b/extension/src/panel/pages/routes/list/ListRoutes.spec.ts @@ -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, @@ -63,6 +63,8 @@ describe('List routes', () => { label: 'First route', }) + await saveLastUsedRouteId('firstRoute') + mockRoutes(selectedRoute, { id: 'secondRoute', label: 'Second route' }) await render( @@ -71,7 +73,9 @@ describe('List routes', () => { { initialSelectedRoute: selectedRoute, initialState: [createTransaction()], - inspectRoutes: ['/:activeRouteId'], + inspectRoutes: [ + '/:activeRouteId/clear-transactions/:newActiveRouteId', + ], }, ) @@ -84,7 +88,7 @@ describe('List routes', () => { screen.getByRole('button', { name: 'Clear transactions' }), ) - await expectRouteToBe('/secondRoute') + await expectRouteToBe('/firstRoute/clear-transactions/secondRoute') }) }) diff --git a/extension/src/panel/pages/routes/list/ListRoutes.tsx b/extension/src/panel/pages/routes/list/ListRoutes.tsx index e866e83e2..40a73814b 100644 --- a/extension/src/panel/pages/routes/list/ListRoutes.tsx +++ b/extension/src/panel/pages/routes/list/ListRoutes.tsx @@ -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' @@ -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}`, + ) + } } } diff --git a/extension/src/panel/pages/routes/list/Route.tsx b/extension/src/panel/pages/routes/list/Route.tsx index b79eb8fb8..f6d744d17 100644 --- a/extension/src/panel/pages/routes/list/Route.tsx +++ b/extension/src/panel/pages/routes/list/Route.tsx @@ -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' @@ -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 ( @@ -102,9 +100,10 @@ export const Route = ({ route }: RouteProps) => { setConfirmClearTransactions(false)} - onConfirm={() => submit(formRef.current, { method: 'post' })} /> ) diff --git a/extension/src/panel/pages/routes/list/intents.ts b/extension/src/panel/pages/routes/list/intents.ts index a6d46f1b7..3f9fd8702 100644 --- a/extension/src/panel/pages/routes/list/intents.ts +++ b/extension/src/panel/pages/routes/list/intents.ts @@ -1,4 +1,5 @@ export enum Intent { addRoute = 'addRoute', launchRoute = 'launchRoute', + clearTransactions = 'clearTransactions', }