Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions frontend/taskdeck-web/src/composables/useCardModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Expand All @@ -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
Expand All @@ -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
Expand Down
59 changes: 59 additions & 0 deletions frontend/taskdeck-web/src/tests/components/CardModal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CardComment[]>()
const secondLoad = createDeferred<CardComment[]>()
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
Expand Down
Loading