mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(web): polish session search and ordering (#551)
Align session search controls, hide the native search clear button, and keep collapsed session previews ordered by activity while still expanding previews for the selected session.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { SessionSummary } from '@/types/api'
|
||||
import { deduplicateSessionsByAgentId, getVisibleSessionPreview, normalizeSearch, sessionMatchesQuery } from './SessionList'
|
||||
import { deduplicateSessionsByAgentId, expandSelectedSessionCollapseOverrides, getVisibleSessionPreview, normalizeSearch, sessionMatchesQuery } from './SessionList'
|
||||
|
||||
function makeSession(overrides: Partial<SessionSummary> & { id: string }): SessionSummary {
|
||||
return {
|
||||
@@ -102,7 +102,7 @@ describe('session list search helpers', () => {
|
||||
})
|
||||
|
||||
describe('getVisibleSessionPreview', () => {
|
||||
it('keeps selected and active sessions inside the collapsed preview', () => {
|
||||
it('does not promote the selected session in collapsed previews', () => {
|
||||
const sessions = Array.from({ length: 6 }, (_, index) => makeSession({
|
||||
id: `s-${index + 1}`,
|
||||
active: index === 4,
|
||||
@@ -111,11 +111,10 @@ describe('getVisibleSessionPreview', () => {
|
||||
}))
|
||||
|
||||
const preview = getVisibleSessionPreview(sessions, {
|
||||
selectedSessionId: 's-6',
|
||||
limit: 3
|
||||
})
|
||||
|
||||
expect(preview.map(session => session.id)).toEqual(['s-6', 's-5', 's-1'])
|
||||
expect(preview.map(session => session.id)).toEqual(['s-5', 's-1', 's-2'])
|
||||
})
|
||||
|
||||
it('returns all sessions when expanded', () => {
|
||||
@@ -127,3 +126,34 @@ describe('getVisibleSessionPreview', () => {
|
||||
expect(getVisibleSessionPreview(sessions, { expanded: true, limit: 2 })).toHaveLength(4)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe('expandSelectedSessionCollapseOverrides', () => {
|
||||
it('expands collapsed project, machine, and session preview overrides for selected sessions', () => {
|
||||
const overrides = new Map<string, boolean>([
|
||||
['machine-1::/work/hapi', true],
|
||||
['sessions::machine-1::/work/hapi', true],
|
||||
['machine::machine-1', true]
|
||||
])
|
||||
|
||||
const result = expandSelectedSessionCollapseOverrides(overrides, {
|
||||
key: 'machine-1::/work/hapi',
|
||||
machineId: 'machine-1'
|
||||
})
|
||||
|
||||
expect(result.has('machine-1::/work/hapi')).toBe(false)
|
||||
expect(result.get('sessions::machine-1::/work/hapi')).toBe(false)
|
||||
expect(result.has('machine::machine-1')).toBe(false)
|
||||
})
|
||||
|
||||
it('sets missing session preview override to expanded', () => {
|
||||
const overrides = new Map<string, boolean>()
|
||||
|
||||
const result = expandSelectedSessionCollapseOverrides(overrides, {
|
||||
key: 'machine-1::/work/hapi',
|
||||
machineId: 'machine-1'
|
||||
})
|
||||
|
||||
expect(result.get('sessions::machine-1::/work/hapi')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -176,6 +176,36 @@ function groupSessionsByDirectory(sessions: SessionSummary[]): SessionGroup[] {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export function expandSelectedSessionCollapseOverrides(
|
||||
overrides: Map<string, boolean>,
|
||||
group: { key: string; machineId: string | null }
|
||||
): Map<string, boolean> {
|
||||
const next = new Map(overrides)
|
||||
let changed = false
|
||||
|
||||
// Expand project group if collapsed. Project and machine keys use true = collapsed.
|
||||
if (overrides.has(group.key) && overrides.get(group.key)) {
|
||||
next.delete(group.key)
|
||||
changed = true
|
||||
}
|
||||
|
||||
// Session preview keys use inverted semantics: false = expanded, true/missing = collapsed.
|
||||
const sessionPreviewKey = `sessions::${group.key}`
|
||||
if (overrides.get(sessionPreviewKey) !== false) {
|
||||
next.set(sessionPreviewKey, false)
|
||||
changed = true
|
||||
}
|
||||
|
||||
const machineKey = `machine::${group.machineId ?? UNKNOWN_MACHINE_ID}`
|
||||
if (overrides.has(machineKey) && overrides.get(machineKey)) {
|
||||
next.delete(machineKey)
|
||||
changed = true
|
||||
}
|
||||
|
||||
return changed ? next : overrides
|
||||
}
|
||||
|
||||
function groupByMachine(
|
||||
groups: SessionGroup[],
|
||||
resolveMachineLabel: (id: string | null) => string
|
||||
@@ -398,7 +428,6 @@ export function getVisibleSessionPreview(
|
||||
sessions: SessionSummary[],
|
||||
options: {
|
||||
expanded?: boolean
|
||||
selectedSessionId?: string | null
|
||||
limit?: number
|
||||
} = {}
|
||||
): SessionSummary[] {
|
||||
@@ -413,11 +442,6 @@ export function getVisibleSessionPreview(
|
||||
visible.push(session)
|
||||
}
|
||||
|
||||
const selectedSession = options.selectedSessionId
|
||||
? sessions.find(session => session.id === options.selectedSessionId)
|
||||
: undefined
|
||||
if (selectedSession) addSession(selectedSession)
|
||||
|
||||
for (const session of sessions) {
|
||||
if (visible.length >= limit) break
|
||||
if (session.active) addSession(session)
|
||||
@@ -438,19 +462,21 @@ function SessionListSearch(props: {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="relative px-3 pb-2">
|
||||
<SearchIcon className="pointer-events-none absolute left-5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-[var(--app-hint)]" />
|
||||
<div className="pointer-events-none absolute inset-y-0 left-5 flex items-center pb-2 text-[var(--app-hint)]">
|
||||
<SearchIcon className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<input
|
||||
type="search"
|
||||
value={props.value}
|
||||
onChange={(event) => props.onChange(event.target.value)}
|
||||
placeholder={t('sessions.search.placeholder')}
|
||||
className="w-full rounded-lg border border-[var(--app-border)] bg-[var(--app-bg)] py-1.5 pl-8 pr-8 text-sm text-[var(--app-fg)] outline-none transition-colors placeholder:text-[var(--app-hint)] focus:border-[var(--app-link)]"
|
||||
className="w-full appearance-none rounded-lg border border-[var(--app-border)] bg-[var(--app-bg)] py-1.5 pl-8 pr-8 text-sm text-[var(--app-fg)] outline-none transition-colors placeholder:text-[var(--app-hint)] focus:border-[var(--app-link)] [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden"
|
||||
/>
|
||||
{props.value ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.onChange('')}
|
||||
className="absolute right-5 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--app-hint)] hover:bg-[var(--app-subtle-bg)] hover:text-[var(--app-fg)]"
|
||||
className="absolute inset-y-0 right-5 flex items-center pb-2 rounded p-0.5 text-[var(--app-hint)] hover:text-[var(--app-fg)]"
|
||||
title={t('sessions.search.clear')}
|
||||
>
|
||||
<XIcon className="h-3.5 w-3.5" />
|
||||
@@ -749,8 +775,7 @@ export function SessionList(props: {
|
||||
return getVisibleSessionPreview(
|
||||
group.sessions,
|
||||
{
|
||||
expanded: isSessionGroupExpanded(group),
|
||||
selectedSessionId
|
||||
expanded: isSessionGroupExpanded(group)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -789,20 +814,7 @@ export function SessionList(props: {
|
||||
g.sessions.some(s => s.id === selectedSessionId)
|
||||
)
|
||||
if (!group) return prev
|
||||
const next = new Map(prev)
|
||||
let changed = false
|
||||
// Expand project group if collapsed
|
||||
if (prev.has(group.key) && prev.get(group.key)) {
|
||||
next.delete(group.key)
|
||||
changed = true
|
||||
}
|
||||
// Expand machine group if collapsed
|
||||
const machineKey = `machine::${group.machineId ?? UNKNOWN_MACHINE_ID}`
|
||||
if (prev.has(machineKey) && prev.get(machineKey)) {
|
||||
next.delete(machineKey)
|
||||
changed = true
|
||||
}
|
||||
return changed ? next : prev
|
||||
return expandSelectedSessionCollapseOverrides(prev, group)
|
||||
})
|
||||
}, [selectedSessionId, allGroups])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user