feat: add recheck button to OrderbookEmptyAlert

This commit is contained in:
theborakompanioni 2026-08-11 19:43:58 +02:00
parent c7fef02ac7
commit 5ddb9a5f00
No known key found for this signature in database
GPG key ID: E8070AF0053AAC0D
6 changed files with 60 additions and 10 deletions

View file

@ -186,7 +186,12 @@ export function SendForm({
const {
hasOrders,
queryResult: { isLoading: orderbookCheckIsLoading, isError: orderbookCheckIsError },
queryResult: {
isLoading: orderbookCheckIsLoading,
isFetching: orderbookCheckIsFetching,
isError: orderbookCheckIsError,
refetch: orderbookRefetch,
},
} = useQueryOrderbook()
const destinationAddressInfo = useMemo(() => {
@ -369,7 +374,9 @@ export function SendForm({
{hasCoinjoinPreconditionWarning && coinjoinPreconditionSummary && (
<SendCoinjoinPreconditionAlert summary={coinjoinPreconditionSummary} />
)}
{isCoinJoin && !orderbookCheckIsLoading && !orderbookCheckIsError && !hasOrders && <OrderbookEmptyAlert />}
{isCoinJoin && !orderbookCheckIsLoading && !orderbookCheckIsError && !hasOrders && (
<OrderbookEmptyAlert isChecking={orderbookCheckIsFetching} onCheckClick={orderbookRefetch} />
)}
</div>
<div className="space-y-2">

View file

@ -76,7 +76,12 @@ export const SweepPage = ({ walletFileName }: SweepPageProps) => {
const [alertMessage, setAlertMessage] = useState<string>()
const {
hasOrders,
queryResult: { isLoading: orderbookCheckIsLoading, isError: orderbookCheckIsError },
queryResult: {
isLoading: orderbookCheckIsLoading,
isError: orderbookCheckIsError,
isFetching: orderbookCheckIsFetching,
refetch: orderbookRefetch,
},
} = useQueryOrderbook()
const feeConfigValidation = useFeeConfigValidation({ walletFileName })
@ -290,7 +295,9 @@ export const SweepPage = ({ walletFileName }: SweepPageProps) => {
<FeeConfigErrorAlert onOpenFeeConfig={() => setShowFeeConfigDialog(true)} className="mb-4" />
)}
{!orderbookCheckIsLoading && !orderbookCheckIsError && !hasOrders && <OrderbookEmptyAlert className="mb-4" />}
{!orderbookCheckIsLoading && !orderbookCheckIsError && !hasOrders && (
<OrderbookEmptyAlert isChecking={orderbookCheckIsFetching} onCheckClick={orderbookRefetch} />
)}
{alertMessage && (
<Alert variant="destructive">

View file

@ -20,11 +20,27 @@ vi.mock('react-router-dom', () => ({
}))
describe('OrderbookEmptyAlert', () => {
it('renders title, description and link to orderbook page', () => {
render(<OrderbookEmptyAlert />)
it('renders title, description, button and link to orderbook page', () => {
render(<OrderbookEmptyAlert isChecking={false} onCheckClick={async () => {}} />)
expect(screen.getByText('orderbook.alert_precheck_empty_title')).toBeInTheDocument()
expect(screen.getByText(/orderbook.alert_precheck_empty_description/u)).toBeInTheDocument()
expect(screen.getByRole('link')).toHaveAttribute('href', '/orderbook')
const actionCheck = screen.getByRole('button', { name: 'orderbook.alert_precheck_empty_text_button_check' })
expect(actionCheck).toBeEnabled()
const actionChecking = screen.queryByRole('button', { name: 'orderbook.alert_precheck_empty_text_button_checking' })
expect(actionChecking).not.toBeInTheDocument()
})
it('disables button while checking', () => {
render(<OrderbookEmptyAlert isChecking={true} onCheckClick={async () => {}} />)
const actionChecking = screen.getByRole('button', { name: 'orderbook.alert_precheck_empty_text_button_checking' })
expect(actionChecking).toBeDisabled()
const actionCheck = screen.queryByRole('button', { name: 'orderbook.alert_precheck_empty_text_button_check' })
expect(actionCheck).not.toBeInTheDocument()
})
})

View file

@ -1,14 +1,17 @@
import { AlertTriangleIcon } from 'lucide-react'
import { AlertTriangleIcon, RefreshCwIcon } from 'lucide-react'
import { Trans, useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Alert, AlertAction, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { routes } from '@/constants/routes'
import { Button } from '../button'
interface OrderbookEmptyAlertProps {
className?: string
isChecking?: boolean
onCheckClick?: () => Promise<unknown>
}
export const OrderbookEmptyAlert = ({ className }: OrderbookEmptyAlertProps) => {
export const OrderbookEmptyAlert = ({ className, isChecking, onCheckClick }: OrderbookEmptyAlertProps) => {
const { t } = useTranslation()
return (
@ -23,6 +26,16 @@ export const OrderbookEmptyAlert = ({ className }: OrderbookEmptyAlertProps) =>
}}
/>
</AlertDescription>
{onCheckClick && (
<AlertAction>
<Button size="xs" type="button" onClick={() => void onCheckClick()} disabled={isChecking}>
<RefreshCwIcon className={isChecking ? 'motion-safe:animate-spin' : undefined} />
{isChecking
? t('orderbook.alert_precheck_empty_text_button_checking')
: t('orderbook.alert_precheck_empty_text_button_check')}
</Button>
</AlertAction>
)}
</Alert>
)
}

View file

@ -1,5 +1,6 @@
import { useQuery, type UseQueryResult } from '@tanstack/react-query'
import { fetchOrderbook, type OrderbookResponse } from '@/lib/api/orderbook'
import { withQueryDelay } from '@/lib/queryClient'
export type UseQueryOrderbookResult = {
hasOrders: boolean
@ -9,7 +10,11 @@ export type UseQueryOrderbookResult = {
export function useQueryOrderbook(): UseQueryOrderbookResult {
const queryResult = useQuery({
queryKey: ['orderbook-precheck'],
queryFn: fetchOrderbook,
queryFn: withQueryDelay(fetchOrderbook, {
// avoid flickering and let user briefly know that something is happening in the background
delayBefore: 1_000,
}),
staleTime: 30 * 1_000,
retry: false,
})

View file

@ -755,6 +755,8 @@
"alert_empty_orderbook": "Orderbook is empty",
"alert_precheck_empty_title": "No active makers found",
"alert_precheck_empty_description": "Your local Orderbook appears to be empty right now. Collaborative transactions might fail. Please check <1>your local Orderbook</1>.",
"alert_precheck_empty_text_button_check": "Check again",
"alert_precheck_empty_text_button_checking": "Checking...",
"error_loading_orderbook_failed": "Error while loading the orderbook. Your current local setup might not support fetching the orderbook. Reason: {{ reason }}",
"text_offer_type_absolute": "absolute",
"text_offer_type_relative": "relative",