fix: keep AI funding panel handoffs in one modal history entry
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<BaseModal :show="show && !funding" title="Connect your AI" max-width="max-w-xl" @close="close">
|
<BaseModal :manage-history="false" :show="show && !funding" title="Connect your AI" max-width="max-w-xl" @close="close">
|
||||||
<p class="text-sm text-white/60 mb-4">Choose how your assistant connects. API keys stay on this node and never enter your chat.</p>
|
<p class="text-sm text-white/60 mb-4">Choose how your assistant connects. API keys stay on this node and never enter your chat.</p>
|
||||||
<p v-if="loading" role="status" class="text-sm text-white/60 mb-3">Checking this node…</p>
|
<p v-if="loading" role="status" class="text-sm text-white/60 mb-3">Checking this node…</p>
|
||||||
<p v-if="error" role="alert" class="text-sm text-amber-200 mb-3">{{ error }}</p>
|
<p v-if="error" role="alert" class="text-sm text-amber-200 mb-3">{{ error }}</p>
|
||||||
@@ -28,12 +28,13 @@
|
|||||||
<button v-if="status?.local_ready" class="glass-button rounded-lg px-4 py-2 text-sm text-white mt-4 w-full" @click="useLocal">Use local AI</button>
|
<button v-if="status?.local_ready" class="glass-button rounded-lg px-4 py-2 text-sm text-white mt-4 w-full" @click="useLocal">Use local AI</button>
|
||||||
<p class="text-xs text-white/40 mt-4">Your draft stays in place when you close this window.</p>
|
<p class="text-xs text-white/40 mt-4">Your draft stays in place when you close this window.</p>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
<ReceiveBitcoinModal :show="show && funding && !scanning" initial-method="ecash" @close="finishFunding" @received="finishFunding" @scan="scanning = true" />
|
<ReceiveBitcoinModal :manage-history="false" :show="show && funding && !scanning" initial-method="ecash" @close="finishFunding" @received="finishFunding" @scan="scanning = true" />
|
||||||
<WalletScanModal :show="show && scanning" @close="finishScan" @sent="finishScan" />
|
<WalletScanModal :show="show && scanning" @close="finishScan" @sent="finishScan" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { useModalHistory } from '@/composables/useModalHistory'
|
||||||
import { rpcClient } from '@/api/rpc-client'
|
import { rpcClient } from '@/api/rpc-client'
|
||||||
import BaseModal from './BaseModal.vue'
|
import BaseModal from './BaseModal.vue'
|
||||||
import ReceiveBitcoinModal from './ReceiveBitcoinModal.vue'
|
import ReceiveBitcoinModal from './ReceiveBitcoinModal.vue'
|
||||||
@@ -52,6 +53,8 @@ interface ProviderStatus {
|
|||||||
const props = defineProps<{ show: boolean }>()
|
const props = defineProps<{ show: boolean }>()
|
||||||
const emit = defineEmits<{ close: []; configured: [provider: Choice | 'auto' | 'local', model?: string] }>()
|
const emit = defineEmits<{ close: []; configured: [provider: Choice | 'auto' | 'local', model?: string] }>()
|
||||||
const choices: { id: Choice; label: string }[] = [{ id: 'routstr', label: 'Routstr · sats' }, { id: 'claude', label: 'Claude API' }, { id: 'openai', label: 'OpenAI API' }]
|
const choices: { id: Choice; label: string }[] = [{ id: 'routstr', label: 'Routstr · sats' }, { id: 'claude', label: 'Claude API' }, { id: 'openai', label: 'OpenAI API' }]
|
||||||
|
// Keep one browser Back entry while switching between connection, funding and scan.
|
||||||
|
useModalHistory(computed(() => props.show), close)
|
||||||
const mode = ref<Choice | null>('routstr')
|
const mode = ref<Choice | null>('routstr')
|
||||||
const key = ref('')
|
const key = ref('')
|
||||||
const model = ref('')
|
const model = ref('')
|
||||||
|
|||||||
@@ -62,10 +62,13 @@ const props = withDefaults(defineProps<{
|
|||||||
maxWidth?: string
|
maxWidth?: string
|
||||||
zIndex?: string
|
zIndex?: string
|
||||||
contentClass?: string
|
contentClass?: string
|
||||||
|
/** A parent flow may own one history entry across several modal panels. */
|
||||||
|
manageHistory?: boolean
|
||||||
}>(), {
|
}>(), {
|
||||||
maxWidth: 'max-w-md',
|
maxWidth: 'max-w-md',
|
||||||
zIndex: 'z-[3000]',
|
zIndex: 'z-[3000]',
|
||||||
contentClass: '',
|
contentClass: '',
|
||||||
|
manageHistory: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -109,7 +112,7 @@ useBodyScrollLock(computed(() => props.show))
|
|||||||
// Browser/mouse/gesture Back closes the modal instead of navigating the
|
// Browser/mouse/gesture Back closes the modal instead of navigating the
|
||||||
// router out from under it — the native-app behaviour kiosk and mobile
|
// router out from under it — the native-app behaviour kiosk and mobile
|
||||||
// browsers expect (the companion webview already provides it natively).
|
// browsers expect (the companion webview already provides it natively).
|
||||||
useModalHistory(computed(() => props.show), close)
|
useModalHistory(computed(() => props.show && props.manageHistory), close)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<BaseModal :show="show && !receiveSuccess" :title="t('web5.receiveBitcoinTitle')" max-width="max-w-2xl" content-class="max-h-[90vh] overflow-y-auto" @close="close">
|
<BaseModal :manage-history="manageHistory !== false" :show="show && !receiveSuccess" :title="t('web5.receiveBitcoinTitle')" max-width="max-w-2xl" content-class="max-h-[90vh] overflow-y-auto" @close="close">
|
||||||
<!-- Method tabs -->
|
<!-- Method tabs -->
|
||||||
<div class="flex gap-1 mb-4 p-1 bg-white/5 rounded-lg">
|
<div class="flex gap-1 mb-4 p-1 bg-white/5 rounded-lg">
|
||||||
<button
|
<button
|
||||||
@@ -111,6 +111,7 @@
|
|||||||
<!-- Completion is deliberately its own modal, matching the Lightning
|
<!-- Completion is deliberately its own modal, matching the Lightning
|
||||||
payment moment. It is not an inline status inside the receive form. -->
|
payment moment. It is not an inline status inside the receive form. -->
|
||||||
<BaseModal
|
<BaseModal
|
||||||
|
:manage-history="manageHistory !== false"
|
||||||
:show="show && !!receiveSuccess"
|
:show="show && !!receiveSuccess"
|
||||||
title="Payment received"
|
title="Payment received"
|
||||||
max-width="max-w-2xl"
|
max-width="max-w-2xl"
|
||||||
@@ -147,6 +148,7 @@ const lightning = useLightningRequired()
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
show: boolean
|
show: boolean
|
||||||
|
manageHistory?: boolean
|
||||||
initialMethod?: 'lightning' | 'onchain' | 'ecash' | 'ark'
|
initialMethod?: 'lightning' | 'onchain' | 'ecash' | 'ark'
|
||||||
/** Optional info banner shown on the on-chain tab (e.g. Zeus channel limits) */
|
/** Optional info banner shown on the on-chain tab (e.g. Zeus channel limits) */
|
||||||
note?: string
|
note?: string
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import WalletScanModal from '../WalletScanModal.vue'
|
|||||||
import { rpcClient } from '@/api/rpc-client'
|
import { rpcClient } from '@/api/rpc-client'
|
||||||
vi.mock('@/api/rpc-client', () => ({ rpcClient: { call: vi.fn() } }))
|
vi.mock('@/api/rpc-client', () => ({ rpcClient: { call: vi.fn() } }))
|
||||||
vi.mock('../WalletScanModal.vue', () => ({ default: { props: ['show'], template: '<div />' } }))
|
vi.mock('../WalletScanModal.vue', () => ({ default: { props: ['show'], template: '<div />' } }))
|
||||||
vi.mock('../ReceiveBitcoinModal.vue', () => ({ default: { props: ['show', 'initialMethod'], template: '<div />' } }))
|
vi.mock('../ReceiveBitcoinModal.vue', () => ({ default: { props: ['show', 'initialMethod', 'manageHistory'], template: '<div />' } }))
|
||||||
vi.mock('@/views/settings/RoutstrBudgetSection.vue', () => ({ default: { template: '<div />' } }))
|
vi.mock('@/views/settings/RoutstrBudgetSection.vue', () => ({ default: { template: '<div />' } }))
|
||||||
const state = () => ({ schema: 1, settings: { provider: 'auto', openai_model: '' }, claude_configured: false, openai_configured: false, local_ready: false, routstr_remaining_sats: 0 })
|
const state = () => ({ schema: 1, settings: { provider: 'auto', openai_model: '' }, claude_configured: false, openai_configured: false, local_ready: false, routstr_remaining_sats: 0 })
|
||||||
function mountModal() { return mount(AIConnectionModal, { props: { show: false }, global: { stubs: { BaseModal: { props: ['show'], template: '<div v-if="show"><slot /></div>' } } } }) }
|
function mountModal() { return mount(AIConnectionModal, { props: { show: false }, global: { stubs: { BaseModal: { props: ['show'], template: '<div v-if="show"><slot /></div>' } } } }) }
|
||||||
@@ -19,7 +19,7 @@ describe('AI connection setup', () => {
|
|||||||
expect(button(w, 'Top up with ecash').exists()).toBe(true)
|
expect(button(w, 'Top up with ecash').exists()).toBe(true)
|
||||||
expect(vi.mocked(rpcClient.call).mock.calls.every(([r]) => !['assistant.budget-set', 'system.settings.set'].includes(r.method))).toBe(true)
|
expect(vi.mocked(rpcClient.call).mock.calls.every(([r]) => !['assistant.budget-set', 'system.settings.set'].includes(r.method))).toBe(true)
|
||||||
await button(w, 'Top up with ecash').trigger('click')
|
await button(w, 'Top up with ecash').trigger('click')
|
||||||
expect(w.findComponent(ReceiveBitcoinModal).props()).toMatchObject({ show: true, initialMethod: 'ecash' })
|
expect(w.findComponent(ReceiveBitcoinModal).props()).toMatchObject({ show: true, initialMethod: 'ecash', manageHistory: false })
|
||||||
w.findComponent(ReceiveBitcoinModal).vm.$emit('scan'); await flushPromises()
|
w.findComponent(ReceiveBitcoinModal).vm.$emit('scan'); await flushPromises()
|
||||||
expect(w.findComponent(WalletScanModal).props('show')).toBe(true)
|
expect(w.findComponent(WalletScanModal).props('show')).toBe(true)
|
||||||
expect(w.findComponent(ReceiveBitcoinModal).props('show')).toBe(false)
|
expect(w.findComponent(ReceiveBitcoinModal).props('show')).toBe(false)
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
import { afterEach, describe, expect, it } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||||
import BaseModal from '../BaseModal.vue'
|
import BaseModal from '../BaseModal.vue'
|
||||||
|
|
||||||
describe('BaseModal', () => {
|
describe('BaseModal', () => {
|
||||||
|
it('lets a parent flow own history while panels change', async () => {
|
||||||
|
const push = vi.spyOn(window.history, 'pushState')
|
||||||
|
const back = vi.spyOn(window.history, 'back')
|
||||||
|
const wrapper = mount(BaseModal, { props: { show: false, title: 'Panel', manageHistory: false } })
|
||||||
|
await wrapper.setProps({ show: true })
|
||||||
|
await wrapper.setProps({ show: false })
|
||||||
|
expect(push).not.toHaveBeenCalled()
|
||||||
|
expect(back).not.toHaveBeenCalled()
|
||||||
|
wrapper.unmount(); push.mockRestore(); back.mockRestore()
|
||||||
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
document.body.style.overflow = ''
|
document.body.style.overflow = ''
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user