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
18 changes: 17 additions & 1 deletion frontend/taskdeck-web/src/store/board/boardCrudStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,18 @@ export function createBoardCrudActions(state: BoardState, helpers: BoardHelpers)
try {
state.loading.value = true
state.error.value = null
state.boards.value = await boardsApi.getBoards(search, includeArchived)
const freshBoards = await boardsApi.getBoards(search, includeArchived)
state.boards.value = freshBoards

// Preserve selection guard: only update activeBoardId if there is no
// current selection or the previously-selected board is no longer in the
// refreshed list (e.g. it was deleted). This prevents polling/subscription
// refreshes from resetting the user's active board to the first item.
const currentId = state.activeBoardId.value
const stillExists = currentId !== null && freshBoards.some((b) => b.id === currentId)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The check currentId !== null is redundant. If currentId is null, freshBoards.some((b) => b.id === currentId) will correctly return false (assuming board IDs are never null), which leads to the desired behavior of resetting the active board. You can simplify this line by removing the explicit null check.

Suggested change
const stillExists = currentId !== null && freshBoards.some((b) => b.id === currentId)
const stillExists = freshBoards.some((b) => b.id === currentId)

if (!stillExists) {
state.activeBoardId.value = freshBoards[0]?.id ?? null
}
} catch (e: unknown) {
helpers.handleApiError(e, 'Failed to fetch boards')
throw e
Expand Down Expand Up @@ -117,6 +128,11 @@ export function createBoardCrudActions(state: BoardState, helpers: BoardHelpers)
// Remove from boards list
state.boards.value = state.boards.value.filter((b) => b.id !== boardId)

// Clear activeBoardId if the deleted board was the active selection
if (state.activeBoardId.value === boardId) {
state.activeBoardId.value = state.boards.value[0]?.id ?? null
}

// Clear current board if it's the one being deleted
if (state.currentBoard.value && state.currentBoard.value.id === boardId) {
state.currentBoard.value = null
Expand Down
2 changes: 2 additions & 0 deletions frontend/taskdeck-web/src/store/board/boardState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface CardFilters {

export function createBoardState() {
const boards = ref<Board[]>([])
const activeBoardId = ref<string | null>(null)
const currentBoard = ref<BoardDetail | null>(null)
const currentBoardCards = ref<Card[]>([])
const currentBoardLabels = ref<Label[]>([])
Expand All @@ -38,6 +39,7 @@ export function createBoardState() {

return {
boards,
activeBoardId,
currentBoard,
currentBoardCards,
currentBoardLabels,
Expand Down
1 change: 1 addition & 0 deletions frontend/taskdeck-web/src/store/boardStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const useBoardStore = defineStore('board', () => {
return {
// State
boards: state.boards,
activeBoardId: state.activeBoardId,
currentBoard: state.currentBoard,
currentBoardCards: state.currentBoardCards,
currentBoardLabels: state.currentBoardLabels,
Expand Down
82 changes: 82 additions & 0 deletions frontend/taskdeck-web/src/tests/store/boardStore.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -698,4 +698,86 @@ describe('boardStore', () => {
expect(store.loading).toBe(false)
})
})

describe('activeBoardId — preserveSelection guard', () => {
const boardA: Board = {
id: 'board-a',
name: 'Board A',
description: '',
isArchived: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
columns: [],
}

const boardB: Board = {
id: 'board-b',
name: 'Board B',
description: '',
isArchived: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
columns: [],
}

it('sets activeBoardId to first board on initial fetchBoards when no prior selection', async () => {
vi.mocked(boardsApi.getBoards).mockResolvedValue([boardA, boardB])

await store.fetchBoards()

expect(store.activeBoardId).toBe('board-a')
})

it('preserves activeBoardId across fetchBoards when selected board still exists', async () => {
// First load — sets selection to boardA (first item)
vi.mocked(boardsApi.getBoards).mockResolvedValue([boardA, boardB])
await store.fetchBoards()
expect(store.activeBoardId).toBe('board-a')

// User selects boardB
store.activeBoardId = 'board-b'

// Poll cycle returns boards in a different order — boardB is still present
vi.mocked(boardsApi.getBoards).mockResolvedValue([boardB, boardA])
await store.fetchBoards()

// Selection must NOT flip back to boardB (now first) or boardA
expect(store.activeBoardId).toBe('board-b')
})

it('falls back to first board when the selected board is removed from the list', async () => {
vi.mocked(boardsApi.getBoards).mockResolvedValue([boardA, boardB])
await store.fetchBoards()
store.activeBoardId = 'board-b'

// boardB has been deleted on the server
vi.mocked(boardsApi.getBoards).mockResolvedValue([boardA])
await store.fetchBoards()

expect(store.activeBoardId).toBe('board-a')
})

it('sets activeBoardId to null when no boards remain after refresh', async () => {
vi.mocked(boardsApi.getBoards).mockResolvedValue([boardA])
await store.fetchBoards()
store.activeBoardId = 'board-a'

vi.mocked(boardsApi.getBoards).mockResolvedValue([])
await store.fetchBoards()

expect(store.activeBoardId).toBeNull()
})

it('clears activeBoardId when the active board is deleted', async () => {
vi.mocked(boardsApi.getBoards).mockResolvedValue([boardA, boardB])
await store.fetchBoards()
store.activeBoardId = 'board-a'

vi.mocked(boardsApi.deleteBoard).mockResolvedValue()
await store.deleteBoard('board-a')

// activeBoardId should fall back to the remaining board
expect(store.activeBoardId).toBe('board-b')
})
})
})
Loading