From c8b298ca45591a2131c1012e1172abbff12bc66b Mon Sep 17 00:00:00 2001 From: vriesd Date: Tue, 14 Jul 2026 14:01:54 +0200 Subject: [PATCH 1/2] lore(release): v0.7.19 Lore: qa-scribe cleared the second bookcase from its writing room. Sessions and library records now share one left-hand shelf, and the open page keeps its place when the scribe moves between ledgers. Release notes: - Consolidate workspace navigation and contextual master lists into a single left rail. - Remove the duplicate library list column and return that width to the active detail. - Preserve Note, Testware, or Findings when switching Sessions. - Adapt library records responsively and cover navigation with regression tests. - Bump synchronized release metadata to v0.7.19. --- CHANGELOG.md | 6 +++ Cargo.lock | 6 +-- Cargo.toml | 2 +- .../io.github.ddv1982.qa-scribe.metainfo.xml | 1 + frontend/package.json | 2 +- frontend/src/App.test.tsx | 41 ++++++++++++++++ frontend/src/app/AppShell.tsx | 37 ++++++++------ frontend/src/app/commandRegistry.ts | 2 +- frontend/src/app/useAppController.ts | 6 +++ frontend/src/styles/collections.css | 4 ++ frontend/src/styles/responsive.css | 49 ++++++++++++------- frontend/src/styles/sidebar.css | 34 ++++++++++--- frontend/src/views/OutputLibraryView.tsx | 31 +++++++----- package.json | 2 +- src-tauri/tauri.conf.json | 2 +- 15 files changed, 166 insertions(+), 59 deletions(-) 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/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..f6ae05a 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -343,6 +343,47 @@ describe('App workflows', () => { await waitFor(() => expect(screen.getByRole('textbox', { name: 'Session title' })).toHaveValue('Beta session')) }) + 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 () => { const user = userEvent.setup() render() diff --git a/frontend/src/app/AppShell.tsx b/frontend/src/app/AppShell.tsx index 9199c20..30ec48d 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,8 @@ export function AppShell(c: AppController) { ? { command: commandById('finding.new'), busy: 'manual-finding' as const } : null const visibleSessions = c.filteredSessions.slice(0, 8) + 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 +95,14 @@ export function AppShell(c: AppController) { + ) : null if (loadState === 'idle' || loadState === 'loading') { return @@ -112,7 +126,8 @@ export function OutputLibraryView({ } return ( -
+
+ {sidebarTarget && recordList ? createPortal(recordList, sidebarTarget) : null}

Cross-session library

@@ -154,18 +169,8 @@ export function OutputLibraryView({

{visibleRecords.length} of {records.length} records

) : null} -
- {visibleRecords.length > 0 ? ( - - ) : null} +
+ {!sidebarTarget ? recordList : null} {selected ? (

Session: {selected.sessionTitle}

diff --git a/package.json b/package.json index 4b9c43a..2b8f0d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "qa-scribe", - "version": "0.7.18", + "version": "0.7.19", "description": "Local-first desktop testing notepad for turning testing sessions into structured testware.", "desktopName": "qa-scribe.desktop", "homepage": "https://github.com/ddv1982/qa-scribe", diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0904543..819b980 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "QA Scribe", - "version": "0.7.18", + "version": "0.7.19", "identifier": "io.github.ddv1982.qa-scribe", "build": { "beforeDevCommand": "cd frontend && bun run dev", From 27dfe8961bc3de72ce9209e0b65ff5b817e8e9b5 Mon Sep 17 00:00:00 2001 From: vriesd Date: Tue, 14 Jul 2026 14:20:02 +0200 Subject: [PATCH 2/2] lore(ci): restore the sidebar trail --- e2e/specs/critical-workflows.e2e.mjs | 2 +- frontend/src/App.test.tsx | 9 ++------- frontend/src/app/AppShell.tsx | 3 ++- frontend/src/app/useAppController.ts | 9 +++------ frontend/src/styles/sidebar.css | 4 +++- 5 files changed, 11 insertions(+), 16 deletions(-) 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/src/App.test.tsx b/frontend/src/App.test.tsx index f6ae05a..5b28c18 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -327,20 +327,18 @@ 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 () => { @@ -355,11 +353,9 @@ describe('App workflows', () => { }), ) 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') @@ -374,7 +370,6 @@ describe('App workflows', () => { }, ]) 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' })) diff --git a/frontend/src/app/AppShell.tsx b/frontend/src/app/AppShell.tsx index 30ec48d..c27ec5c 100644 --- a/frontend/src/app/AppShell.tsx +++ b/frontend/src/app/AppShell.tsx @@ -29,6 +29,7 @@ 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' @@ -111,7 +112,7 @@ export function AppShell(c: AppController) { type="button" role="option" aria-selected={c.activeSession?.id === session.id} - tabIndex={c.activeSession?.id === session.id ? 0 : -1} + tabIndex={tabbableSessionId === session.id ? 0 : -1} disabled={c.isBusy && c.activeSession?.id !== session.id} onClick={() => void c.openSessionInCurrentView(session)} onKeyDown={handleSessionOptionKeyDown} diff --git a/frontend/src/app/useAppController.ts b/frontend/src/app/useAppController.ts index cd722d8..fac6e1c 100644 --- a/frontend/src/app/useAppController.ts +++ b/frontend/src/app/useAppController.ts @@ -248,13 +248,10 @@ export function useAppController() { requestActiveView('settings') } - function closeSettings() { - requestActiveView(settingsReturnViewRef.current) - } - - async function openSessionInCurrentView(session: (typeof sessions)[number]) { + function closeSettings() { requestActiveView(settingsReturnViewRef.current) } + function openSessionInCurrentView(session: (typeof sessions)[number]) { const destination = activeView === 'testware' || activeView === 'findings' ? activeView : 'sessions' - await sessionActions.openSession(session, true, () => setActiveView(destination)) + return sessionActions.openSession(session, true, () => setActiveView(destination)) } async function openLibraryRecord(sessionId: string, view: 'testware' | 'findings', recordId: string) { diff --git a/frontend/src/styles/sidebar.css b/frontend/src/styles/sidebar.css index e12cb2f..a019614 100644 --- a/frontend/src/styles/sidebar.css +++ b/frontend/src/styles/sidebar.css @@ -65,8 +65,10 @@ } .library-sidebar-slot { + flex: 1 1 auto; min-height: 0; - overflow: hidden; + overflow-x: hidden; + overflow-y: auto; } .library-sidebar-slot .record-master-list {