Skip to content

Commit

Permalink
chore: restructure pages to resemble an actual web-app; use loaders a…
Browse files Browse the repository at this point in the history
…nd actions (#340)

* use loader to pull routes in list routes and edit route component

* start handling the case of no routes

* move routes around

* create more focussed context

* reorg context and app structure more

* remove some obsolete code

* migrate useSelectedRouteId hook

* pass state more directly

* use react-router instead of react-router-dom

* adjust list routes spec

* adjust edit route spec

* port more things into loaders

* adjust list routes component

* adjust edit link

* update breadcrumbs

* move list and edit routes out of active route scope

* add action to no route component

* fix: do not break on IDs that contain `-`

* make sure to correctly save initial route

* add add new route on list

* edit route requires route to be present

* use loader for transactions

* remove loader from transactions again

* resolve dependency cycles

* adjust specs again matching routes

* adjust e2e tests

* remove route through action

* add handling for removing the last route

* add createRoute helper

* adjust edit spec to also use action

* adjust e2e test
  • Loading branch information
frontendphil authored Dec 12, 2024
1 parent e096e9c commit c9f3cb4
Show file tree
Hide file tree
Showing 78 changed files with 699 additions and 454 deletions.
1 change: 0 additions & 1 deletion extension/e2e/accountHandling/lockedAccount.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const openConfiguration = async (
page: Page,
account: `0x${string}` = defaultMockAccount,
) => {
await page.getByRole('link', { name: 'Configure routes' }).click()
await page.getByRole('button', { name: 'Add Route' }).click()

// MONDAY PHIL: This button is not being enabled
Expand Down
1 change: 0 additions & 1 deletion extension/e2e/accountHandling/unavailableWallet.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const openConfiguration = async (
page: Page,
account: `0x${string}` = defaultMockAccount,
) => {
await page.getByRole('link', { name: 'Configure routes' }).click()
await page.getByRole('button', { name: 'Add Route' }).click()
await page.getByRole('button', { name: 'Connect with MetaMask' }).click()
await expect(
Expand Down
1 change: 0 additions & 1 deletion extension/e2e/accountHandling/wrongChain.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const openConfiguration = async (
page: Page,
account: `0x${string}` = defaultMockAccount,
) => {
await page.getByRole('link', { name: 'Configure routes' }).click()
await page.getByRole('button', { name: 'Add Route' }).click()
await page.getByRole('button', { name: 'Connect with MetaMask' }).click()
await expect(
Expand Down
2 changes: 1 addition & 1 deletion extension/e2e/smoketest.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ test('possibility to open the panel', async ({ page }) => {

await expect(
extension.getByRole('heading', {
name: 'Recording Transactions',
name: 'Welcome to Zodiac Pilot',
}),
).toBeInViewport()
})
2 changes: 1 addition & 1 deletion extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
"date-fns": "^4.1.0",
"lucide-react": "^0.468.0",
"react-toastify": "10.0.6",
"react-router-dom": "7.0.2",
"react-router": "7.0.2",
"react-stick": "^5.0.6",
"zod": "^3.23.8"
}
Expand Down
2 changes: 1 addition & 1 deletion extension/src/components/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Children, type ComponentProps, type PropsWithChildren } from 'react'
import { Link } from 'react-router-dom'
import { Link } from 'react-router'

export const Breadcrumbs = ({ children }: PropsWithChildren) => (
<div className="flex items-center gap-2 font-mono text-xs uppercase opacity-75">
Expand Down
7 changes: 5 additions & 2 deletions extension/src/components/buttons/BaseButton.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import classNames from 'classnames'
import type { LucideIcon } from 'lucide-react'
import type { ComponentPropsWithoutRef } from 'react'
import { Link } from 'react-router-dom'
import { Link } from 'react-router'

type SharedButtonProps = {
fluid?: boolean
iconOnly?: boolean
icon?: LucideIcon
size?: 'small' | 'base'
submit?: boolean
}

export type BaseButtonProps = ComponentPropsWithoutRef<'button'> &
export type BaseButtonProps = Omit<ComponentPropsWithoutRef<'button'>, 'type'> &
SharedButtonProps

export const BaseButton = ({
Expand All @@ -21,10 +22,12 @@ export const BaseButton = ({
size = 'base',
children,
title,
submit = false,
...props
}: BaseButtonProps) => (
<button
{...props}
type={submit ? 'submit' : undefined}
title={title ? title : typeof children === 'string' ? children : undefined}
className={classNames(
'flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md border text-sm transition-all disabled:cursor-not-allowed disabled:opacity-60',
Expand Down
13 changes: 5 additions & 8 deletions extension/src/panel/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ import { ProvideInjectedWallet } from '@/providers'
import { invariant } from '@epic-web/invariant'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { createHashRouter, RouterProvider } from 'react-router-dom'
import { createHashRouter, RouterProvider } from 'react-router'
import { ToastContainer } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.min.css'
import '../global.css'
import { pages } from './pages'
import { ProvideProvider } from './providers-ui'
import { ProvideState } from './state'
import { usePilotPort } from './usePilotPort'

Expand All @@ -38,13 +37,11 @@ const Root = () => {
<ProvideState>
<ProvideExecutionRoutes>
<ProvideInjectedWallet>
<ProvideProvider>
<div className="flex h-full flex-1 flex-col">
<RouterProvider router={router} />
</div>
<div className="flex h-full flex-1 flex-col">
<RouterProvider router={router} />
</div>

<ToastContainer position="top-center" />
</ProvideProvider>
<ToastContainer position="top-center" />
</ProvideInjectedWallet>
</ProvideExecutionRoutes>
</ProvideState>
Expand Down
30 changes: 30 additions & 0 deletions extension/src/panel/execution-routes/ExecutionRouteContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { ExecutionRoute } from '@/types'
import { invariant } from '@epic-web/invariant'
import { createContext, useContext, type PropsWithChildren } from 'react'

const ExecutionRouteContext = createContext<{ route: ExecutionRoute | null }>({
route: null,
})

export const ProvideExecutionRoute = ({
route,
children,
}: PropsWithChildren<{ route: ExecutionRoute }>) => (
<ExecutionRouteContext.Provider value={{ route }}>
{children}
</ExecutionRouteContext.Provider>
)

export const useExecutionRoute = () => {
const { route } = useContext(ExecutionRouteContext)

invariant(route != null, 'Could not find active route on context')

return route
}

export const useSelectedRouteId = () => {
const route = useExecutionRoute()

return route.id
}
77 changes: 4 additions & 73 deletions extension/src/panel/execution-routes/ExecutionRoutesContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,29 @@ import {
type PropsWithChildren,
useCallback,
useContext,
useEffect,
useMemo,
} from 'react'
import { useFunctionRef, useStorageEntries } from '../utils'
import {
ProvideSelectedExecutionRoute,
useSelectedRouteId,
} from './SelectedRouteContext'
import { useStorageEntries } from '../utils'

type Context = {
routes: ExecutionRoute[]
saveRoute: (route: ExecutionRoute) => void
removeRoute: (routeId: string) => void
}

const ExecutionRoutesContext = createContext<Context>({
routes: [],
saveRoute() {
throw new Error(
'"saveRoute" is not available outside of `<ProvideExecutionRoutes />` context.',
)
},
removeRoute() {
throw new Error(
'"removeRoute" is not available outside of `<ProvideExecutionRoutes /> context.',
)
},
})

type ProvideExecutionRoutesProps = PropsWithChildren<{
initialSelectedRouteId?: string
}>
type ProvideExecutionRoutesProps = PropsWithChildren

export const ProvideExecutionRoutes = ({
children,
initialSelectedRouteId,
}: ProvideExecutionRoutesProps) => {
// we store routes as individual storage entries to alleviate concurrent write issues and to avoid running into the 8kb storage entry limit
// (see: https://developer.chrome.com/docs/extensions/reference/api/storage#property-sync)
const [routes, setRoute, removeRoute] =
useStorageEntries<ExecutionRoute>('routes')
const [, setRoute] = useStorageEntries<ExecutionRoute>('routes')

const saveRoute = useCallback(
(route: ExecutionRoute) => {
Expand All @@ -53,70 +35,19 @@ export const ProvideExecutionRoutes = ({
[setRoute],
)

const routesList = useMemo(
() => (routes == null ? [] : Object.values(routes)),
[routes],
)

// wait for routes to be loaded from storage
if (!routes) {
return null
}

return (
<ExecutionRoutesContext.Provider
value={{
routes: routesList,
saveRoute,
removeRoute,
}}
>
<ProvideSelectedExecutionRoute initialValue={initialSelectedRouteId}>
{children}
</ProvideSelectedExecutionRoute>
{children}
</ExecutionRoutesContext.Provider>
)
}

export const useExecutionRoutes = () => {
const { routes } = useContext(ExecutionRoutesContext)

return routes
}

export const useSaveExecutionRoute = () => {
const { saveRoute } = useContext(ExecutionRoutesContext)

return saveRoute
}

export const useRemoveExecutionRoute = () => {
const { removeRoute } = useContext(ExecutionRoutesContext)

return removeRoute
}

export const useMarkRouteAsUsed = () => {
const [selectedRouteId] = useSelectedRouteId()
const routes = useExecutionRoutes()
const saveRoute = useSaveExecutionRoute()

const updateRef = useFunctionRef((routeId) => {
if (routeId == null) {
return
}

const route = routes.find((route) => route.id === routeId)

if (route == null) {
return
}

saveRoute({ ...route, lastUsed: Date.now() })
})

useEffect(() => {
console.debug('update last used timestamp for route', selectedRouteId)
updateRef.current!(selectedRouteId)
}, [selectedRouteId, updateRef])
}
53 changes: 0 additions & 53 deletions extension/src/panel/execution-routes/SelectedRouteContext.tsx

This file was deleted.

14 changes: 14 additions & 0 deletions extension/src/panel/execution-routes/createRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ETH_ZERO_ADDRESS } from '@/chains'
import { ProviderType } from '@/types'
import { nanoid } from 'nanoid'
import { saveRoute } from './saveRoute'

export const createRoute = () =>
saveRoute({
id: nanoid(),
label: '',
providerType: ProviderType.InjectedWallet,
avatar: ETH_ZERO_ADDRESS,
initiator: undefined,
waypoints: undefined,
})
4 changes: 4 additions & 0 deletions extension/src/panel/execution-routes/getLastUsedRouteId.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { getStorageEntry } from '../utils'

export const getLastUsedRouteId = () =>
getStorageEntry<string | null>({ key: 'lastUsedRoute' })
14 changes: 14 additions & 0 deletions extension/src/panel/execution-routes/getRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { type ExecutionRoute } from '@/types'
import { invariant } from '@epic-web/invariant'
import { getStorageEntry } from '../utils'

export const getRoute = async (routeId: string) => {
const route = await getStorageEntry<ExecutionRoute | undefined>({
collection: 'routes',
key: routeId,
})

invariant(route != null, `Could not find route with id "${routeId}"`)

return route
}
8 changes: 8 additions & 0 deletions extension/src/panel/execution-routes/getRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { ExecutionRoute } from '@/types'
import { getStorageEntries } from '../utils'

export const getRoutes = async () => {
const routes = await getStorageEntries<ExecutionRoute>('routes')

return Object.values(routes)
}
18 changes: 13 additions & 5 deletions extension/src/panel/execution-routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
export { createRoute } from './createRoute'
export {
ProvideExecutionRoute,
useExecutionRoute,
useSelectedRouteId,
} from './ExecutionRouteContext'
export {
ProvideExecutionRoutes,
useExecutionRoutes,
useMarkRouteAsUsed,
useRemoveExecutionRoute,
useSaveExecutionRoute,
} from './ExecutionRoutesContext'
export { useSelectedRouteId } from './SelectedRouteContext'
export { INITIAL_DEFAULT_ROUTE, useExecutionRoute } from './useExecutionRoute'
export { getLastUsedRouteId } from './getLastUsedRouteId'
export { getRoute } from './getRoute'
export { getRoutes } from './getRoutes'
export { markRouteAsUsed } from './markRouteAsUsed'
export { removeRoute } from './removeRoute'
export { saveLastUsedRouteId } from './saveLastUsedRouteId'
export { saveRoute } from './saveRoute'
export { useRouteConnect } from './useRouteConnect'
export { useRouteProvider } from './useRouteProvider'
5 changes: 5 additions & 0 deletions extension/src/panel/execution-routes/markRouteAsUsed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { ExecutionRoute } from '@/types'
import { saveRoute } from './saveRoute'

export const markRouteAsUsed = (route: ExecutionRoute) =>
saveRoute({ ...route, lastUsed: Date.now() })
Loading

0 comments on commit c9f3cb4

Please sign in to comment.