From 7284b09661f53fee55180c6f9f40b71c56bb8f38 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Wed, 9 Sep 2026 13:21:58 +0100 Subject: [PATCH 1/3] Keep triage edit cancellable while loading --- .../paper/inbox/PaperTriageRowEdit.spec.ts | 46 ++++++++++ .../paper/inbox/PaperTriageTable.spec.ts | 91 +++++++++++++++++++ .../views/paper/inbox/PaperTriageRowEdit.vue | 35 ++++++- .../views/paper/inbox/PaperTriageTable.vue | 15 +++ 4 files changed, 186 insertions(+), 1 deletion(-) diff --git a/frontend/taskdeck-web/src/tests/views/paper/inbox/PaperTriageRowEdit.spec.ts b/frontend/taskdeck-web/src/tests/views/paper/inbox/PaperTriageRowEdit.spec.ts index 5b26af607a..33b013c2e9 100644 --- a/frontend/taskdeck-web/src/tests/views/paper/inbox/PaperTriageRowEdit.spec.ts +++ b/frontend/taskdeck-web/src/tests/views/paper/inbox/PaperTriageRowEdit.spec.ts @@ -54,6 +54,52 @@ describe('PaperTriageRowEdit', () => { // ── the editable path ────────────────────────────────────────────────────── + it('offers Cancel while the detail is loading and ignores a late response', async () => { + let resolveDetail!: (detail: CaptureItem) => void + mockCaptureStore.fetchDetail.mockImplementation( + () => new Promise((resolve) => { resolveDetail = resolve }), + ) + + const wrapper = mount(PaperTriageRowEdit, { props: { itemId: 'capture-1' } }) + await flushPromises() + + expect(wrapper.find('[data-testid="capture-edit-loading"]').exists()).toBe(true) + expect(wrapper.get('button[data-action="edit-cancel"]').text()).toContain('Cancel') + + const options = mockCaptureStore.fetchDetail.mock.calls[0][1] as { + requestOptions?: { signal?: AbortSignal } + shouldCache?: () => boolean + } + await wrapper.get('button[data-action="edit-cancel"]').trigger('click') + + expect(wrapper.emitted('close')).toHaveLength(1) + expect(options.requestOptions?.signal?.aborted).toBe(true) + expect(options.shouldCache?.()).toBe(false) + + resolveDetail(makeDetail()) + await flushPromises() + + expect(wrapper.attributes('data-edit-state')).toBe('loading') + expect(wrapper.find('[data-testid="capture-edit-textarea"]').exists()).toBe(false) + }) + + it('does not reopen the load error after loading Cancel', async () => { + let rejectDetail!: (reason?: unknown) => void + mockCaptureStore.fetchDetail.mockImplementation( + () => new Promise((_resolve, reject) => { rejectDetail = reject }), + ) + + const wrapper = mount(PaperTriageRowEdit, { props: { itemId: 'capture-1' } }) + await flushPromises() + await wrapper.get('button[data-action="edit-cancel"]').trigger('click') + + rejectDetail(new Error('late network failure')) + await flushPromises() + + expect(wrapper.attributes('data-edit-state')).toBe('loading') + expect(wrapper.find('[data-testid="capture-edit-load-error"]').exists()).toBe(false) + }) + it('loads the untruncated text rather than offering the row excerpt', async () => { const wrapper = await mountEditor() diff --git a/frontend/taskdeck-web/src/tests/views/paper/inbox/PaperTriageTable.spec.ts b/frontend/taskdeck-web/src/tests/views/paper/inbox/PaperTriageTable.spec.ts index a6dcc87b69..ab1acd509f 100644 --- a/frontend/taskdeck-web/src/tests/views/paper/inbox/PaperTriageTable.spec.ts +++ b/frontend/taskdeck-web/src/tests/views/paper/inbox/PaperTriageTable.spec.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { flushPromises, mount } from '@vue/test-utils' import { nextTick, reactive } from 'vue' import PaperTriageTable from '../../../../views/paper/inbox/PaperTriageTable.vue' +import PaperTriageRowEdit from '../../../../views/paper/inbox/PaperTriageRowEdit.vue' import type { CaptureItemSummary, CaptureStatusValue } from '../../../../types/capture' import { i18n, type SupportedLocale } from '../../../../i18n' @@ -1043,6 +1044,53 @@ describe('PaperTriageTable', () => { expect(row.find('button[data-action="accept"]').attributes('disabled')).toBeUndefined() }) + it('cancels a loading editor, keeps row controls usable, and returns focus to Edit', async () => { + let resolveDetail!: (detail: unknown) => void + mockCaptureStore.fetchDetail.mockImplementationOnce( + () => new Promise((resolve) => { resolveDetail = resolve }), + ) + const wrapper = mount(PaperTriageTable, { + props: { items: makeItems() }, + attachTo: document.body, + }) + + const editButton = wrapper.findAll('button[data-action="edit"]')[0] + await editButton.trigger('click') + await flushPromises() + + const cancelButton = wrapper.get('button[data-action="edit-cancel"]') + expect(wrapper.find('[data-testid="capture-edit-loading"]').exists()).toBe(true) + ;(cancelButton.element as HTMLButtonElement).focus() + await cancelButton.trigger('click') + await flushPromises() + + expect(wrapper.find('[data-testid="capture-edit"]').exists()).toBe(false) + expect(editButton.attributes('disabled')).toBeUndefined() + expect(document.activeElement).toBe(editButton.element) + + resolveDetail({}) + await flushPromises() + expect(wrapper.find('[data-testid="capture-edit"]').exists()).toBe(false) + wrapper.unmount() + }) + + it('does not steal focus from a persistent row control when the editor closes', async () => { + const wrapper = mount(PaperTriageTable, { + props: { items: makeItems() }, + attachTo: document.body, + }) + await wrapper.findAll('button[data-action="edit"]')[0].trigger('click') + await flushPromises() + + const persistentControl = wrapper.findAll('.paper-triage__open')[0] + ;(persistentControl.element as HTMLButtonElement).focus() + wrapper.findComponent(PaperTriageRowEdit).vm.$emit('close') + await nextTick() + + expect(document.activeElement).toBe(persistentControl.element) + wrapper.unmount() + }) + it('does not narrate an open editor as a decision', async () => { // The row is still undecided while its text is being corrected — claiming // "Sending to Review…" here is the GH-1944 lie in a new place. @@ -1282,6 +1330,49 @@ describe('PaperTriageTable', () => { expect(mockCaptureStore.updateSuggestion).not.toHaveBeenCalled() }) + it('keeps a held correction when loading Cancel closes the returning editor', async () => { + const wrapper = mount(PaperTriageTable, { props: { items: makeItems() } }) + const typed = await openEditorAndType(wrapper, 0, 'a correction behind a deferred read') + + await wrapper.setProps({ items: makeItems().slice(1) }) + await flushPromises() + await wrapper.setProps({ items: makeItems() }) + await flushPromises() + + let resolveDetail!: (detail: unknown) => void + mockCaptureStore.fetchDetail.mockImplementationOnce( + () => new Promise((resolve) => { resolveDetail = resolve }), + ) + await wrapper.findAll('button[data-action="edit"]')[0].trigger('click') + await flushPromises() + await wrapper.get('[data-testid="capture-edit-loading"] button[data-action="edit-cancel"]') + .trigger('click') + await flushPromises() + + expect(noticeKinds(wrapper)).toContain('held') + resolveDetail({}) + await flushPromises() + + mockCaptureStore.fetchDetail.mockResolvedValueOnce({ + id: 'capture-1', + userId: 'user-1', + boardId: 'board-alpha', + status: 'New', + source: 'Typed', + textExcerpt: 'First excerpt', + rawText: 'First excerpt in full', + createdAt: new Date('2026-04-25T09:42:00Z').toISOString(), + processedAt: null, + retryCount: 0, + provenance: null, + canEditSuggestion: true, + }) + await wrapper.findAll('button[data-action="edit"]')[0].trigger('click') + await flushPromises() + expect(wrapper.get('[data-testid="capture-edit-textarea"]').element.value) + .toBe(typed) + }) + it('holds the correction while another editor is open, and says that is why', async () => { const items = makeItems() items[1] = { ...items[1], status: 'New' } diff --git a/frontend/taskdeck-web/src/views/paper/inbox/PaperTriageRowEdit.vue b/frontend/taskdeck-web/src/views/paper/inbox/PaperTriageRowEdit.vue index dad78f4a31..2dcc11f9fb 100644 --- a/frontend/taskdeck-web/src/views/paper/inbox/PaperTriageRowEdit.vue +++ b/frontend/taskdeck-web/src/views/paper/inbox/PaperTriageRowEdit.vue @@ -41,7 +41,7 @@ export type PaperTriageDraftReport =