feat(web): align session action menus (#1061)

This commit is contained in:
Ananovo
2026-07-18 12:24:47 +08:00
committed by GitHub
parent bc2545d303
commit f2e3149ea8
4 changed files with 63 additions and 11 deletions
+6 -7
View File
@@ -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) {
+1 -1
View File
@@ -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 (
+12
View File
@@ -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)}