mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat(web): align session action menus (#1061)
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type { ApiClient } from '@/api/client'
|
||||
import type { Session } from '@/types/api'
|
||||
import {
|
||||
downloadSessionExport,
|
||||
readSessionExportFormat,
|
||||
@@ -21,7 +20,7 @@ import {
|
||||
type SessionExportDialogProps = {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
session: Session
|
||||
sessionId: string
|
||||
api: ApiClient | null
|
||||
}
|
||||
|
||||
@@ -65,14 +64,14 @@ export function SessionExportDialog(props: SessionExportDialogProps) {
|
||||
abortRef.current = controller
|
||||
|
||||
try {
|
||||
const result = await downloadSessionExport(props.api, props.session.id, format, {
|
||||
const result = await downloadSessionExport(props.api, props.sessionId, format, {
|
||||
signal: controller.signal
|
||||
})
|
||||
toast.addToast({
|
||||
title: t('session.export.toast.success.title'),
|
||||
body: t('session.export.toast.success.body', { filename: result.filename }),
|
||||
sessionId: props.session.id,
|
||||
url: `/sessions/${props.session.id}`
|
||||
sessionId: props.sessionId,
|
||||
url: `/sessions/${props.sessionId}`
|
||||
})
|
||||
props.onClose()
|
||||
} catch (error) {
|
||||
@@ -86,8 +85,8 @@ export function SessionExportDialog(props: SessionExportDialogProps) {
|
||||
toast.addToast({
|
||||
title: t('session.export.toast.error.title'),
|
||||
body: message,
|
||||
sessionId: props.session.id,
|
||||
url: `/sessions/${props.session.id}`
|
||||
sessionId: props.sessionId,
|
||||
url: `/sessions/${props.sessionId}`
|
||||
})
|
||||
} finally {
|
||||
if (abortRef.current === controller) {
|
||||
|
||||
@@ -291,7 +291,7 @@ export function SessionHeader(props: {
|
||||
<SessionExportDialog
|
||||
isOpen={exportOpen}
|
||||
onClose={() => setExportOpen(false)}
|
||||
session={session}
|
||||
sessionId={session.id}
|
||||
api={api}
|
||||
/>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { SessionSummary } from '@/types/api'
|
||||
import { I18nProvider } from '@/lib/i18n-context'
|
||||
import { ToastProvider } from '@/lib/toast-context'
|
||||
import { SessionList } from './SessionList'
|
||||
|
||||
afterEach(() => cleanup())
|
||||
@@ -38,9 +39,11 @@ function renderWithProviders(children: ReactNode) {
|
||||
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<I18nProvider>
|
||||
{children}
|
||||
</I18nProvider>
|
||||
<ToastProvider>
|
||||
<I18nProvider>
|
||||
{children}
|
||||
</I18nProvider>
|
||||
</ToastProvider>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
@@ -101,6 +104,44 @@ describe('SessionList directory action', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('SessionList action menu parity', () => {
|
||||
it.each([
|
||||
['running', true],
|
||||
['closed', false]
|
||||
] as const)('offers conversation export for a %s session', (_label, active) => {
|
||||
const session = makeSession({
|
||||
id: `session-${active ? 'running' : 'closed'}`,
|
||||
active,
|
||||
updatedAt: Date.now(),
|
||||
metadata: {
|
||||
path: '/home/ubuntu',
|
||||
machineId: 'machine-1',
|
||||
name: active ? 'Running session' : 'Closed session',
|
||||
flavor: 'codex'
|
||||
}
|
||||
})
|
||||
|
||||
renderWithProviders(
|
||||
<SessionList
|
||||
sessions={[session]}
|
||||
selectedSessionId={null}
|
||||
onSelect={vi.fn()}
|
||||
onNewSession={vi.fn()}
|
||||
onRefresh={vi.fn()}
|
||||
isLoading={false}
|
||||
renderHeader={false}
|
||||
api={null}
|
||||
/>
|
||||
)
|
||||
|
||||
fireEvent.contextMenu(screen.getByRole('button', { name: new RegExp(active ? 'Running session' : 'Closed session') }))
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: 'Export conversation' }))
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
expect(screen.getByRole('heading', { name: 'Export conversation' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('SessionList collapse behavior', () => {
|
||||
function renderSessionList(sessions: SessionSummary[], selectedSessionId = 'session-running') {
|
||||
return (
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useLongPress } from '@/hooks/useLongPress'
|
||||
import { usePlatform } from '@/hooks/usePlatform'
|
||||
import { useSessionActions } from '@/hooks/mutations/useSessionActions'
|
||||
import { SessionActionMenu } from '@/components/SessionActionMenu'
|
||||
import { SessionExportDialog } from '@/components/SessionExportDialog'
|
||||
import { RenameSessionDialog } from '@/components/RenameSessionDialog'
|
||||
import { ConfirmDialog } from '@/components/ui/ConfirmDialog'
|
||||
import { CopyIcon, CheckIcon, ScheduleIcon } from '@/components/icons'
|
||||
@@ -595,6 +596,7 @@ function SessionItem(props: {
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const [menuAnchorPoint, setMenuAnchorPoint] = useState<{ x: number; y: number }>({ x: 0, y: 0 })
|
||||
const [renameOpen, setRenameOpen] = useState(false)
|
||||
const [exportOpen, setExportOpen] = useState(false)
|
||||
const [archiveOpen, setArchiveOpen] = useState(false)
|
||||
const [deleteOpen, setDeleteOpen] = useState(false)
|
||||
const {
|
||||
@@ -748,6 +750,7 @@ function SessionItem(props: {
|
||||
onClose={() => setMenuOpen(false)}
|
||||
sessionActive={s.active}
|
||||
onRename={() => setRenameOpen(true)}
|
||||
onExport={() => setExportOpen(true)}
|
||||
onArchive={() => setArchiveOpen(true)}
|
||||
onReopen={cursorReopenDisabledReason ? undefined : handleReopen}
|
||||
reopenDisabledReason={cursorReopenDisabledReason}
|
||||
@@ -776,6 +779,15 @@ function SessionItem(props: {
|
||||
isPending={isPending}
|
||||
/>
|
||||
|
||||
{exportOpen ? (
|
||||
<SessionExportDialog
|
||||
isOpen={true}
|
||||
onClose={() => setExportOpen(false)}
|
||||
sessionId={s.id}
|
||||
api={api}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<ConfirmDialog
|
||||
isOpen={archiveOpen}
|
||||
onClose={() => setArchiveOpen(false)}
|
||||
|
||||
Reference in New Issue
Block a user