diff --git a/CHANGELOG.md b/CHANGELOG.md index 696ad2e..9332f04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v0.7.19 - 2026-07-14 + +- Consolidate workspace destinations and contextual Session or library records into one left rail, removing the duplicate library master-list column and returning that space to the active detail. +- Preserve the active Note, Testware, or Findings workspace when switching Sessions so the selected Session updates the current task instead of forcing a return to the Note. +- Adapt library records to a horizontal master strip on narrow windows, retain visible selection and keyboard focus behavior, and cover the unified navigation with interaction and responsive visual checks. + ## v0.7.18 - 2026-07-14 - Keep the rich-text editor instance stable while switching between read-only record previews, preventing a destroyed TipTap view from crashing React and leaving the application blank. diff --git a/Cargo.lock b/Cargo.lock index f18fefa..7943f74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2765,7 +2765,7 @@ checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" [[package]] name = "qa-scribe-app" -version = "0.7.18" +version = "0.7.19" dependencies = [ "qa-scribe-core", "serde_json", @@ -2773,7 +2773,7 @@ dependencies = [ [[package]] name = "qa-scribe-core" -version = "0.7.18" +version = "0.7.19" dependencies = [ "base64 0.22.1", "chrono", @@ -2788,7 +2788,7 @@ dependencies = [ [[package]] name = "qa-scribe-tauri" -version = "0.7.18" +version = "0.7.19" dependencies = [ "base64 0.22.1", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 5eb039e..ce849e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ resolver = "3" [workspace.package] -version = "0.7.18" +version = "0.7.19" edition = "2024" license = "MIT" authors = ["qa-scribe contributors"] diff --git a/build/linux/io.github.ddv1982.qa-scribe.metainfo.xml b/build/linux/io.github.ddv1982.qa-scribe.metainfo.xml index 29440c0..8aa7b4e 100644 --- a/build/linux/io.github.ddv1982.qa-scribe.metainfo.xml +++ b/build/linux/io.github.ddv1982.qa-scribe.metainfo.xml @@ -18,6 +18,7 @@ ProjectManagement + diff --git a/e2e/specs/critical-workflows.e2e.mjs b/e2e/specs/critical-workflows.e2e.mjs index f35496b..39ff7f5 100644 --- a/e2e/specs/critical-workflows.e2e.mjs +++ b/e2e/specs/critical-workflows.e2e.mjs @@ -9,7 +9,7 @@ async function button(label) { } async function rail(label) { - return (await $('nav[aria-label="Primary"]')).$(`button*=${label}`) + return (await $('nav[aria-label="Workspace sections"]')).$(`button*=${label}`) } async function sessionTab(label) { diff --git a/frontend/package.json b/frontend/package.json index 4cb881a..a1db8ec 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "qa-scribe-frontend", "private": true, - "version": "0.7.18", + "version": "0.7.19", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 7ddb78e..5b28c18 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -327,20 +327,56 @@ describe('App workflows', () => { }), ) render() - await waitFor(() => expect(screen.getByRole('textbox', { name: 'Session title' })).toHaveValue('Alpha session')) const sessionListbox = screen.getByRole('listbox', { name: 'Sessions' }) const alphaOption = within(sessionListbox).getByRole('option', { name: /alpha session/i }) const betaOption = within(sessionListbox).getByRole('option', { name: /beta session/i }) - alphaOption.focus() await user.keyboard('{ArrowDown}') expect(betaOption).toHaveFocus() - await user.keyboard('{Enter}') - await waitFor(() => expect(tauriMock.openSessionNoteState).toHaveBeenLastCalledWith('session-2')) await waitFor(() => expect(screen.getByRole('textbox', { name: 'Session title' })).toHaveValue('Beta session')) + await user.type(screen.getByRole('textbox', { name: 'Search Sessions' }), 'Alpha') + expect(alphaOption).toHaveAttribute('tabindex', '0') + }) + + it('keeps the active Session workspace tab when switching Sessions', async () => { + const user = userEvent.setup() + const firstSession = sessionFixture({ id: 'session-1', title: 'Alpha session' }) + const secondSession = sessionFixture({ id: 'session-2', title: 'Beta session' }) + tauriMock.listRecentSessions.mockResolvedValueOnce([firstSession, secondSession]) + tauriMock.openSessionNoteState.mockImplementation(async (sessionId: string) => + sessionNoteStateFixture({ + session: sessionId === secondSession.id ? secondSession : firstSession, + noteEntry: entryFixture({ sessionId }), + }), + ) + render() + await waitFor(() => expect(screen.getByRole('textbox', { name: 'Session title' })).toHaveValue('Alpha session')) + await user.click(screen.getByRole('tab', { name: /testware/i })) + await user.click(within(screen.getByRole('listbox', { name: 'Sessions' })).getByRole('option', { name: /beta session/i })) + await waitFor(() => expect(tauriMock.openSessionNoteState).toHaveBeenLastCalledWith('session-2')) + await waitFor(() => expect(screen.getByRole('tab', { name: /testware/i })).toHaveAttribute('aria-selected', 'true')) + expect(screen.getByLabelText('Current search scope')).toHaveTextContent('Beta session') + }) + + it('uses the single left rail as the master list for library records', async () => { + const user = userEvent.setup() + tauriMock.listDraftLibrary.mockResolvedValueOnce([ + { + draft: draftFixture({ id: 'draft-library', title: 'Checkout coverage' }), + sessionTitle: 'Checkout session', + }, + ]) + render() + await waitFor(() => expect(screen.getByRole('textbox', { name: 'Session title' })).toHaveValue('Checkout session')) + const workspaceNavigation = screen.getByRole('navigation', { name: 'Workspace sections' }) + await user.click(within(workspaceNavigation).getByRole('button', { name: 'Testware library' })) + + const libraryList = await screen.findByRole('complementary', { name: 'Testware library records' }) + expect(libraryList.closest('.left-rail')).toBeInTheDocument() + expect(screen.getByRole('heading', { name: 'Checkout coverage' })).toBeInTheDocument() }) it('creates manual testware after saving pending note edits', async () => { diff --git a/frontend/src/app/AppShell.tsx b/frontend/src/app/AppShell.tsx index 9199c20..c27ec5c 100644 --- a/frontend/src/app/AppShell.tsx +++ b/frontend/src/app/AppShell.tsx @@ -17,6 +17,7 @@ export { DeleteConfirmationDialog } from './AppOverlays' export function AppShell(c: AppController) { const [commandPaletteOpen, setCommandPaletteOpen] = useState(false) + const [librarySidebarTarget, setLibrarySidebarTarget] = useState(null) const commands = createCommandRegistry(c) const commandById = (id: AppCommandId) => commands.find((command) => command.id === id) const runCommand = (id: AppCommandId) => commandById(id)?.run() @@ -28,6 +29,9 @@ export function AppShell(c: AppController) { ? { command: commandById('finding.new'), busy: 'manual-finding' as const } : null const visibleSessions = c.filteredSessions.slice(0, 8) + const tabbableSessionId = visibleSessions.some((session) => session.id === c.activeSession?.id) ? c.activeSession?.id : visibleSessions[0]?.id + const sessionScopedView = c.activeView === 'sessions' || c.activeView === 'testware' || c.activeView === 'findings' + const libraryView = c.activeView === 'testware-library' || c.activeView === 'findings-library' useEffect(() => { function handleShortcut(event: globalThis.KeyboardEvent) { @@ -92,12 +96,14 @@ export function AppShell(c: AppController) {