diff --git a/frontend/taskdeck-web/src/composables/useCardModal.ts b/frontend/taskdeck-web/src/composables/useCardModal.ts index 1f41e0704..fe8c71745 100644 --- a/frontend/taskdeck-web/src/composables/useCardModal.ts +++ b/frontend/taskdeck-web/src/composables/useCardModal.ts @@ -138,7 +138,7 @@ export function useCardModal(options: UseCardModalOptions) { boardStore.setEditingCard(null) } boardStore.setEditingCard(newCard.id) - void boardStore.fetchCardComments(newCard.boardId, newCard.id) + void loadCardComments(newCard) void loadCaptureProvenance() } } @@ -149,7 +149,7 @@ export function useCardModal(options: UseCardModalOptions) { async (isOpen) => { if (isOpen) { expectedUpdatedAt.value = card.value.updatedAt - await boardStore.fetchCardComments(card.value.boardId, card.value.id) + void loadCardComments(card.value) await loadCaptureProvenance() boardStore.setEditingCard(card.value.id) return @@ -176,6 +176,15 @@ export function useCardModal(options: UseCardModalOptions) { { immediate: true } ) + function loadCardComments(targetCard: Card) { + return boardStore.fetchCardComments(targetCard.boardId, targetCard.id).catch((error: unknown) => { + // The store owns the user-facing error state and toast. Keep cached comments intact + // and let the rest of the card editor continue loading, but never swallow the failure + // silently: this is the only reporting sink once the rejection is caught here. + logError('Failed to load card comments:', error) + }) + } + // Provenance async function loadCaptureProvenance() { const targetCard = card.value diff --git a/frontend/taskdeck-web/src/tests/components/CardModal.spec.ts b/frontend/taskdeck-web/src/tests/components/CardModal.spec.ts index aabf5d522..9cc78f72a 100644 --- a/frontend/taskdeck-web/src/tests/components/CardModal.spec.ts +++ b/frontend/taskdeck-web/src/tests/components/CardModal.spec.ts @@ -103,6 +103,65 @@ describe('CardModal', () => { expect(mockStore.fetchCardProvenance).toHaveBeenCalledTimes(1) }) + it('keeps the editor and cached comments usable when comment loading fails', async () => { + mockStore.getCardComments.mockReturnValue([makeOwnComment()]) + mockStore.fetchCardComments.mockRejectedValue(new Error('comments unavailable')) + + const wrapper = mount(CardModal, { + props: { card, isOpen: true, labels }, + }) + await flushPromises() + + expect(wrapper.get('#card-title').exists()).toBe(true) + expect(wrapper.text()).toContain('Delete me') + expect(mockStore.fetchCardProvenance).toHaveBeenCalledWith('board-1', 'card-1') + expect(mockStore.setEditingCard).toHaveBeenCalledWith('card-1') + + wrapper.unmount() + }) + + it('does not let a stale comment rejection disturb a newer card session', async () => { + const firstLoad = createDeferred() + const secondLoad = createDeferred() + const firstComment = makeOwnComment() + const secondComment: CardComment = { + ...firstComment, + id: 'comment-2', + cardId: 'card-2', + content: 'Card B comment', + } + mockStore.fetchCardComments.mockImplementation((_boardId: string, cardId: string) => ( + cardId === 'card-1' ? firstLoad.promise : secondLoad.promise + )) + mockStore.getCardComments.mockImplementation((cardId: string) => ( + cardId === 'card-1' ? [firstComment] : [secondComment] + )) + + const wrapper = mount(CardModal, { + props: { card, isOpen: true, labels }, + }) + await nextTick() + + const secondCard: Card = { + ...card, + id: 'card-2', + title: 'Card B', + updatedAt: '2026-08-26T12:00:00.000Z', + } + await wrapper.setProps({ card: secondCard }) + await nextTick() + + firstLoad.reject(new Error('stale comments unavailable')) + secondLoad.resolve([secondComment]) + await flushPromises() + + expect((wrapper.get('#card-title').element as HTMLInputElement).value).toBe('Card B') + expect(wrapper.text()).toContain('Card B comment') + expect(mockStore.setEditingCard).toHaveBeenLastCalledWith('card-2') + + wrapper.unmount() + }) + it('keeps provenance responses scoped to the card that requested them', async () => { let resolveFirst!: (value: any) => void let resolveSecond!: (value: any) => void