mirror of
https://github.com/joinmarket-webui/jam.git
synced 2026-08-13 12:33:26 +02:00
refactor: standardize remaining dialog forms (#1358)
* refactor(earn): standardize move-to-jar form * refactor(earn): standardize bond renewal form * refactor(send): standardize utxo selection form * chore(settings): remove stale form migration todo * refactor(earn): rely on form confirmation default
This commit is contained in:
parent
01a952ff63
commit
0e24250e3d
11 changed files with 295 additions and 66 deletions
17
src/components/earn/MoveToJarDialog.schema.test.ts
Normal file
17
src/components/earn/MoveToJarDialog.schema.test.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { createMoveToJarFormSchema } from './MoveToJarDialog.schema'
|
||||
|
||||
describe('createMoveToJarFormSchema', () => {
|
||||
const schema = createMoveToJarFormSchema([0, 2])
|
||||
|
||||
it('accepts an available destination jar', async () => {
|
||||
await expect(schema.validate({ destinationJarIndex: 2 })).resolves.toEqual({ destinationJarIndex: 2 })
|
||||
})
|
||||
|
||||
it.each([{ destinationJarIndex: 1 }, { destinationJarIndex: undefined }])(
|
||||
'rejects an unavailable destination jar',
|
||||
async (values) => {
|
||||
await expect(schema.isValid(values)).resolves.toBe(false)
|
||||
},
|
||||
)
|
||||
})
|
||||
13
src/components/earn/MoveToJarDialog.schema.ts
Normal file
13
src/components/earn/MoveToJarDialog.schema.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import * as yup from 'yup'
|
||||
import type { JarIndex } from '@/types/global'
|
||||
|
||||
export type MoveToJarFormValues = {
|
||||
destinationJarIndex: JarIndex
|
||||
}
|
||||
|
||||
export const createMoveToJarFormSchema = (availableJarIndexes: JarIndex[]): yup.ObjectSchema<MoveToJarFormValues> =>
|
||||
yup
|
||||
.object({
|
||||
destinationJarIndex: yup.number().oneOf(availableJarIndexes).required(),
|
||||
})
|
||||
.required()
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import { useState } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import { getaddressOptions } from '@joinmarket-webui/joinmarket-ng-api-ts/@tanstack/react-query'
|
||||
import type { DirectSendResponse } from '@joinmarket-webui/joinmarket-ng-api-ts/jm'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { AlertTriangleIcon, UnlockIcon } from 'lucide-react'
|
||||
import { useForm, useWatch } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import { Alert, AlertTitle } from '@/components/ui/alert'
|
||||
|
|
@ -11,7 +13,7 @@ import { useJamWalletInfoContext } from '@/context/JamWalletInfoContext'
|
|||
import { useApiClient } from '@/hooks/useApiClient'
|
||||
import type { FidelityBondUtxo } from '@/hooks/useQueryUtxos'
|
||||
import type { WalletFileName } from '@/lib/utils'
|
||||
import type { JarIndex } from '@/types/global'
|
||||
import { createMoveToJarFormSchema, type MoveToJarFormValues } from './MoveToJarDialog.schema'
|
||||
import { FidelityBondDialogLayout } from './fidelity-bond/FidelityBondDialogLayout'
|
||||
import {
|
||||
AddressPreview,
|
||||
|
|
@ -45,9 +47,25 @@ export function MoveToJarDialog({ open, onOpenChange, walletFileName, utxo }: Mo
|
|||
const walletInfo = useJamWalletInfoContext()
|
||||
|
||||
const [step, setStep] = useState<Step>('select_jar')
|
||||
const [selectedJarIndex, setSelectedJarIndex] = useState<JarIndex | undefined>()
|
||||
const [txResult, setTxResult] = useState<DirectSendResponse | undefined>()
|
||||
|
||||
const availableJarIndexes = useMemo(() => walletInfo.jars.map((jar) => jar.jarIndex), [walletInfo.jars])
|
||||
const formSchema = useMemo(() => createMoveToJarFormSchema(availableJarIndexes), [availableJarIndexes])
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
formState: { isSubmitting },
|
||||
} = useForm<MoveToJarFormValues>({
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
destinationJarIndex: undefined,
|
||||
},
|
||||
resolver: yupResolver(formSchema),
|
||||
})
|
||||
const selectedJarIndex = useWatch({ control, name: 'destinationJarIndex' })
|
||||
|
||||
const { sweep, isLoading, error, setError, sourceJar } = useFidelityBondSweep({
|
||||
walletFileName,
|
||||
utxo,
|
||||
|
|
@ -77,7 +95,7 @@ export function MoveToJarDialog({ open, onOpenChange, walletFileName, utxo }: Mo
|
|||
|
||||
const handleReset = () => {
|
||||
setStep('select_jar')
|
||||
setSelectedJarIndex(undefined)
|
||||
reset()
|
||||
setTxResult(undefined)
|
||||
setError(undefined)
|
||||
}
|
||||
|
|
@ -89,8 +107,8 @@ export function MoveToJarDialog({ open, onOpenChange, walletFileName, utxo }: Mo
|
|||
onOpenChange(newOpen)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (selectedJarIndex === undefined || !destinationAddress) return
|
||||
const submitMove = handleSubmit(async () => {
|
||||
if (!destinationAddress) return
|
||||
|
||||
setStep('sending')
|
||||
const swept = await sweep(destinationAddress, (result) => {
|
||||
|
|
@ -99,7 +117,7 @@ export function MoveToJarDialog({ open, onOpenChange, walletFileName, utxo }: Mo
|
|||
toast.success(t('earn.fidelity_bond.move.success_text'))
|
||||
})
|
||||
if (!swept) setStep('confirm')
|
||||
}
|
||||
})
|
||||
|
||||
const renderFooter = () => {
|
||||
switch (step) {
|
||||
|
|
@ -117,9 +135,9 @@ export function MoveToJarDialog({ open, onOpenChange, walletFileName, utxo }: Mo
|
|||
<WizardStepFooter
|
||||
onBack={() => setStep('select_jar')}
|
||||
onCancel={() => handleOpenChange(false)}
|
||||
onPrimary={() => void handleSubmit()}
|
||||
onPrimary={() => void submitMove()}
|
||||
primaryDisabled={!destinationAddress}
|
||||
isLoading={isLoading}
|
||||
isLoading={isLoading || isSubmitting}
|
||||
primaryLabel={t('earn.fidelity_bond.move.text_button_submit')}
|
||||
/>
|
||||
)
|
||||
|
|
@ -150,7 +168,13 @@ export function MoveToJarDialog({ open, onOpenChange, walletFileName, utxo }: Mo
|
|||
|
||||
<FidelityBondJarSelector
|
||||
selectedJarIndex={selectedJarIndex}
|
||||
onSelect={setSelectedJarIndex}
|
||||
onSelect={(jarIndex) =>
|
||||
setValue('destinationJarIndex', jarIndex, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
}
|
||||
isJarDisabled={() => false}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
16
src/components/earn/RenewBondDialog.schema.test.ts
Normal file
16
src/components/earn/RenewBondDialog.schema.test.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { renewBondFormSchema } from './RenewBondDialog.schema'
|
||||
|
||||
describe('renewBondFormSchema', () => {
|
||||
it('accepts a lockdate after confirmation', async () => {
|
||||
const values = { lockdate: '2030-01' as const, confirmationAccepted: true }
|
||||
await expect(renewBondFormSchema.validate(values)).resolves.toEqual(values)
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ lockdate: undefined, confirmationAccepted: true },
|
||||
{ lockdate: '2030-01' as const, confirmationAccepted: false },
|
||||
])('rejects incomplete renewal values', async (values) => {
|
||||
await expect(renewBondFormSchema.isValid(values)).resolves.toBe(false)
|
||||
})
|
||||
})
|
||||
19
src/components/earn/RenewBondDialog.schema.ts
Normal file
19
src/components/earn/RenewBondDialog.schema.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import * as yup from 'yup'
|
||||
import * as fb from '@/lib/fidelityBondUtils'
|
||||
|
||||
export type RenewBondFormValues = {
|
||||
lockdate?: fb.Lockdate
|
||||
confirmationAccepted: boolean
|
||||
}
|
||||
|
||||
export const RENEW_BOND_FORM_DEFAULT_VALUES: RenewBondFormValues = {
|
||||
lockdate: undefined,
|
||||
confirmationAccepted: false,
|
||||
}
|
||||
|
||||
export const renewBondFormSchema: yup.ObjectSchema<RenewBondFormValues> = yup
|
||||
.object({
|
||||
lockdate: yup.string<fb.Lockdate>().required(),
|
||||
confirmationAccepted: yup.boolean().oneOf([true]).required(),
|
||||
})
|
||||
.required()
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import { useState } from 'react'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import { gettimelockaddressOptions } from '@joinmarket-webui/joinmarket-ng-api-ts/@tanstack/react-query'
|
||||
import type { DirectSendResponse } from '@joinmarket-webui/joinmarket-ng-api-ts/jm'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { AlertTriangleIcon, CalendarIcon, RefreshCwIcon } from 'lucide-react'
|
||||
import { useForm, useWatch } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
|
|
@ -11,6 +13,7 @@ import { useApiClient } from '@/hooks/useApiClient'
|
|||
import type { FidelityBondUtxo } from '@/hooks/useQueryUtxos'
|
||||
import * as fb from '@/lib/fidelityBondUtils'
|
||||
import type { WalletFileName } from '@/lib/utils'
|
||||
import { RENEW_BOND_FORM_DEFAULT_VALUES, renewBondFormSchema, type RenewBondFormValues } from './RenewBondDialog.schema'
|
||||
import { FidelityBondDialogLayout } from './fidelity-bond/FidelityBondDialogLayout'
|
||||
import {
|
||||
AddressPreview,
|
||||
|
|
@ -44,10 +47,25 @@ export function RenewBondDialog({ open, onOpenChange, walletFileName, utxo }: Re
|
|||
const client = useApiClient()
|
||||
|
||||
const [step, setStep] = useState<Step>('select_date')
|
||||
const [selectedLockdate, setSelectedLockdate] = useState<fb.Lockdate | ''>('')
|
||||
const [confirmationChecked, setConfirmationChecked] = useState(false)
|
||||
const [txResult, setTxResult] = useState<DirectSendResponse | undefined>()
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
formState: { isSubmitting },
|
||||
} = useForm<RenewBondFormValues>({
|
||||
mode: 'onChange',
|
||||
defaultValues: RENEW_BOND_FORM_DEFAULT_VALUES,
|
||||
resolver: yupResolver(renewBondFormSchema),
|
||||
})
|
||||
const selectedLockdate = useWatch({ control, name: 'lockdate' })
|
||||
const confirmationChecked = useWatch({
|
||||
control,
|
||||
name: 'confirmationAccepted',
|
||||
})
|
||||
|
||||
const { sweep, isLoading, error, setError, sourceJar } = useFidelityBondSweep({
|
||||
walletFileName,
|
||||
utxo,
|
||||
|
|
@ -76,8 +94,7 @@ export function RenewBondDialog({ open, onOpenChange, walletFileName, utxo }: Re
|
|||
|
||||
const handleReset = () => {
|
||||
setStep('select_date')
|
||||
setSelectedLockdate('')
|
||||
setConfirmationChecked(false)
|
||||
reset(RENEW_BOND_FORM_DEFAULT_VALUES)
|
||||
setTxResult(undefined)
|
||||
setError(undefined)
|
||||
}
|
||||
|
|
@ -89,7 +106,7 @@ export function RenewBondDialog({ open, onOpenChange, walletFileName, utxo }: Re
|
|||
onOpenChange(newOpen)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const submitRenewal = handleSubmit(async () => {
|
||||
if (!destinationAddress) return
|
||||
|
||||
setStep('sending')
|
||||
|
|
@ -99,7 +116,7 @@ export function RenewBondDialog({ open, onOpenChange, walletFileName, utxo }: Re
|
|||
toast.success(t('earn.fidelity_bond.renew.success_text'))
|
||||
})
|
||||
if (!swept) setStep('confirm')
|
||||
}
|
||||
})
|
||||
|
||||
const renderFooter = () => {
|
||||
switch (step) {
|
||||
|
|
@ -117,9 +134,9 @@ export function RenewBondDialog({ open, onOpenChange, walletFileName, utxo }: Re
|
|||
<WizardStepFooter
|
||||
onBack={() => setStep('select_date')}
|
||||
onCancel={() => handleOpenChange(false)}
|
||||
onPrimary={() => void handleSubmit()}
|
||||
onPrimary={() => void submitRenewal()}
|
||||
primaryDisabled={!confirmationChecked || !destinationAddress}
|
||||
isLoading={isLoading}
|
||||
isLoading={isLoading || isSubmitting}
|
||||
primaryLabel={t('earn.fidelity_bond.renew.text_button_submit')}
|
||||
/>
|
||||
)
|
||||
|
|
@ -148,7 +165,17 @@ export function RenewBondDialog({ open, onOpenChange, walletFileName, utxo }: Re
|
|||
<FidelityBondAmount value={utxo.value} className="text-lg" />
|
||||
</InfoCard>
|
||||
|
||||
<LockdateSelect id="renew-lockdate" value={selectedLockdate} onChange={setSelectedLockdate} />
|
||||
<LockdateSelect
|
||||
id="renew-lockdate"
|
||||
value={selectedLockdate ?? ''}
|
||||
onChange={(lockdate) =>
|
||||
setValue('lockdate', lockdate || undefined, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -193,7 +220,13 @@ export function RenewBondDialog({ open, onOpenChange, walletFileName, utxo }: Re
|
|||
<ConfirmationToggle
|
||||
id="renew-confirmation"
|
||||
checked={confirmationChecked}
|
||||
onCheckedChange={setConfirmationChecked}
|
||||
onCheckedChange={(checked) =>
|
||||
setValue('confirmationAccepted', checked, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
22
src/components/send/UtxoSelectionDialog.schema.test.ts
Normal file
22
src/components/send/UtxoSelectionDialog.schema.test.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { createUtxoSelectionFormSchema } from './UtxoSelectionDialog.schema'
|
||||
|
||||
describe('createUtxoSelectionFormSchema', () => {
|
||||
const schema = createUtxoSelectionFormSchema(['tx-a:0', 'tx-b:1'])
|
||||
|
||||
it('accepts selectable UTXO rows and a filter', async () => {
|
||||
const values = {
|
||||
filter: 'confirmed',
|
||||
rowSelection: { 'tx-a:0': true, 'tx-b:1': false },
|
||||
}
|
||||
await expect(schema.validate(values)).resolves.toEqual(values)
|
||||
})
|
||||
|
||||
it('allows clearing the selection', async () => {
|
||||
await expect(schema.isValid({ filter: '', rowSelection: {} })).resolves.toBe(true)
|
||||
})
|
||||
|
||||
it('rejects selected rows that are not available', async () => {
|
||||
await expect(schema.isValid({ filter: '', rowSelection: { 'unknown:0': true } })).resolves.toBe(false)
|
||||
})
|
||||
})
|
||||
32
src/components/send/UtxoSelectionDialog.schema.ts
Normal file
32
src/components/send/UtxoSelectionDialog.schema.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import type { RowSelectionState } from '@tanstack/react-table'
|
||||
import * as yup from 'yup'
|
||||
|
||||
export type UtxoSelectionFormValues = {
|
||||
filter: string
|
||||
rowSelection: RowSelectionState
|
||||
}
|
||||
|
||||
export const UTXO_SELECTION_FORM_DEFAULT_VALUES: UtxoSelectionFormValues = {
|
||||
filter: '',
|
||||
rowSelection: {},
|
||||
}
|
||||
|
||||
export const createUtxoSelectionFormSchema = (
|
||||
selectableUtxoIds: string[],
|
||||
): yup.ObjectSchema<UtxoSelectionFormValues> => {
|
||||
const selectableUtxoIdSet = new Set(selectableUtxoIds)
|
||||
|
||||
return yup
|
||||
.object({
|
||||
filter: yup.string().defined(),
|
||||
rowSelection: yup
|
||||
.mixed<RowSelectionState>()
|
||||
.defined()
|
||||
.test('selectable-utxos', (value) =>
|
||||
Object.entries(value).every(
|
||||
([utxoId, selected]) => typeof selected === 'boolean' && (!selected || selectableUtxoIdSet.has(utxoId)),
|
||||
),
|
||||
),
|
||||
})
|
||||
.required()
|
||||
}
|
||||
|
|
@ -37,40 +37,49 @@ export const UtxoSelectionDialog = ({
|
|||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="flex max-w-6xl flex-col overflow-hidden">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('show_utxos.title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('show_utxos.subtitle', { count: selectedCount })} {t('show_utxos.text_subtitle_addon')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form
|
||||
className="contents"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
void onSubmit()
|
||||
}}
|
||||
noValidate
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('show_utxos.title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('show_utxos.subtitle', { count: selectedCount })} {t('show_utxos.text_subtitle_addon')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Input
|
||||
className="shrink-0"
|
||||
value={filter}
|
||||
onChange={(event) => onFilterChange(event.target.value)}
|
||||
placeholder={t('jar_details.utxo_list.placeholder_search')}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<JarUtxosTable
|
||||
globalFilter={filter}
|
||||
tableEntries={tableEntries}
|
||||
pinnedEntries={[]}
|
||||
initialRowSelection={initialRowSelection}
|
||||
onRowSelectionChange={onRowSelectionChange}
|
||||
enableRowSelection={enableRowSelection}
|
||||
<Input
|
||||
className="shrink-0"
|
||||
value={filter}
|
||||
onChange={(event) => onFilterChange(event.target.value)}
|
||||
placeholder={t('jar_details.utxo_list.placeholder_search')}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<JarUtxosTable
|
||||
globalFilter={filter}
|
||||
tableEntries={tableEntries}
|
||||
pinnedEntries={[]}
|
||||
initialRowSelection={initialRowSelection}
|
||||
onRowSelectionChange={onRowSelectionChange}
|
||||
enableRowSelection={enableRowSelection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="shrink-0">
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={isSubmitting}>
|
||||
{t('modal.confirm_button_reject')}
|
||||
</Button>
|
||||
<Button type="button" onClick={() => void onSubmit()} disabled={isSubmitting}>
|
||||
{isSubmitting ? <Spinner /> : undefined}
|
||||
{t('modal.confirm_button_accept')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
<DialogFooter className="shrink-0">
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={isSubmitting}>
|
||||
{t('modal.confirm_button_reject')}
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? <Spinner /> : undefined}
|
||||
{t('modal.confirm_button_accept')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ type SeedPhraseDialogProps = WithRequiredProperty<
|
|||
autoCloseTimeout: Milliseconds
|
||||
}
|
||||
|
||||
// TODO: use react-hook-form and yup schema
|
||||
export const SeedPhraseDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,17 @@
|
|||
import { useMemo, useState } from 'react'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import { freezeMutation } from '@joinmarket-webui/joinmarket-ng-api-ts/@tanstack/react-query'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import type { RowSelectionState } from '@tanstack/react-table'
|
||||
import { useForm, useWatch } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import type { UtxoSelectionDialogProps } from '@/components/send/UtxoSelectionDialog'
|
||||
import {
|
||||
createUtxoSelectionFormSchema,
|
||||
UTXO_SELECTION_FORM_DEFAULT_VALUES,
|
||||
type UtxoSelectionFormValues,
|
||||
} from '@/components/send/UtxoSelectionDialog.schema'
|
||||
import { useJamWalletInfoContext, type AddressSummary, type Jar } from '@/context/JamWalletInfoContext'
|
||||
import { useApiClient } from '@/hooks/useApiClient'
|
||||
import type { Utxo } from '@/hooks/useQueryUtxos'
|
||||
|
|
@ -26,8 +33,6 @@ export const useUtxoSelectionDialog = ({ walletFileName, sourceJar, addressSumma
|
|||
|
||||
const { refetch: walletInfoRefetch } = useJamWalletInfoContext()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [filter, setFilter] = useState('')
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
|
||||
const tableEntries = useMemo(() => {
|
||||
return (sourceJar?.utxos || []).map((utxo) => ({
|
||||
|
|
@ -45,6 +50,26 @@ export const useUtxoSelectionDialog = ({ walletFileName, sourceJar, addressSumma
|
|||
}, {} as RowSelectionState)
|
||||
}, [sourceJar?.utxos])
|
||||
|
||||
const selectableUtxoIds = useMemo(
|
||||
() => (sourceJar?.utxos || []).filter((utxo) => !fb.utxo.isFidelityBond(utxo)).map((utxo) => utxo.utxo),
|
||||
[sourceJar?.utxos],
|
||||
)
|
||||
const formSchema = useMemo(() => createUtxoSelectionFormSchema(selectableUtxoIds), [selectableUtxoIds])
|
||||
const {
|
||||
control,
|
||||
getValues,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
formState: { isSubmitting: isFormSubmitting },
|
||||
} = useForm<UtxoSelectionFormValues>({
|
||||
mode: 'onChange',
|
||||
defaultValues: UTXO_SELECTION_FORM_DEFAULT_VALUES,
|
||||
resolver: yupResolver(formSchema),
|
||||
})
|
||||
const filter = useWatch({ control, name: 'filter', defaultValue: '' })
|
||||
const rowSelection = useWatch({ control, name: 'rowSelection', defaultValue: {} })
|
||||
|
||||
const selectedUtxos = useMemo(() => {
|
||||
return (sourceJar?.utxos || []).filter((utxo) => rowSelection[utxo.utxo] === true)
|
||||
}, [sourceJar?.utxos, rowSelection])
|
||||
|
|
@ -56,7 +81,7 @@ export const useUtxoSelectionDialog = ({ walletFileName, sourceJar, addressSumma
|
|||
retry: false,
|
||||
})
|
||||
|
||||
const { mutateAsync: applyUtxoSelectionMutateAsync, isPending: isSubmitting } = useMutation({
|
||||
const { mutateAsync: applyUtxoSelectionMutateAsync, isPending: isMutationPending } = useMutation({
|
||||
mutationFn: async ({ utxosToFreeze, utxosToUnfreeze }: { utxosToFreeze: Utxo[]; utxosToUnfreeze: Utxo[] }) => {
|
||||
const [freezeResult, unfreezeResult] = await Promise.all([
|
||||
Promise.allSettled(
|
||||
|
|
@ -94,25 +119,28 @@ export const useUtxoSelectionDialog = ({ walletFileName, sourceJar, addressSumma
|
|||
const onOpenUtxoSelector = () => {
|
||||
if (!sourceJar) return
|
||||
toast.dismiss(SEND_AUTO_SELECTION_TOAST_ID)
|
||||
setFilter('')
|
||||
setRowSelection(initialRowSelection)
|
||||
reset({
|
||||
filter: '',
|
||||
rowSelection: initialRowSelection,
|
||||
})
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
const onSubmit = handleSubmit(async ({ rowSelection: submittedRowSelection }) => {
|
||||
if (!sourceJar) return
|
||||
|
||||
const selectedUtxoIds = new Set(selectedUtxos.map((it) => it.utxo))
|
||||
const selectedAddresses = new Set(selectedUtxos.map((it) => it.address))
|
||||
const submittedUtxos = sourceJar.utxos.filter((utxo) => submittedRowSelection[utxo.utxo] === true)
|
||||
const selectedUtxoIds = new Set(submittedUtxos.map((it) => it.utxo))
|
||||
const selectedAddresses = new Set(submittedUtxos.map((it) => it.address))
|
||||
const mutableUtxos = sourceJar.utxos.filter((it) => !fb.utxo.isFidelityBond(it))
|
||||
const groupedSelectedUtxos = mutableUtxos.filter((it) => selectedAddresses.has(it.address))
|
||||
const groupedDeselectedUtxos = mutableUtxos.filter((it) => !selectedAddresses.has(it.address))
|
||||
const userDeselectedUtxos = mutableUtxos.filter((it) => !selectedUtxoIds.has(it.utxo))
|
||||
|
||||
if (groupedSelectedUtxos.length > selectedUtxos.length) {
|
||||
if (groupedSelectedUtxos.length > submittedUtxos.length) {
|
||||
toast.warning(t('jar_details.utxo_list.toast_auto_selection_title'), {
|
||||
description: t('jar_details.utxo_list.toast_auto_selected_matching', {
|
||||
count: groupedSelectedUtxos.length - selectedUtxos.length,
|
||||
count: groupedSelectedUtxos.length - submittedUtxos.length,
|
||||
}),
|
||||
id: SEND_AUTO_SELECTION_TOAST_ID,
|
||||
})
|
||||
|
|
@ -178,7 +206,9 @@ export const useUtxoSelectionDialog = ({ walletFileName, sourceJar, addressSumma
|
|||
toast.error(t('jar_details.utxo_list.toast_unfreeze_error', { count: utxosToUnfreeze.length }))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const isSubmitting = isFormSubmitting || isMutationPending
|
||||
|
||||
const dialogProps: UtxoSelectionDialogProps = {
|
||||
open,
|
||||
|
|
@ -190,11 +220,26 @@ export const useUtxoSelectionDialog = ({ walletFileName, sourceJar, addressSumma
|
|||
enableRowSelection: isSubmitting ? false : undefined,
|
||||
onOpenChange: (nextOpen: boolean) => {
|
||||
if (isSubmitting) return
|
||||
if (!nextOpen) {
|
||||
reset(UTXO_SELECTION_FORM_DEFAULT_VALUES)
|
||||
}
|
||||
setOpen(nextOpen)
|
||||
},
|
||||
onFilterChange: setFilter,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onSubmit,
|
||||
onFilterChange: (nextFilter) =>
|
||||
setValue('filter', nextFilter, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
}),
|
||||
onRowSelectionChange: (updater) => {
|
||||
const currentSelection = getValues('rowSelection')
|
||||
const nextSelection = typeof updater === 'function' ? updater(currentSelection) : updater
|
||||
setValue('rowSelection', nextSelection, {
|
||||
shouldDirty: true,
|
||||
shouldTouch: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
},
|
||||
onSubmit: async () => onSubmit(),
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue