Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion frontend/src/components/project/views/ProjectKanban.vue
Original file line number Diff line number Diff line change
Expand Up @@ -624,10 +624,10 @@ async function updateTaskPosition(e) {
projectId: projectIdWithFallback.value,
}))
Object.assign(newTask, updatedTaskBucket.task)
newTask.bucketId = updatedTaskBucket.bucketId
if (updatedTaskBucket.bucketId !== newTask.bucketId) {
kanbanStore.moveTaskToBucket(newTask, updatedTaskBucket.bucketId)
}
newTask.bucketId = updatedTaskBucket.bucketId
if (updatedTaskBucket.bucket) {
kanbanStore.setBucketById(updatedTaskBucket.bucket, false)
}
Expand Down
93 changes: 93 additions & 0 deletions frontend/src/stores/kanban.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {beforeEach, describe, expect, it, vi} from 'vitest'
import {createPinia, setActivePinia} from 'pinia'

vi.mock('vue-router', () => ({
useRouter: () => ({push: vi.fn()}),
}))

vi.mock('vue-i18n', () => ({
useI18n: () => ({t: (key: string) => key}),
createI18n: () => ({global: {t: (key: string) => key}}),
}))

vi.mock('@/stores/base', () => ({
useBaseStore: () => ({
currentProject: null,
setCurrentProject: vi.fn(),
}),
}))

vi.mock('@/stores/auth', () => ({
useAuthStore: () => ({
authUser: true,
info: null,
}),
}))

import {useKanbanStore} from './kanban'

import type {IBucket} from '@/modelTypes/IBucket'
import type {ITask} from '@/modelTypes/ITask'

function makeBucket(id: number, title: string, tasks: ITask[] = []): IBucket {
return {
id,
title,
projectViewId: 1,
tasks,
count: tasks.length,
} as IBucket
}

function makeTask(id: number, bucketId: number): ITask {
return {
id,
title: `Task ${id}`,
bucketId,
} as ITask
}

describe('kanban store: moveTaskToBucket', () => {
beforeEach(() => {
setActivePinia(createPinia())
})

it('relocates a task from its current bucket into the target bucket', () => {
const kanban = useKanbanStore()
kanban.setBuckets([makeBucket(1, 'To-Do'), makeBucket(2, 'Done')])

const task = makeTask(42, 2)
kanban.addTaskToBucket(task)
expect(kanban.buckets[1].tasks.map(t => t.id)).toEqual([42])

kanban.moveTaskToBucket(task, 1)

expect(kanban.buckets[0].tasks.map(t => t.id)).toEqual([42])
expect(kanban.buckets[0].tasks[0].bucketId).toBe(1)
expect(kanban.buckets[1].tasks.map(t => t.id)).toEqual([])
})

it('is a no-op when the task is already in the target bucket', () => {
const kanban = useKanbanStore()
kanban.setBuckets([makeBucket(1, 'To-Do'), makeBucket(2, 'Done')])

const task = makeTask(42, 1)
kanban.addTaskToBucket(task)

kanban.moveTaskToBucket(task, 1)

expect(kanban.buckets[0].tasks.map(t => t.id)).toEqual([42])
expect(kanban.buckets[1].tasks.map(t => t.id)).toEqual([])
})

it('is a no-op when the task is not present in any bucket', () => {
const kanban = useKanbanStore()
kanban.setBuckets([makeBucket(1, 'To-Do'), makeBucket(2, 'Done')])

const strayTask = makeTask(99, 2)
kanban.moveTaskToBucket(strayTask, 1)

expect(kanban.buckets[0].tasks).toEqual([])
expect(kanban.buckets[1].tasks).toEqual([])
})
})
49 changes: 49 additions & 0 deletions frontend/tests/e2e/project/project-view-kanban.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,55 @@ test.describe('Project View Kanban', () => {
await expect(page.locator('.kanban .bucket:nth-child(1) .tasks')).not.toContainText(tasks[0].title)
})

test('Recurring task dropped on done bucket moves back to the default bucket', async ({authenticatedPage: page}) => {
// Reproduces https://github.com/go-vikunja/vikunja/issues/2618
const projects = await ProjectFactory.create(1)
await ProjectViewFactory.create(1, {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

no need to seed and then override again later

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Implemented in 830b546 — dropped the redundant second seed by setting done_bucket_id: 2 on the initial view create and pinning bucket IDs via {increment}.

id: 10,
project_id: projects[0].id,
view_kind: 3,
bucket_configuration_mode: 1,
})
const localBuckets = await BucketFactory.create(2, {
project_view_id: 10,
})
// Re-seed the view with done_bucket_id set now that the bucket exists
await ProjectViewFactory.create(1, {
id: 10,
project_id: projects[0].id,
view_kind: 3,
bucket_configuration_mode: 1,
done_bucket_id: localBuckets[1].id,
})

const tomorrow = new Date()
tomorrow.setDate(tomorrow.getDate() + 1)
const tasks = await TaskFactory.create(1, {
id: 100,
project_id: projects[0].id,
title: 'Recurring Task',
repeat_after: 604800,
repeat_mode: 2,
due_date: tomorrow.toISOString(),
})
await TaskBucketFactory.create(1, {
task_id: tasks[0].id,
bucket_id: localBuckets[0].id,
project_view_id: 10,
})

await page.goto(`/projects/${projects[0].id}/10`)

const sourceTask = page.locator('.kanban .bucket .tasks .task').filter({hasText: tasks[0].title}).first()
const doneBucket = page.locator('.kanban .bucket:nth-child(2) .tasks')
await sourceTask.dragTo(doneBucket)

// After the drop, the backend's recurrence redirect must be honored in the UI:
// the card appears back in the first (default/backlog) bucket with no refresh.
await expect(page.locator('.kanban .bucket:nth-child(1) .tasks')).toContainText(tasks[0].title)
await expect(page.locator('.kanban .bucket:nth-child(2) .tasks')).not.toContainText(tasks[0].title)
})

test('Should navigate to the task when the task card is clicked', async ({authenticatedPage: page}) => {
const tasks = await createTaskWithBuckets(buckets, 5)
await page.goto('/projects/1/4')
Expand Down
Loading